pax_global_header00006660000000000000000000000064141460147270014520gustar00rootroot0000000000000052 comment=0757658f27fc5358b64c419c744507e52e063120 exchangelib-4.6.1/000077500000000000000000000000001414601472700140015ustar00rootroot00000000000000exchangelib-4.6.1/.codacy.yaml000066400000000000000000000000731414601472700162050ustar00rootroot00000000000000--- exclude_paths: - docs/** - scripts/** - tests/** exchangelib-4.6.1/.deepsource.toml000066400000000000000000000002671414601472700171170ustar00rootroot00000000000000version = 1 test_patterns = ["tests/**"] exclude_patterns = ["scripts/**", "tests/**"] [[analyzers]] name = "python" enabled = true [analyzers.meta] runtime_version = "3.x.x" exchangelib-4.6.1/.flake8000066400000000000000000000001011414601472700151440ustar00rootroot00000000000000[flake8] max-line-length = 120 exclude = .git,__pycache__,vendor exchangelib-4.6.1/.github/000077500000000000000000000000001414601472700153415ustar00rootroot00000000000000exchangelib-4.6.1/.github/FUNDING.yml000066400000000000000000000000271414601472700171550ustar00rootroot00000000000000github: [ecederstrand] exchangelib-4.6.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001414601472700175245ustar00rootroot00000000000000exchangelib-4.6.1/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000013101414601472700222110ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. If applicable, describe how other clients like Outlook or OWA behave. **To Reproduce** If applicable, add the shortest possible script that reproduces the error. **Expected behavior** A clear and concise description of what you expected to happen. If applicable, add links to EWS documentation on MSDN or other relevant source. **Log output** If applicable, add relevant output from [debug logging](https://ecederstrand.github.io/exchangelib/#troubleshooting) . **Additional context** For example, Python and exchangelib versions. exchangelib-4.6.1/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011601414601472700232470ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** If this is concerns missing functionality from EWS, please provide links to this feature in MSDN. exchangelib-4.6.1/.github/workflows/000077500000000000000000000000001414601472700173765ustar00rootroot00000000000000exchangelib-4.6.1/.github/workflows/python-package.yml000066400000000000000000000073001414601472700230330ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python package on: push: branches: [ master ] pull_request: branches: [ master ] jobs: pre_job: # Cancels all other running workflow runs. We don't want to have two parallel test # suites running against the test server. runs-on: ubuntu-latest outputs: should_skip: ${{ steps.skip_check.outputs.should_skip }} steps: - id: skip_check uses: fkirc/skip-duplicate-actions@master with: concurrent_skipping: always cancel_others: true build: # Install all requirements, run the test suite, and clean up the server test account. runs-on: ubuntu-latest needs: pre_job strategy: matrix: python-version: ['3.7', '3.8', '3.9', '3.10'] include: # Allow failure on Python dev - e.g. Cython install regularly fails - python-version: "3.11-dev" allowed_failure: true max-parallel: 1 steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - 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 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 - name: Upgrade pip run: | python -m pip install --upgrade pip - name: Install cutting-edge Cython-based packages on Python dev versions continue-on-error: ${{ matrix.allowed_failure || false }} if: matrix.python-version == '3.11-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 - name: Install dependencies continue-on-error: ${{ matrix.allowed_failure || false }} run: | python -m pip install . python -m pip install -r test-requirements.txt - name: Test with coverage continue-on-error: ${{ matrix.allowed_failure || false }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | unittest-parallel -j 4 --class-fixtures --coverage --coverage-source exchangelib coveralls --service=github cleanup: # Clean up the server test account regardless of whether test failed runs-on: ubuntu-latest needs: build if: ${{ always() }} steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.10' - 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 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 - name: Upgrade pip run: | python -m pip install --upgrade pip - name: Install dependencies run: | python -m pip install . python -m pip install -r test-requirements.txt - name: Clean up test account run: | PYTHONPATH=./ python scripts/wipe_test_account.py exchangelib-4.6.1/.gitignore000066400000000000000000000001401414601472700157640ustar00rootroot00000000000000.eggs .idea .coverage *.pyc *.swp *.egg-info build dist __pycache__ settings.yml scratch*.py exchangelib-4.6.1/CHANGELOG.md000066400000000000000000001040211414601472700156100ustar00rootroot00000000000000Change Log ========== HEAD ---- 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: ```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() ``` - 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. 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. 4.4.0 ----- - 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. 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. 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 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. 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`. 3.3.1 ----- - 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. 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`. 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 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 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. 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 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). 2.1.1 ----- - 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()`. 2.0.1 ----- - 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: ```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) ``` - 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') 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. 1.12.5 ------ - 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. 1.12.3 ------ - 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. 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. 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. 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. 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`. 1.11.2 ------ - Bugfix release 1.11.1 ------ - 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: ```python qs = a.inbox.filter(...).iterator() qs.page_size = 123 for item in items: print(item) ``` 1.10.7 ------ - 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`. 1.10.5 ------ - Bugfix release 1.10.4 ------ - 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. 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()` 1.10.1 ------ - 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: ```python from exchangelib.protocol import BaseProtocol, NoVerifyHTTPAdapter BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter ``` 1.9.6 ----- - 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. 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. 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')` 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 1.9.1 ----- - 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. 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. 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.: ```python 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'] ``` 1.7.6 ----- - 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 1.7.4 ----- - Add Python2 support 1.7.3 ----- - 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 # 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: f.write(attachment.content) 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) item.attach(my_file) my_calendar_item = CalendarItem(...) 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) 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. - 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()`: ```python class LunchMenu(ExtendedProperty): 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') item.save() 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.: ```python item = CalendarItem(...) # Plain-text body item.body = Body('Hello UNIX-beard pine user!') # Also plain-text body, works as before 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.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.: ```python items = [] for i in range(4): 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']) 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. 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. 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.: ```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 ) 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()`. **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'], ) ``` 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'], ) ``` 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`. 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 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. 1.3.3 ----- - Add support for `organizer` attribute on `CalendarItem`. Implemented with a new `folders.Mailbox` class. 1.2 --- - Initial import exchangelib-4.6.1/CODE_OF_CONDUCT.md000066400000000000000000000064271414601472700166110ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at erik@cederstrand.dk. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq exchangelib-4.6.1/LICENSE000066400000000000000000000024411414601472700150070ustar00rootroot00000000000000Copyright (c) 2009 Erik Cederstrand Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. exchangelib-4.6.1/MANIFEST.in000066400000000000000000000001131414601472700155320ustar00rootroot00000000000000include MANIFEST.in include LICENSE include CHANGELOG.md include README.md exchangelib-4.6.1/README.md000066400000000000000000000040551414601472700152640ustar00rootroot00000000000000Exchange Web Services client library ==================================== This module is an ORM for your Exchange mailbox, providing Django-style access to all your data. It is a platform-independent, well-performing, well-behaving, well-documented, well-tested and simple interface for communicating with an on-premise Microsoft Exchange 2007-2016 server or Office365 using Exchange Web Services (EWS). Among other things, it implements autodiscover, and functions for searching, creating, updating, deleting, exporting and uploading calendar, mailbox, task, contact and distribution list items. [![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://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) ## Teaser Here's a short example of how `exchangelib` works. Let's print the first 100 inbox messages in reverse order: ```python from exchangelib import Credentials, Account 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]: print(item.subject, item.sender, item.datetime_received) ``` ## Documentation Documentation is available at [https://ecederstrand.github.io/exchangelib/](https://ecederstrand.github.io/exchangelib/). Source code documentation is available at [https://ecederstrand.github.io/exchangelib/exchangelib/](https://ecederstrand.github.io/exchangelib/exchangelib/). exchangelib-4.6.1/docs/000077500000000000000000000000001414601472700147315ustar00rootroot00000000000000exchangelib-4.6.1/docs/_config.yml000066400000000000000000000003641414601472700170630ustar00rootroot00000000000000--- theme: jekyll-theme-minimal title : exchangelib author : name : Erik Cederstrand email : erik@cederstrand.dk github : ecederstrand markdown: kramdown github: username : ecederstrand project : exchangelib exchangelib-4.6.1/docs/assets/000077500000000000000000000000001414601472700162335ustar00rootroot00000000000000exchangelib-4.6.1/docs/assets/css/000077500000000000000000000000001414601472700170235ustar00rootroot00000000000000exchangelib-4.6.1/docs/assets/css/style.scss000066400000000000000000000004531414601472700210620ustar00rootroot00000000000000--- --- @import "{{ site.theme }}"; @media screen and (min-device-width:768px) { section { width:850px; float:right; padding-bottom:50px; } .wrapper { width:1210px; margin:0 auto; } } @media screen and (max-device-width:768px) { section { font-size: 75%; } } exchangelib-4.6.1/docs/exchangelib/000077500000000000000000000000001414601472700172025ustar00rootroot00000000000000exchangelib-4.6.1/docs/exchangelib/account.html000066400000000000000000004140061414601472700215310ustar00rootroot00000000000000 exchangelib.account API documentation

Module exchangelib.account

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:
    """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):
        """

        :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))
        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)

        # 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)

    @property
    def primary_smtp_address(self):
        return self.identity.primary_smtp_address

    @threaded_cached_property
    def admin_audit_logs(self):
        return self.root.get_default_folder(AdminAuditLogs)

    @threaded_cached_property
    def archive_deleted_items(self):
        return self.archive_root.get_default_folder(ArchiveDeletedItems)

    @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

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) :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:

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):
        """

        :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))
        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)

        # 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)

    @property
    def primary_smtp_address(self):
        return self.identity.primary_smtp_address

    @threaded_cached_property
    def admin_audit_logs(self):
        return self.root.get_default_folder(AdminAuditLogs)

    @threaded_cached_property
    def archive_deleted_items(self):
        return self.archive_root.get_default_folder(ArchiveDeletedItems)

    @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

    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_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
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_purges
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_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_versions
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_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 calendar
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 conflicts
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 contacts
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 conversation_history
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 delegates

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

Expand source code
@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
var directory
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 domain
Expand source code
@property
def domain(self):
    return get_domain(self.primary_smtp_address)
var drafts
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 favorites
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 im_contact_list
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 inbox
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 journal
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 junk
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 local_failures
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 mail_tips

See self.oof_settings about caching considerations.

Expand source code
@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',
    )
var 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 my_contacts
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 notes
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 oof_settings
Expand source code
@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),
    )
var outbox
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 people_connect
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 primary_smtp_address
Expand source code
@property
def primary_smtp_address(self):
    return self.identity.primary_smtp_address
var public_folders_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 quick_contacts
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 recipient_cache
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 recoverable_items_deletions
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 recoverable_items_purges
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 recoverable_items_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 recoverable_items_versions
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 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 search_folders
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 sent
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 server_failures
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 sync_issues
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 tasks
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))
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 trash
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 voice_mail
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))

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
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_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
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_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):
    """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_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):
    """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_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
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 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
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_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
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_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):
    """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 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
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 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
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

Expand source code
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)
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
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={})
    )
class Identity (primary_smtp_address=None, smtp_address=None, upn=None, sid=None)

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:
    """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))
exchangelib-4.6.1/docs/exchangelib/attachments.html000066400000000000000000001522151414601472700224110ustar00rootroot00000000000000 exchangelib.attachments API documentation

Module exchangelib.attachments

Expand source code
import io
import logging
import mimetypes

from .fields import BooleanField, TextField, IntegerField, URIField, DateTimeField, EWSElementField, Base64Field, \
    ItemField, IdField, FieldPath
from .properties import EWSElement, EWSMeta
from .services import GetAttachment, CreateAttachment, DeleteAttachment

log = logging.getLogger(__name__)


class AttachmentId(EWSElement):
    """'id' and 'changekey' are UUIDs generated by Exchange.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/attachmentid
    """

    ELEMENT_NAME = 'AttachmentId'

    ID_ATTR = 'Id'
    ROOT_ID_ATTR = 'RootItemId'
    ROOT_CHANGEKEY_ATTR = 'RootItemChangeKey'

    id = IdField(field_uri=ID_ATTR, is_required=True)
    root_id = IdField(field_uri=ROOT_ID_ATTR)
    root_changekey = IdField(field_uri=ROOT_CHANGEKEY_ATTR)


class Attachment(EWSElement, metaclass=EWSMeta):
    """Base class for FileAttachment and ItemAttachment."""

    attachment_id = EWSElementField(value_cls=AttachmentId)
    name = TextField(field_uri='Name')
    content_type = TextField(field_uri='ContentType')
    content_id = TextField(field_uri='ContentId')
    content_location = URIField(field_uri='ContentLocation')
    size = IntegerField(field_uri='Size', is_read_only=True)  # Attachment size in bytes
    last_modified_time = DateTimeField(field_uri='LastModifiedTime')
    is_inline = BooleanField(field_uri='IsInline')

    __slots__ = 'parent_item',

    def __init__(self, **kwargs):
        self.parent_item = kwargs.pop('parent_item', None)
        super().__init__(**kwargs)

    def clean(self, version=None):
        from .items import Item
        if self.parent_item is not None and not isinstance(self.parent_item, Item):
            raise ValueError("'parent_item' value %r must be an Item instance" % self.parent_item)
        if self.content_type is None and self.name is not None:
            self.content_type = mimetypes.guess_type(self.name)[0] or 'application/octet-stream'
        super().clean(version=version)

    def attach(self):
        # Adds this attachment to an item and updates the changekey of the parent item
        if self.attachment_id:
            raise ValueError('This attachment has already been created')
        if not self.parent_item or not self.parent_item.account:
            raise ValueError('Parent item %s must have an account' % self.parent_item)
        item = CreateAttachment(account=self.parent_item.account).get(parent_item=self.parent_item, items=[self])
        attachment_id = item.attachment_id
        if attachment_id.root_id != self.parent_item.id:
            raise ValueError("'root_id' vs. 'id' mismatch")
        if attachment_id.root_changekey == self.parent_item.changekey:
            raise ValueError('root_id changekey match')
        self.parent_item.changekey = attachment_id.root_changekey
        # EWS does not like receiving root_id and root_changekey on subsequent requests
        attachment_id.root_id = None
        attachment_id.root_changekey = None
        self.attachment_id = attachment_id

    def detach(self):
        # Deletes an attachment remotely and updates the changekey of the parent item
        if not self.attachment_id:
            raise ValueError('This attachment has not been created')
        if not self.parent_item or not self.parent_item.account:
            raise ValueError('Parent item %s must have an account' % self.parent_item)
        root_item_id = DeleteAttachment(account=self.parent_item.account).get(items=[self.attachment_id])
        if root_item_id.id != self.parent_item.id:
            raise ValueError("'root_item_id.id' mismatch")
        if root_item_id.changekey == self.parent_item.changekey:
            raise ValueError("'root_item_id.changekey' match")
        self.parent_item.changekey = root_item_id.changekey
        self.parent_item = None
        self.attachment_id = None

    def __hash__(self):
        if self.attachment_id:
            return hash(self.attachment_id)
        # Be careful to avoid recursion on the back-reference to the parent item
        return hash(tuple(getattr(self, f) for f in self._slots_keys if f != 'parent_item'))

    def __repr__(self):
        return self.__class__.__name__ + '(%s)' % ', '.join(
            '%s=%r' % (f.name, getattr(self, f.name)) for f in self.FIELDS if f.name not in ('_item', '_content')
        )


class FileAttachment(Attachment):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/fileattachment"""

    ELEMENT_NAME = 'FileAttachment'

    is_contact_photo = BooleanField(field_uri='IsContactPhoto')
    _content = Base64Field(field_uri='Content')

    __slots__ = '_fp',

    def __init__(self, **kwargs):
        kwargs['_content'] = kwargs.pop('content', None)
        super().__init__(**kwargs)
        self._fp = None

    @property
    def fp(self):
        # Return a file-like object for the content. This avoids creating multiple in-memory copies of the content.
        if self._fp is None:
            self._init_fp()
        return self._fp

    def _init_fp(self):
        # Create a file-like object for the attachment content. We try hard to reduce memory consumption so we never
        # store the full attachment content in-memory.
        if not self.parent_item or not self.parent_item.account:
            raise ValueError('%s must have an account' % self.__class__.__name__)
        self._fp = FileAttachmentIO(attachment=self)

    @property
    def content(self):
        """Return the attachment content. Stores a local copy of the content in case you want to upload the attachment
        again later.
        """
        if self.attachment_id is None:
            return self._content
        if self._content is not None:
            return self._content
        # We have an ID to the data but still haven't called GetAttachment to get the actual data. Do that now.
        with self.fp as fp:
            self._content = fp.read()
        return self._content

    @content.setter
    def content(self, value):
        """Replace the attachment content."""
        if not isinstance(value, bytes):
            raise ValueError("'value' %r must be a bytes object" % value)
        self._content = value

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
        kwargs['content'] = kwargs.pop('_content')
        cls._clear(elem)
        return cls(**kwargs)

    def to_xml(self, version):
        self._content = self.content  # Make sure content is available, to avoid ErrorRequiredPropertyMissing
        return super().to_xml(version=version)

    def __getstate__(self):
        # The fp does not need to be pickled
        state = {k: getattr(self, k) for k in self._slots_keys}
        del state['_fp']
        return state

    def __setstate__(self, state):
        # Restore the fp
        for k in self._slots_keys:
            setattr(self, k, state.get(k))
        self._fp = None


class ItemAttachment(Attachment):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/itemattachment"""

    ELEMENT_NAME = 'ItemAttachment'

    _item = ItemField(field_uri='Item')

    def __init__(self, **kwargs):
        kwargs['_item'] = kwargs.pop('item', None)
        super().__init__(**kwargs)

    @property
    def item(self):
        from .folders import BaseFolder
        if self.attachment_id is None:
            return self._item
        if self._item is not None:
            return self._item
        # We have an ID to the data but still haven't called GetAttachment to get the actual data. Do that now.
        if not self.parent_item or not self.parent_item.account:
            raise ValueError('%s must have an account' % self.__class__.__name__)
        additional_fields = {
            FieldPath(field=f) for f in BaseFolder.allowed_item_fields(version=self.parent_item.account.version)
        }
        attachment = GetAttachment(account=self.parent_item.account).get(
            items=[self.attachment_id], include_mime_content=True, body_type=None, filter_html_content=None,
            additional_fields=additional_fields,
        )
        self._item = attachment.item
        return self._item

    @item.setter
    def item(self, value):
        from .items import Item
        if not isinstance(value, Item):
            raise ValueError("'value' %r must be an Item object" % value)
        self._item = value

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
        kwargs['item'] = kwargs.pop('_item')
        cls._clear(elem)
        return cls(**kwargs)


class FileAttachmentIO(io.RawIOBase):
    """A BytesIO where the stream of data comes from the GetAttachment service."""

    def __init__(self, attachment):
        self._attachment = attachment
        self._overflow = None

    def readable(self):
        return True

    @property
    def closed(self):
        return self._stream is None

    def readinto(self, b):
        buf_size = len(b)  # We can't return more than l bytes
        try:
            chunk = self._overflow or next(self._stream)
        except StopIteration:
            return 0
        else:
            output, self._overflow = chunk[:buf_size], chunk[buf_size:]
            b[:len(output)] = output
            return len(output)

    def __enter__(self):
        self._stream = GetAttachment(account=self._attachment.parent_item.account).stream_file_content(
            attachment_id=self._attachment.attachment_id
        )
        self._overflow = None
        return io.BufferedReader(self, buffer_size=io.DEFAULT_BUFFER_SIZE)

    def __exit__(self, *args, **kwargs):
        self._stream = None
        self._overflow = None

Classes

class Attachment (**kwargs)

Base class for FileAttachment and ItemAttachment.

Expand source code
class Attachment(EWSElement, metaclass=EWSMeta):
    """Base class for FileAttachment and ItemAttachment."""

    attachment_id = EWSElementField(value_cls=AttachmentId)
    name = TextField(field_uri='Name')
    content_type = TextField(field_uri='ContentType')
    content_id = TextField(field_uri='ContentId')
    content_location = URIField(field_uri='ContentLocation')
    size = IntegerField(field_uri='Size', is_read_only=True)  # Attachment size in bytes
    last_modified_time = DateTimeField(field_uri='LastModifiedTime')
    is_inline = BooleanField(field_uri='IsInline')

    __slots__ = 'parent_item',

    def __init__(self, **kwargs):
        self.parent_item = kwargs.pop('parent_item', None)
        super().__init__(**kwargs)

    def clean(self, version=None):
        from .items import Item
        if self.parent_item is not None and not isinstance(self.parent_item, Item):
            raise ValueError("'parent_item' value %r must be an Item instance" % self.parent_item)
        if self.content_type is None and self.name is not None:
            self.content_type = mimetypes.guess_type(self.name)[0] or 'application/octet-stream'
        super().clean(version=version)

    def attach(self):
        # Adds this attachment to an item and updates the changekey of the parent item
        if self.attachment_id:
            raise ValueError('This attachment has already been created')
        if not self.parent_item or not self.parent_item.account:
            raise ValueError('Parent item %s must have an account' % self.parent_item)
        item = CreateAttachment(account=self.parent_item.account).get(parent_item=self.parent_item, items=[self])
        attachment_id = item.attachment_id
        if attachment_id.root_id != self.parent_item.id:
            raise ValueError("'root_id' vs. 'id' mismatch")
        if attachment_id.root_changekey == self.parent_item.changekey:
            raise ValueError('root_id changekey match')
        self.parent_item.changekey = attachment_id.root_changekey
        # EWS does not like receiving root_id and root_changekey on subsequent requests
        attachment_id.root_id = None
        attachment_id.root_changekey = None
        self.attachment_id = attachment_id

    def detach(self):
        # Deletes an attachment remotely and updates the changekey of the parent item
        if not self.attachment_id:
            raise ValueError('This attachment has not been created')
        if not self.parent_item or not self.parent_item.account:
            raise ValueError('Parent item %s must have an account' % self.parent_item)
        root_item_id = DeleteAttachment(account=self.parent_item.account).get(items=[self.attachment_id])
        if root_item_id.id != self.parent_item.id:
            raise ValueError("'root_item_id.id' mismatch")
        if root_item_id.changekey == self.parent_item.changekey:
            raise ValueError("'root_item_id.changekey' match")
        self.parent_item.changekey = root_item_id.changekey
        self.parent_item = None
        self.attachment_id = None

    def __hash__(self):
        if self.attachment_id:
            return hash(self.attachment_id)
        # Be careful to avoid recursion on the back-reference to the parent item
        return hash(tuple(getattr(self, f) for f in self._slots_keys if f != 'parent_item'))

    def __repr__(self):
        return self.__class__.__name__ + '(%s)' % ', '.join(
            '%s=%r' % (f.name, getattr(self, f.name)) for f in self.FIELDS if f.name not in ('_item', '_content')
        )

Ancestors

Subclasses

Class variables

var FIELDS

Instance variables

var attachment_id
var content_id
var content_location
var content_type
var is_inline
var last_modified_time
var name
var parent_item

Return an attribute of instance, which is of type owner.

var size

Methods

def attach(self)
Expand source code
def attach(self):
    # Adds this attachment to an item and updates the changekey of the parent item
    if self.attachment_id:
        raise ValueError('This attachment has already been created')
    if not self.parent_item or not self.parent_item.account:
        raise ValueError('Parent item %s must have an account' % self.parent_item)
    item = CreateAttachment(account=self.parent_item.account).get(parent_item=self.parent_item, items=[self])
    attachment_id = item.attachment_id
    if attachment_id.root_id != self.parent_item.id:
        raise ValueError("'root_id' vs. 'id' mismatch")
    if attachment_id.root_changekey == self.parent_item.changekey:
        raise ValueError('root_id changekey match')
    self.parent_item.changekey = attachment_id.root_changekey
    # EWS does not like receiving root_id and root_changekey on subsequent requests
    attachment_id.root_id = None
    attachment_id.root_changekey = None
    self.attachment_id = attachment_id
def clean(self, version=None)
Expand source code
def clean(self, version=None):
    from .items import Item
    if self.parent_item is not None and not isinstance(self.parent_item, Item):
        raise ValueError("'parent_item' value %r must be an Item instance" % self.parent_item)
    if self.content_type is None and self.name is not None:
        self.content_type = mimetypes.guess_type(self.name)[0] or 'application/octet-stream'
    super().clean(version=version)
def detach(self)
Expand source code
def detach(self):
    # Deletes an attachment remotely and updates the changekey of the parent item
    if not self.attachment_id:
        raise ValueError('This attachment has not been created')
    if not self.parent_item or not self.parent_item.account:
        raise ValueError('Parent item %s must have an account' % self.parent_item)
    root_item_id = DeleteAttachment(account=self.parent_item.account).get(items=[self.attachment_id])
    if root_item_id.id != self.parent_item.id:
        raise ValueError("'root_item_id.id' mismatch")
    if root_item_id.changekey == self.parent_item.changekey:
        raise ValueError("'root_item_id.changekey' match")
    self.parent_item.changekey = root_item_id.changekey
    self.parent_item = None
    self.attachment_id = None

Inherited members

class AttachmentId (**kwargs)
Expand source code
class AttachmentId(EWSElement):
    """'id' and 'changekey' are UUIDs generated by Exchange.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/attachmentid
    """

    ELEMENT_NAME = 'AttachmentId'

    ID_ATTR = 'Id'
    ROOT_ID_ATTR = 'RootItemId'
    ROOT_CHANGEKEY_ATTR = 'RootItemChangeKey'

    id = IdField(field_uri=ID_ATTR, is_required=True)
    root_id = IdField(field_uri=ROOT_ID_ATTR)
    root_changekey = IdField(field_uri=ROOT_CHANGEKEY_ATTR)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var ID_ATTR
var ROOT_CHANGEKEY_ATTR
var ROOT_ID_ATTR

Instance variables

var id
var root_changekey
var root_id

Inherited members

class FileAttachment (**kwargs)
Expand source code
class FileAttachment(Attachment):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/fileattachment"""

    ELEMENT_NAME = 'FileAttachment'

    is_contact_photo = BooleanField(field_uri='IsContactPhoto')
    _content = Base64Field(field_uri='Content')

    __slots__ = '_fp',

    def __init__(self, **kwargs):
        kwargs['_content'] = kwargs.pop('content', None)
        super().__init__(**kwargs)
        self._fp = None

    @property
    def fp(self):
        # Return a file-like object for the content. This avoids creating multiple in-memory copies of the content.
        if self._fp is None:
            self._init_fp()
        return self._fp

    def _init_fp(self):
        # Create a file-like object for the attachment content. We try hard to reduce memory consumption so we never
        # store the full attachment content in-memory.
        if not self.parent_item or not self.parent_item.account:
            raise ValueError('%s must have an account' % self.__class__.__name__)
        self._fp = FileAttachmentIO(attachment=self)

    @property
    def content(self):
        """Return the attachment content. Stores a local copy of the content in case you want to upload the attachment
        again later.
        """
        if self.attachment_id is None:
            return self._content
        if self._content is not None:
            return self._content
        # We have an ID to the data but still haven't called GetAttachment to get the actual data. Do that now.
        with self.fp as fp:
            self._content = fp.read()
        return self._content

    @content.setter
    def content(self, value):
        """Replace the attachment content."""
        if not isinstance(value, bytes):
            raise ValueError("'value' %r must be a bytes object" % value)
        self._content = value

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
        kwargs['content'] = kwargs.pop('_content')
        cls._clear(elem)
        return cls(**kwargs)

    def to_xml(self, version):
        self._content = self.content  # Make sure content is available, to avoid ErrorRequiredPropertyMissing
        return super().to_xml(version=version)

    def __getstate__(self):
        # The fp does not need to be pickled
        state = {k: getattr(self, k) for k in self._slots_keys}
        del state['_fp']
        return state

    def __setstate__(self, state):
        # Restore the fp
        for k in self._slots_keys:
            setattr(self, k, state.get(k))
        self._fp = None

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
    kwargs['content'] = kwargs.pop('_content')
    cls._clear(elem)
    return cls(**kwargs)

Instance variables

var content

Return the attachment content. Stores a local copy of the content in case you want to upload the attachment again later.

Expand source code
@property
def content(self):
    """Return the attachment content. Stores a local copy of the content in case you want to upload the attachment
    again later.
    """
    if self.attachment_id is None:
        return self._content
    if self._content is not None:
        return self._content
    # We have an ID to the data but still haven't called GetAttachment to get the actual data. Do that now.
    with self.fp as fp:
        self._content = fp.read()
    return self._content
var fp
Expand source code
@property
def fp(self):
    # Return a file-like object for the content. This avoids creating multiple in-memory copies of the content.
    if self._fp is None:
        self._init_fp()
    return self._fp
var is_contact_photo

Methods

def to_xml(self, version)
Expand source code
def to_xml(self, version):
    self._content = self.content  # Make sure content is available, to avoid ErrorRequiredPropertyMissing
    return super().to_xml(version=version)

Inherited members

class FileAttachmentIO (attachment)

A BytesIO where the stream of data comes from the GetAttachment service.

Expand source code
class FileAttachmentIO(io.RawIOBase):
    """A BytesIO where the stream of data comes from the GetAttachment service."""

    def __init__(self, attachment):
        self._attachment = attachment
        self._overflow = None

    def readable(self):
        return True

    @property
    def closed(self):
        return self._stream is None

    def readinto(self, b):
        buf_size = len(b)  # We can't return more than l bytes
        try:
            chunk = self._overflow or next(self._stream)
        except StopIteration:
            return 0
        else:
            output, self._overflow = chunk[:buf_size], chunk[buf_size:]
            b[:len(output)] = output
            return len(output)

    def __enter__(self):
        self._stream = GetAttachment(account=self._attachment.parent_item.account).stream_file_content(
            attachment_id=self._attachment.attachment_id
        )
        self._overflow = None
        return io.BufferedReader(self, buffer_size=io.DEFAULT_BUFFER_SIZE)

    def __exit__(self, *args, **kwargs):
        self._stream = None
        self._overflow = None

Ancestors

  • io.RawIOBase
  • _io._RawIOBase
  • io.IOBase
  • _io._IOBase

Instance variables

var closed
Expand source code
@property
def closed(self):
    return self._stream is None

Methods

def readable(self)

Return whether object was opened for reading.

If False, read() will raise OSError.

Expand source code
def readable(self):
    return True
def readinto(self, b)
Expand source code
def readinto(self, b):
    buf_size = len(b)  # We can't return more than l bytes
    try:
        chunk = self._overflow or next(self._stream)
    except StopIteration:
        return 0
    else:
        output, self._overflow = chunk[:buf_size], chunk[buf_size:]
        b[:len(output)] = output
        return len(output)
class ItemAttachment (**kwargs)
Expand source code
class ItemAttachment(Attachment):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/itemattachment"""

    ELEMENT_NAME = 'ItemAttachment'

    _item = ItemField(field_uri='Item')

    def __init__(self, **kwargs):
        kwargs['_item'] = kwargs.pop('item', None)
        super().__init__(**kwargs)

    @property
    def item(self):
        from .folders import BaseFolder
        if self.attachment_id is None:
            return self._item
        if self._item is not None:
            return self._item
        # We have an ID to the data but still haven't called GetAttachment to get the actual data. Do that now.
        if not self.parent_item or not self.parent_item.account:
            raise ValueError('%s must have an account' % self.__class__.__name__)
        additional_fields = {
            FieldPath(field=f) for f in BaseFolder.allowed_item_fields(version=self.parent_item.account.version)
        }
        attachment = GetAttachment(account=self.parent_item.account).get(
            items=[self.attachment_id], include_mime_content=True, body_type=None, filter_html_content=None,
            additional_fields=additional_fields,
        )
        self._item = attachment.item
        return self._item

    @item.setter
    def item(self, value):
        from .items import Item
        if not isinstance(value, Item):
            raise ValueError("'value' %r must be an Item object" % value)
        self._item = value

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
        kwargs['item'] = kwargs.pop('_item')
        cls._clear(elem)
        return cls(**kwargs)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
    kwargs['item'] = kwargs.pop('_item')
    cls._clear(elem)
    return cls(**kwargs)

Instance variables

var item
Expand source code
@property
def item(self):
    from .folders import BaseFolder
    if self.attachment_id is None:
        return self._item
    if self._item is not None:
        return self._item
    # We have an ID to the data but still haven't called GetAttachment to get the actual data. Do that now.
    if not self.parent_item or not self.parent_item.account:
        raise ValueError('%s must have an account' % self.__class__.__name__)
    additional_fields = {
        FieldPath(field=f) for f in BaseFolder.allowed_item_fields(version=self.parent_item.account.version)
    }
    attachment = GetAttachment(account=self.parent_item.account).get(
        items=[self.attachment_id], include_mime_content=True, body_type=None, filter_html_content=None,
        additional_fields=additional_fields,
    )
    self._item = attachment.item
    return self._item

Inherited members

exchangelib-4.6.1/docs/exchangelib/autodiscover/000077500000000000000000000000001414601472700217115ustar00rootroot00000000000000exchangelib-4.6.1/docs/exchangelib/autodiscover/cache.html000066400000000000000000000545761414601472700236630ustar00rootroot00000000000000 exchangelib.autodiscover.cache API documentation

Module exchangelib.autodiscover.cache

Expand source code
import getpass
import glob
import logging
import os
import shelve
import sys
import tempfile
from contextlib import contextmanager
from threading import RLock

from .protocol import AutodiscoverProtocol
from ..configuration import Configuration

log = logging.getLogger(__name__)


def shelve_filename():
    # Add the version of the cache format to the filename. If we change the format of the cached data, this version
    # must be bumped. Otherwise, new versions of this package cannot open cache files generated by older versions.
    version = 2
    # 'shelve' may pickle objects using different pickle protocol versions. Append the python major+minor version
    # numbers to the filename. Also append the username, to avoid permission errors.
    major, minor = sys.version_info[:2]
    try:
        user = getpass.getuser()
    except KeyError:
        # getuser() fails on some systems. Provide a sane default. See issue #448
        user = 'exchangelib'
    return 'exchangelib.{version}.cache.{user}.py{major}{minor}'.format(
        version=version, user=user, major=major, minor=minor
    )


AUTODISCOVER_PERSISTENT_STORAGE = os.path.join(tempfile.gettempdir(), shelve_filename())


@contextmanager
def shelve_open_with_failover(filename):
    # We can expect empty or corrupt files. Whatever happens, just delete the cache file and try again.
    # 'shelve' may add a backend-specific suffix to the file, so also delete all files with a suffix.
    # We don't know which file caused the error, so just delete them all.
    try:
        shelve_handle = shelve.open(filename)
        # Try to actually use the shelve. Some implementations may allow opening the file but then throw
        # errors on access.
        try:
            _ = shelve_handle['']
        except KeyError:
            # The entry doesn't exist. This is expected.
            pass
    except Exception as e:
        for f in glob.glob(filename + '*'):
            log.warning('Deleting invalid cache file %s (%r)', f, e)
            os.unlink(f)
        shelve_handle = shelve.open(filename)
    yield shelve_handle


class AutodiscoverCache:
    """Stores the translation from (email domain, credentials) -> AutodiscoverProtocol object so we can re-use TCP
    connections to an autodiscover server within the same process. Also persists the email domain -> (autodiscover
    endpoint URL, auth_type) translation to the filesystem so the cache can be shared between multiple processes.

    According to Microsoft, we may forever cache the (email domain -> autodiscover endpoint URL) mapping, or until
    it stops responding. My previous experience with Exchange products in mind, I'm not sure if I should trust that
    advice. But it could save some valuable seconds every time we start a new connection to a known server. In any
    case, the persistent storage must not contain any sensitive information since the cache could be readable by
    unprivileged users. Domain, endpoint and auth_type are OK to cache since this info is make publicly available on
    HTTP and DNS servers via the autodiscover protocol. Just don't persist any credentials info.

    If an autodiscover lookup fails for any reason, the corresponding cache entry must be purged.

    'shelve' is supposedly thread-safe and process-safe, which suits our needs.
    """

    def __init__(self):
        self._protocols = {}  # Mapping from (domain, credentials) to AutodiscoverProtocol
        self._lock = RLock()

    @property
    def _storage_file(self):
        return AUTODISCOVER_PERSISTENT_STORAGE

    def clear(self):
        # Wipe the entire cache
        with shelve_open_with_failover(self._storage_file) as db:
            db.clear()
        self._protocols.clear()

    def __len__(self):
        return len(self._protocols)

    def __contains__(self, key):
        domain = key[0]
        with shelve_open_with_failover(self._storage_file) as db:
            return str(domain) in db

    def __getitem__(self, key):
        protocol = self._protocols.get(key)
        if protocol:
            return protocol
        domain, credentials = key
        with shelve_open_with_failover(self._storage_file) as db:
            endpoint, auth_type, retry_policy = db[str(domain)]  # It's OK to fail with KeyError here
        protocol = AutodiscoverProtocol(config=Configuration(
            service_endpoint=endpoint, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
        ))
        self._protocols[key] = protocol
        return protocol

    def __setitem__(self, key, protocol):
        # Populate both local and persistent cache
        domain = key[0]
        with shelve_open_with_failover(self._storage_file) as db:
            # Don't change this payload without bumping the cache file version in shelve_filename()
            db[str(domain)] = (protocol.service_endpoint, protocol.auth_type, protocol.retry_policy)
        self._protocols[key] = protocol

    def __delitem__(self, key):
        # Empty both local and persistent cache. Don't fail on non-existing entries because we could end here
        # multiple times due to race conditions.
        domain = key[0]
        with shelve_open_with_failover(self._storage_file) as db:
            try:
                del db[str(domain)]
            except KeyError:
                pass
        try:
            del self._protocols[key]
        except KeyError:
            pass

    def close(self):
        # Close all open connections
        for (domain, _), protocol in self._protocols.items():
            log.debug('Domain %s: Closing sessions', domain)
            protocol.close()
            del protocol
        self._protocols.clear()

    def __enter__(self):
        self._lock.__enter__()

    def __exit__(self, *args, **kwargs):
        self._lock.__exit__(*args, **kwargs)

    def __del__(self):
        # pylint: disable=bare-except
        try:
            self.close()
        except Exception:  # nosec
            # __del__ should never fail
            pass

    def __str__(self):
        return str(self._protocols)


autodiscover_cache = AutodiscoverCache()

Functions

def shelve_filename()
Expand source code
def shelve_filename():
    # Add the version of the cache format to the filename. If we change the format of the cached data, this version
    # must be bumped. Otherwise, new versions of this package cannot open cache files generated by older versions.
    version = 2
    # 'shelve' may pickle objects using different pickle protocol versions. Append the python major+minor version
    # numbers to the filename. Also append the username, to avoid permission errors.
    major, minor = sys.version_info[:2]
    try:
        user = getpass.getuser()
    except KeyError:
        # getuser() fails on some systems. Provide a sane default. See issue #448
        user = 'exchangelib'
    return 'exchangelib.{version}.cache.{user}.py{major}{minor}'.format(
        version=version, user=user, major=major, minor=minor
    )
def shelve_open_with_failover(filename)
Expand source code
@contextmanager
def shelve_open_with_failover(filename):
    # We can expect empty or corrupt files. Whatever happens, just delete the cache file and try again.
    # 'shelve' may add a backend-specific suffix to the file, so also delete all files with a suffix.
    # We don't know which file caused the error, so just delete them all.
    try:
        shelve_handle = shelve.open(filename)
        # Try to actually use the shelve. Some implementations may allow opening the file but then throw
        # errors on access.
        try:
            _ = shelve_handle['']
        except KeyError:
            # The entry doesn't exist. This is expected.
            pass
    except Exception as e:
        for f in glob.glob(filename + '*'):
            log.warning('Deleting invalid cache file %s (%r)', f, e)
            os.unlink(f)
        shelve_handle = shelve.open(filename)
    yield shelve_handle

Classes

class AutodiscoverCache

Stores the translation from (email domain, credentials) -> AutodiscoverProtocol object so we can re-use TCP connections to an autodiscover server within the same process. Also persists the email domain -> (autodiscover endpoint URL, auth_type) translation to the filesystem so the cache can be shared between multiple processes.

According to Microsoft, we may forever cache the (email domain -> autodiscover endpoint URL) mapping, or until it stops responding. My previous experience with Exchange products in mind, I'm not sure if I should trust that advice. But it could save some valuable seconds every time we start a new connection to a known server. In any case, the persistent storage must not contain any sensitive information since the cache could be readable by unprivileged users. Domain, endpoint and auth_type are OK to cache since this info is make publicly available on HTTP and DNS servers via the autodiscover protocol. Just don't persist any credentials info.

If an autodiscover lookup fails for any reason, the corresponding cache entry must be purged.

'shelve' is supposedly thread-safe and process-safe, which suits our needs.

Expand source code
class AutodiscoverCache:
    """Stores the translation from (email domain, credentials) -> AutodiscoverProtocol object so we can re-use TCP
    connections to an autodiscover server within the same process. Also persists the email domain -> (autodiscover
    endpoint URL, auth_type) translation to the filesystem so the cache can be shared between multiple processes.

    According to Microsoft, we may forever cache the (email domain -> autodiscover endpoint URL) mapping, or until
    it stops responding. My previous experience with Exchange products in mind, I'm not sure if I should trust that
    advice. But it could save some valuable seconds every time we start a new connection to a known server. In any
    case, the persistent storage must not contain any sensitive information since the cache could be readable by
    unprivileged users. Domain, endpoint and auth_type are OK to cache since this info is make publicly available on
    HTTP and DNS servers via the autodiscover protocol. Just don't persist any credentials info.

    If an autodiscover lookup fails for any reason, the corresponding cache entry must be purged.

    'shelve' is supposedly thread-safe and process-safe, which suits our needs.
    """

    def __init__(self):
        self._protocols = {}  # Mapping from (domain, credentials) to AutodiscoverProtocol
        self._lock = RLock()

    @property
    def _storage_file(self):
        return AUTODISCOVER_PERSISTENT_STORAGE

    def clear(self):
        # Wipe the entire cache
        with shelve_open_with_failover(self._storage_file) as db:
            db.clear()
        self._protocols.clear()

    def __len__(self):
        return len(self._protocols)

    def __contains__(self, key):
        domain = key[0]
        with shelve_open_with_failover(self._storage_file) as db:
            return str(domain) in db

    def __getitem__(self, key):
        protocol = self._protocols.get(key)
        if protocol:
            return protocol
        domain, credentials = key
        with shelve_open_with_failover(self._storage_file) as db:
            endpoint, auth_type, retry_policy = db[str(domain)]  # It's OK to fail with KeyError here
        protocol = AutodiscoverProtocol(config=Configuration(
            service_endpoint=endpoint, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
        ))
        self._protocols[key] = protocol
        return protocol

    def __setitem__(self, key, protocol):
        # Populate both local and persistent cache
        domain = key[0]
        with shelve_open_with_failover(self._storage_file) as db:
            # Don't change this payload without bumping the cache file version in shelve_filename()
            db[str(domain)] = (protocol.service_endpoint, protocol.auth_type, protocol.retry_policy)
        self._protocols[key] = protocol

    def __delitem__(self, key):
        # Empty both local and persistent cache. Don't fail on non-existing entries because we could end here
        # multiple times due to race conditions.
        domain = key[0]
        with shelve_open_with_failover(self._storage_file) as db:
            try:
                del db[str(domain)]
            except KeyError:
                pass
        try:
            del self._protocols[key]
        except KeyError:
            pass

    def close(self):
        # Close all open connections
        for (domain, _), protocol in self._protocols.items():
            log.debug('Domain %s: Closing sessions', domain)
            protocol.close()
            del protocol
        self._protocols.clear()

    def __enter__(self):
        self._lock.__enter__()

    def __exit__(self, *args, **kwargs):
        self._lock.__exit__(*args, **kwargs)

    def __del__(self):
        # pylint: disable=bare-except
        try:
            self.close()
        except Exception:  # nosec
            # __del__ should never fail
            pass

    def __str__(self):
        return str(self._protocols)

Methods

def clear(self)
Expand source code
def clear(self):
    # Wipe the entire cache
    with shelve_open_with_failover(self._storage_file) as db:
        db.clear()
    self._protocols.clear()
def close(self)
Expand source code
def close(self):
    # Close all open connections
    for (domain, _), protocol in self._protocols.items():
        log.debug('Domain %s: Closing sessions', domain)
        protocol.close()
        del protocol
    self._protocols.clear()
exchangelib-4.6.1/docs/exchangelib/autodiscover/discovery.html000066400000000000000000002176621414601472700246240ustar00rootroot00000000000000 exchangelib.autodiscover.discovery API documentation

Module exchangelib.autodiscover.discovery

Expand source code
import logging
import time
from urllib.parse import urlparse

import dns.resolver
from cached_property import threaded_cached_property

from .cache import autodiscover_cache
from .properties import Autodiscover
from .protocol import AutodiscoverProtocol
from ..configuration import Configuration
from ..credentials import OAuth2Credentials
from ..errors import AutoDiscoverFailed, AutoDiscoverCircularRedirect, TransportError, RedirectError, UnauthorizedError
from ..protocol import Protocol, FailFast
from ..transport import get_auth_method_from_response, DEFAULT_HEADERS, NOAUTH, OAUTH2, GSSAPI, AUTH_TYPE_MAP, \
    CREDENTIALS_REQUIRED
from ..util import post_ratelimited, get_domain, get_redirect_url, _back_off_if_needed, \
    DummyResponse, CONNECTION_ERRORS, TLS_ERRORS
from ..version import Version

log = logging.getLogger(__name__)


def discover(email, credentials=None, auth_type=None, retry_policy=None):
    return Autodiscovery(
        email=email, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
    ).discover()


class SrvRecord:
    """A container for autodiscover-related SRV records in DNS."""

    def __init__(self, priority, weight, port, srv):
        self.priority = priority
        self.weight = weight
        self.port = port
        self.srv = srv

    def __eq__(self, other):
        for k in self.__dict__:
            if getattr(self, k) != getattr(other, k):
                return False
        return True


class Autodiscovery:
    """Autodiscover is a Microsoft protocol for automatically getting the endpoint of the Exchange server and other
    connection-related settings holding the email address using only the email address, and username and password of the
    user.

    For a description of the protocol implemented, see "Autodiscover for Exchange ActiveSync developers":

    https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-interoperability-guidance/hh352638%28v%3dexchg.140%29

    Descriptions of the steps from the article are provided in their respective methods in this class.

    For a description of how to handle autodiscover error messages, see:

    https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/handling-autodiscover-error-messages

    A tip from the article:
    The client can perform steps 1 through 4 in any order or in parallel to expedite the process, but it must wait for
    responses to finish at each step before proceeding. Given that many organizations prefer to use the URL in step 2 to
    set up the Autodiscover service, the client might try this step first.

    Another possibly newer resource which has not yet been attempted is "Outlook 2016 Implementation of Autodiscover":
    https://support.microsoft.com/en-us/help/3211279/outlook-2016-implementation-of-autodiscover

    WARNING: The autodiscover protocol is very complicated. If you have problems autodiscovering using this
    implementation, start by doing an official test at https://testconnectivity.microsoft.com
    """

    # When connecting to servers that may not be serving the correct endpoint, we should use a retry policy that does
    # not leave us hanging for a long time on each step in the protocol.
    INITIAL_RETRY_POLICY = FailFast()
    RETRY_WAIT = 10  # Seconds to wait before retry on connection errors
    MAX_REDIRECTS = 10  # Maximum number of URL redirects before we give up
    DNS_RESOLVER_KWARGS = {}
    DNS_RESOLVER_ATTRS = {
        'timeout': AutodiscoverProtocol.TIMEOUT,
    }

    def __init__(self, email, credentials=None, auth_type=None, retry_policy=None):
        """

        :param email: The email address to autodiscover
        :param credentials: Credentials with authorization to make autodiscover lookups for this Account
            (Default value = None)
        :param auth_type:  (Default value = None)
        :param retry_policy:  (Default value = None)
        """
        self.email = email
        self.credentials = credentials
        self.auth_type = auth_type  # The auth type that the resulting protocol instance should have
        self.retry_policy = retry_policy  # The retry policy that the resulting protocol instance should have
        self._urls_visited = []  # Collects HTTP and Autodiscover redirects
        self._redirect_count = 0
        self._emails_visited = []  # Collects Autodiscover email redirects

    def discover(self):
        self._emails_visited.append(self.email.lower())

        # Check the autodiscover cache to see if we already know the autodiscover service endpoint for this email
        # domain. Use a lock to guard against multiple threads competing to cache information.
        log.debug('Waiting for autodiscover_cache lock')
        with autodiscover_cache:
            log.debug('autodiscover_cache lock acquired')
            cache_key = self._cache_key
            domain = get_domain(self.email)
            if cache_key in autodiscover_cache:
                ad_protocol = autodiscover_cache[cache_key]
                log.debug('Cache hit for key %s: %s', cache_key, ad_protocol.service_endpoint)
                try:
                    ad_response = self._quick(protocol=ad_protocol)
                except AutoDiscoverFailed:
                    # Autodiscover no longer works with this domain. Clear cache and try again after releasing the lock
                    log.debug('AD request failure. Removing cache for key %s', cache_key)
                    del autodiscover_cache[cache_key]
                    ad_response = self._step_1(hostname=domain)
            else:
                # This will cache the result
                ad_response = self._step_1(hostname=domain)

        log.debug('Released autodiscover_cache_lock')
        if ad_response.redirect_address:
            log.debug('Got a redirect address: %s', ad_response.redirect_address)
            if ad_response.redirect_address.lower() in self._emails_visited:
                raise AutoDiscoverCircularRedirect('We were redirected to an email address we have already seen')

            # Start over, but with the new email address
            self.email = ad_response.redirect_address
            return self.discover()

        # We successfully received a response. Clear the cache of seen emails etc.
        self.clear()
        return self._build_response(ad_response=ad_response)

    def clear(self):
        # This resets cached variables
        self._urls_visited = []
        self._redirect_count = 0
        self._emails_visited = []

    @property
    def _cache_key(self):
        # We may be using multiple different credentials and changing our minds on TLS verification. This key
        # combination should be safe for caching.
        domain = get_domain(self.email)
        return domain, self.credentials

    @threaded_cached_property
    def resolver(self):
        resolver = dns.resolver.Resolver(**self.DNS_RESOLVER_KWARGS)
        for k, v in self.DNS_RESOLVER_ATTRS.items():
            setattr(resolver, k, v)
        return resolver

    def _build_response(self, ad_response):
        ews_url = ad_response.ews_url
        if not ews_url:
            raise AutoDiscoverFailed("Response is missing an 'ews_url' value")
        if not ad_response.autodiscover_smtp_address:
            # Autodiscover does not always return an email address. In that case, the requesting email should be used
            ad_response.user.autodiscover_smtp_address = self.email

        # Get the server version. Not all protocol entries have a server version so we cheat a bit and also look at the
        # other ones that point to the same endpoint.
        for protocol in ad_response.account.protocols:
            if not protocol.ews_url or not protocol.server_version:
                continue
            if protocol.ews_url.lower() == ews_url.lower():
                version = Version(build=protocol.server_version)
                break
        else:
            version = None
        # We may not want to use the auth_package hints in the AD response. It could be incorrect and we can just guess.
        protocol = Protocol(
            config=Configuration(
                service_endpoint=ews_url,
                credentials=self.credentials,
                version=version,
                auth_type=self.auth_type,
                retry_policy=self.retry_policy,
            )
        )
        return ad_response, protocol

    def _quick(self, protocol):
        # Reset auth type and retry policy if we requested non-default values
        if self.auth_type:
            protocol.config.auth_type = self.auth_type
        if self.retry_policy:
            protocol.config.retry_policy = self.retry_policy
        try:
            r = self._get_authenticated_response(protocol=protocol)
        except TransportError as e:
            raise AutoDiscoverFailed('Response error: %s' % e)
        if r.status_code == 200:
            try:
                ad = Autodiscover.from_bytes(bytes_content=r.content)
                return self._step_5(ad=ad)
            except ValueError as e:
                raise AutoDiscoverFailed('Invalid response: %s' % e)
        raise AutoDiscoverFailed('Invalid response code: %s' % r.status_code)

    def _redirect_url_is_valid(self, url):
        """Three separate responses can be “Redirect responses”:
        * An HTTP status code (301, 302) with a new URL
        * An HTTP status code of 200, but with a payload XML containing a redirect to a different URL
        * An HTTP status code of 200, but with a payload XML containing a different SMTP address as the target address

        We only handle the HTTP 302 redirects here. We validate the URL received in the redirect response to ensure that
        it does not redirect to non-SSL endpoints or SSL endpoints with invalid certificates, and that the redirect is
        not circular. Finally, we should fail after 10 redirects.

        :param url:
        :return:
        """
        if url.lower() in self._urls_visited:
            log.warning('We have already tried this URL: %s', url)
            return False

        if self._redirect_count >= self.MAX_REDIRECTS:
            log.warning('We reached max redirects at URL: %s', url)
            return False

        # We require TLS endpoints
        if not url.startswith('https://'):
            log.debug('Invalid scheme for URL: %s', url)
            return False

        # Quick test that the endpoint responds and that TLS handshake is OK
        try:
            self._get_unauthenticated_response(url, method='head')
        except TransportError as e:
            log.debug('Response error on redirect URL %s: %s', url, e)
            return False

        self._redirect_count += 1
        return True

    def _get_unauthenticated_response(self, url, method='post'):
        """Get auth type by tasting headers from the server. Do POST requests be default. HEAD is too error prone, and
        some servers are set up to redirect to OWA on all requests except POST to the autodiscover endpoint.

        :param url:
        :param method:  (Default value = 'post')
        :return:
        """
        # We are connecting to untrusted servers here, so take necessary precautions.
        hostname = urlparse(url).netloc
        if not self._is_valid_hostname(hostname):
            # 'requests' is really bad at reporting that a hostname cannot be resolved. Let's check this separately.
            # Don't retry on DNS errors. They will most likely be persistent.
            raise TransportError('%r has no DNS entry' % hostname)

        kwargs = dict(
            url=url, headers=DEFAULT_HEADERS.copy(), allow_redirects=False, timeout=AutodiscoverProtocol.TIMEOUT
        )
        if method == 'post':
            kwargs['data'] = Autodiscover.payload(email=self.email)
        retry = 0
        t_start = time.monotonic()
        while True:
            _back_off_if_needed(self.INITIAL_RETRY_POLICY.back_off_until)
            log.debug('Trying to get response from %s', url)
            with AutodiscoverProtocol.raw_session(url) as s:
                try:
                    r = getattr(s, method)(**kwargs)
                    r.close()  # Release memory
                    break
                except TLS_ERRORS as e:
                    # Don't retry on TLS errors. They will most likely be persistent.
                    raise TransportError(str(e))
                except CONNECTION_ERRORS as e:
                    r = DummyResponse(url=url, headers={}, request_headers=kwargs['headers'])
                    total_wait = time.monotonic() - t_start
                    if self.INITIAL_RETRY_POLICY.may_retry_on_error(response=r, wait=total_wait):
                        log.debug("Connection error on URL %s (retry %s, error: %s). Cool down", url, retry, e)
                        # Don't respect the 'Retry-After' header. We don't know if this is a useful endpoint, and we
                        # want autodiscover to be reasonably fast.
                        self.INITIAL_RETRY_POLICY.back_off(self.RETRY_WAIT)
                        retry += 1
                        continue
                    log.debug("Connection error on URL %s: %s", url, e)
                    raise TransportError(str(e))
        try:
            auth_type = get_auth_method_from_response(response=r)
        except UnauthorizedError:
            # Failed to guess the auth type
            auth_type = NOAUTH
        if r.status_code in (301, 302) and 'location' in r.headers:
            # Make the redirect URL absolute
            try:
                r.headers['location'] = get_redirect_url(r)
            except TransportError:
                del r.headers['location']
        return auth_type, r

    def _get_authenticated_response(self, protocol):
        """Get a response by using the credentials provided. We guess the auth type along the way.

        :param protocol:
        :return:
        """
        # Redo the request with the correct auth
        data = Autodiscover.payload(email=self.email)
        headers = DEFAULT_HEADERS.copy()
        session = protocol.get_session()
        if GSSAPI in AUTH_TYPE_MAP and isinstance(session.auth, AUTH_TYPE_MAP[GSSAPI]):
            # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/pox-autodiscover-request-for-exchange
            headers['X-ClientCanHandle'] = 'Negotiate'
        try:
            r, session = post_ratelimited(protocol=protocol, session=session, url=protocol.service_endpoint,
                                          headers=headers, data=data, allow_redirects=False, stream=False)
            protocol.release_session(session)
        except UnauthorizedError as e:
            # It's entirely possible for the endpoint to ask for login. We should continue if login fails because this
            # isn't necessarily the right endpoint to use.
            raise TransportError(str(e))
        except RedirectError as e:
            r = DummyResponse(url=protocol.service_endpoint, headers={'location': e.url}, request_headers=None,
                              status_code=302)
        return r

    def _attempt_response(self, url):
        """Return an (is_valid_response, response) tuple.

        :param url:
        :return:
        """
        self._urls_visited.append(url.lower())
        log.debug('Attempting to get a valid response from %s', url)
        try:
            auth_type, r = self._get_unauthenticated_response(url=url)
            if isinstance(self.credentials, OAuth2Credentials):
                # This type of credentials *must* use the OAuth auth type
                auth_type = OAUTH2
            elif self.credentials is None and auth_type in CREDENTIALS_REQUIRED:
                raise ValueError('Auth type %r was detected but no credentials were provided' % auth_type)
            ad_protocol = AutodiscoverProtocol(
                config=Configuration(
                    service_endpoint=url,
                    credentials=self.credentials,
                    auth_type=auth_type,
                    retry_policy=self.INITIAL_RETRY_POLICY,
                )
            )
            if auth_type != NOAUTH:
                r = self._get_authenticated_response(protocol=ad_protocol)
        except TransportError as e:
            log.debug('Failed to get a response: %s', e)
            return False, None
        if r.status_code in (301, 302) and 'location' in r.headers:
            redirect_url = get_redirect_url(r)
            if self._redirect_url_is_valid(url=redirect_url):
                # The protocol does not specify this explicitly, but by looking at how testconnectivity.microsoft.com
                # works, it seems that we should follow this URL now and try to get a valid response.
                return self._attempt_response(url=redirect_url)
        if r.status_code == 200:
            try:
                ad = Autodiscover.from_bytes(bytes_content=r.content)
                # We got a valid response. Unless this is a URL redirect response, we cache the result
                if ad.response is None or not ad.response.redirect_url:
                    cache_key = self._cache_key
                    log.debug('Adding cache entry for key %s: %s', cache_key, ad_protocol.service_endpoint)
                    autodiscover_cache[cache_key] = ad_protocol
                return True, ad
            except ValueError as e:
                log.debug('Invalid response: %s', e)
        return False, None

    def _is_valid_hostname(self, hostname):
        log.debug('Checking if %s can be looked up in DNS', hostname)
        try:
            self.resolver.resolve(hostname)
        except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
            return False
        return True

    def _get_srv_records(self, hostname):
        """Send a DNS query for SRV entries for the hostname.

        An SRV entry that has been formatted for autodiscovery will have the following format:

            canonical name = mail.example.com.
            service = 8 100 443 webmail.example.com.

        The first three numbers in the service line are: priority, weight, port

        :param hostname:
        :return:
        """
        log.debug('Attempting to get SRV records for %s', hostname)
        records = []
        try:
            answers = self.resolver.resolve('%s.' % hostname, 'SRV')
        except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) as e:
            log.debug('DNS lookup failure: %s', e)
            return records
        for rdata in answers:
            try:
                vals = rdata.to_text().strip().rstrip('.').split(' ')
                # Raise ValueError if the first three are not ints, and IndexError if there are less than 4 values
                priority, weight, port, srv = int(vals[0]), int(vals[1]), int(vals[2]), vals[3]
                record = SrvRecord(priority=priority, weight=weight, port=port, srv=srv)
                log.debug('Found SRV record %s ', record)
                records.append(record)
            except (ValueError, IndexError):
                log.debug('Incompatible SRV record for %s (%s)', hostname, rdata.to_text())
        return records

    def _step_1(self, hostname):
        """Perform step 1, where the client sends an Autodiscover request to
        https://example.com/autodiscover/autodiscover.xml and then does one of the following:
            * If the Autodiscover attempt succeeds, the client proceeds to step 5.
            * If the Autodiscover attempt fails, the client proceeds to step 2.

        :param hostname:
        :return:
        """
        url = 'https://%s/Autodiscover/Autodiscover.xml' % hostname
        log.info('Step 1: Trying autodiscover on %r with email %r', url, self.email)
        is_valid_response, ad = self._attempt_response(url=url)
        if is_valid_response:
            return self._step_5(ad=ad)
        return self._step_2(hostname=hostname)

    def _step_2(self, hostname):
        """Perform step 2, where the client sends an Autodiscover request to
        https://autodiscover.example.com/autodiscover/autodiscover.xml and then does one of the following:
            * If the Autodiscover attempt succeeds, the client proceeds to step 5.
            * If the Autodiscover attempt fails, the client proceeds to step 3.

        :param hostname:
        :return:
        """
        url = 'https://autodiscover.%s/Autodiscover/Autodiscover.xml' % hostname
        log.info('Step 2: Trying autodiscover on %r with email %r', url, self.email)
        is_valid_response, ad = self._attempt_response(url=url)
        if is_valid_response:
            return self._step_5(ad=ad)
        return self._step_3(hostname=hostname)

    def _step_3(self, hostname):
        """Perform step 3, where the client sends an unauth'ed GET method request to
        http://autodiscover.example.com/autodiscover/autodiscover.xml (Note that this is a non-HTTPS endpoint). The
        client then does one of the following:
            * If the GET request returns a 302 redirect response, it gets the redirection URL from the 'Location' HTTP
            header and validates it as described in the "Redirect responses" section. The client then does one of the
            following:
                * If the redirection URL is valid, the client tries the URL and then does one of the following:
                    * If the attempt succeeds, the client proceeds to step 5.
                    * If the attempt fails, the client proceeds to step 4.
                * If the redirection URL is not valid, the client proceeds to step 4.
            * If the GET request does not return a 302 redirect response, the client proceeds to step 4.

        :param hostname:
        :return:
        """
        url = 'http://autodiscover.%s/Autodiscover/Autodiscover.xml' % hostname
        log.info('Step 3: Trying autodiscover on %r with email %r', url, self.email)
        try:
            _, r = self._get_unauthenticated_response(url=url, method='get')
        except TransportError:
            r = DummyResponse(url=url, headers={}, request_headers={})
        if r.status_code in (301, 302) and 'location' in r.headers:
            redirect_url = get_redirect_url(r)
            if self._redirect_url_is_valid(url=redirect_url):
                is_valid_response, ad = self._attempt_response(url=redirect_url)
                if is_valid_response:
                    return self._step_5(ad=ad)
                return self._step_4(hostname=hostname)
            return self._step_4(hostname=hostname)
        return self._step_4(hostname=hostname)

    def _step_4(self, hostname):
        """Perform step 4, where the client performs a Domain Name System (DNS) query for an SRV record for
        _autodiscover._tcp.example.com. The query might return multiple records. The client selects only records that
        point to an SSL endpoint and that have the highest priority and weight. One of the following actions then
        occurs:
            * If no such records are returned, the client proceeds to step 6.
            * If records are returned, the application randomly chooses a record in the list and validates the endpoint
              that it points to by following the process described in the "Redirect Response" section. The client then
              does one of the following:
                * If the redirection URL is valid, the client tries the URL and then does one of the following:
                    * If the attempt succeeds, the client proceeds to step 5.
                    * If the attempt fails, the client proceeds to step 6.
                * If the redirection URL is not valid, the client proceeds to step 6.

        :param hostname:
        :return:
        """
        dns_hostname = '_autodiscover._tcp.%s' % hostname
        log.info('Step 4: Trying autodiscover on %r with email %r', dns_hostname, self.email)
        srv_records = self._get_srv_records(dns_hostname)
        try:
            srv_host = _select_srv_host(srv_records)
        except ValueError:
            srv_host = None
        if not srv_host:
            return self._step_6()
        redirect_url = 'https://%s/Autodiscover/Autodiscover.xml' % srv_host
        if self._redirect_url_is_valid(url=redirect_url):
            is_valid_response, ad = self._attempt_response(url=redirect_url)
            if is_valid_response:
                return self._step_5(ad=ad)
            return self._step_6()
        return self._step_6()

    def _step_5(self, ad):
        """Perform step 5. When a valid Autodiscover request succeeds, the following sequence occurs:
            * If the server responds with an HTTP 302 redirect, the client validates the redirection URL according to
              the process defined in the "Redirect responses" and then does one of the following:
                * If the redirection URL is valid, the client tries the URL and then does one of the following:
                    * If the attempt succeeds, the client repeats step 5 from the beginning.
                    * If the attempt fails, the client proceeds to step 6.
                * If the redirection URL is not valid, the client proceeds to step 6.
            * If the server responds with a valid Autodiscover response, the client does one of the following:
                * If the value of the Action element is "Redirect", the client gets the redirection email address from
                  the Redirect element and then returns to step 1, using this new email address.
                * If the value of the Action element is "Settings", the client has successfully received the requested
                  configuration settings for the specified user. The client does not need to proceed to step 6.

        :param ad:
        :return:
        """
        log.info('Step 5: Checking response')
        if ad.response is None:
            # This is not explicit in the protocol, but let's raise errors here
            ad.raise_errors()

        ad_response = ad.response
        if ad_response.redirect_url:
            log.debug('Got a redirect URL: %s', ad_response.redirect_url)
            # We are diverging a bit from the protocol here. We will never get an HTTP 302 since earlier steps already
            # followed the redirects where possible. Instead, we handle retirect responses here.
            if self._redirect_url_is_valid(url=ad_response.redirect_url):
                is_valid_response, ad = self._attempt_response(url=ad_response.redirect_url)
                if is_valid_response:
                    return self._step_5(ad=ad)
                return self._step_6()
            log.debug('Invalid redirect URL: %s', ad_response.redirect_url)
            return self._step_6()
        # This could be an email redirect. Let outer layer handle this
        return ad_response

    def _step_6(self):
        """Perform step 6. If the client cannot contact the Autodiscover service, the client should ask the user for
        the Exchange server name and use it to construct an Exchange EWS URL. The client should try to use this URL for
        future requests.
        """
        raise AutoDiscoverFailed(
            'All steps in the autodiscover protocol failed for email %r. If you think this is an error, consider doing '
            'an official test at https://testconnectivity.microsoft.com' % self.email)


def _select_srv_host(srv_records):
    """Select the record with the highest priority, that also supports TLS.

    :param srv_records:
    :return:
    """
    best_record = None
    for srv_record in srv_records:
        if srv_record.port != 443:
            log.debug('Skipping SRV record %r (no TLS)', srv_record)
            continue
        # Assume port 443 will serve TLS. If not, autodiscover will probably also be broken for others.
        if best_record is None or best_record.priority < srv_record.priority:
            best_record = srv_record
    if not best_record:
        raise ValueError('No suitable records')
    return best_record.srv

Functions

def discover(email, credentials=None, auth_type=None, retry_policy=None)
Expand source code
def discover(email, credentials=None, auth_type=None, retry_policy=None):
    return Autodiscovery(
        email=email, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
    ).discover()

Classes

class Autodiscovery (email, credentials=None, auth_type=None, retry_policy=None)

Autodiscover is a Microsoft protocol for automatically getting the endpoint of the Exchange server and other connection-related settings holding the email address using only the email address, and username and password of the user.

For a description of the protocol implemented, see "Autodiscover for Exchange ActiveSync developers":

https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-interoperability-guidance/hh352638%28v%3dexchg.140%29

Descriptions of the steps from the article are provided in their respective methods in this class.

For a description of how to handle autodiscover error messages, see:

https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/handling-autodiscover-error-messages

A tip from the article: The client can perform steps 1 through 4 in any order or in parallel to expedite the process, but it must wait for responses to finish at each step before proceeding. Given that many organizations prefer to use the URL in step 2 to set up the Autodiscover service, the client might try this step first.

Another possibly newer resource which has not yet been attempted is "Outlook 2016 Implementation of Autodiscover": https://support.microsoft.com/en-us/help/3211279/outlook-2016-implementation-of-autodiscover

WARNING: The autodiscover protocol is very complicated. If you have problems autodiscovering using this implementation, start by doing an official test at https://testconnectivity.microsoft.com

:param email: The email address to autodiscover :param credentials: Credentials with authorization to make autodiscover lookups for this Account (Default value = None) :param auth_type: (Default value = None) :param retry_policy: (Default value = None)

Expand source code
class Autodiscovery:
    """Autodiscover is a Microsoft protocol for automatically getting the endpoint of the Exchange server and other
    connection-related settings holding the email address using only the email address, and username and password of the
    user.

    For a description of the protocol implemented, see "Autodiscover for Exchange ActiveSync developers":

    https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-interoperability-guidance/hh352638%28v%3dexchg.140%29

    Descriptions of the steps from the article are provided in their respective methods in this class.

    For a description of how to handle autodiscover error messages, see:

    https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/handling-autodiscover-error-messages

    A tip from the article:
    The client can perform steps 1 through 4 in any order or in parallel to expedite the process, but it must wait for
    responses to finish at each step before proceeding. Given that many organizations prefer to use the URL in step 2 to
    set up the Autodiscover service, the client might try this step first.

    Another possibly newer resource which has not yet been attempted is "Outlook 2016 Implementation of Autodiscover":
    https://support.microsoft.com/en-us/help/3211279/outlook-2016-implementation-of-autodiscover

    WARNING: The autodiscover protocol is very complicated. If you have problems autodiscovering using this
    implementation, start by doing an official test at https://testconnectivity.microsoft.com
    """

    # When connecting to servers that may not be serving the correct endpoint, we should use a retry policy that does
    # not leave us hanging for a long time on each step in the protocol.
    INITIAL_RETRY_POLICY = FailFast()
    RETRY_WAIT = 10  # Seconds to wait before retry on connection errors
    MAX_REDIRECTS = 10  # Maximum number of URL redirects before we give up
    DNS_RESOLVER_KWARGS = {}
    DNS_RESOLVER_ATTRS = {
        'timeout': AutodiscoverProtocol.TIMEOUT,
    }

    def __init__(self, email, credentials=None, auth_type=None, retry_policy=None):
        """

        :param email: The email address to autodiscover
        :param credentials: Credentials with authorization to make autodiscover lookups for this Account
            (Default value = None)
        :param auth_type:  (Default value = None)
        :param retry_policy:  (Default value = None)
        """
        self.email = email
        self.credentials = credentials
        self.auth_type = auth_type  # The auth type that the resulting protocol instance should have
        self.retry_policy = retry_policy  # The retry policy that the resulting protocol instance should have
        self._urls_visited = []  # Collects HTTP and Autodiscover redirects
        self._redirect_count = 0
        self._emails_visited = []  # Collects Autodiscover email redirects

    def discover(self):
        self._emails_visited.append(self.email.lower())

        # Check the autodiscover cache to see if we already know the autodiscover service endpoint for this email
        # domain. Use a lock to guard against multiple threads competing to cache information.
        log.debug('Waiting for autodiscover_cache lock')
        with autodiscover_cache:
            log.debug('autodiscover_cache lock acquired')
            cache_key = self._cache_key
            domain = get_domain(self.email)
            if cache_key in autodiscover_cache:
                ad_protocol = autodiscover_cache[cache_key]
                log.debug('Cache hit for key %s: %s', cache_key, ad_protocol.service_endpoint)
                try:
                    ad_response = self._quick(protocol=ad_protocol)
                except AutoDiscoverFailed:
                    # Autodiscover no longer works with this domain. Clear cache and try again after releasing the lock
                    log.debug('AD request failure. Removing cache for key %s', cache_key)
                    del autodiscover_cache[cache_key]
                    ad_response = self._step_1(hostname=domain)
            else:
                # This will cache the result
                ad_response = self._step_1(hostname=domain)

        log.debug('Released autodiscover_cache_lock')
        if ad_response.redirect_address:
            log.debug('Got a redirect address: %s', ad_response.redirect_address)
            if ad_response.redirect_address.lower() in self._emails_visited:
                raise AutoDiscoverCircularRedirect('We were redirected to an email address we have already seen')

            # Start over, but with the new email address
            self.email = ad_response.redirect_address
            return self.discover()

        # We successfully received a response. Clear the cache of seen emails etc.
        self.clear()
        return self._build_response(ad_response=ad_response)

    def clear(self):
        # This resets cached variables
        self._urls_visited = []
        self._redirect_count = 0
        self._emails_visited = []

    @property
    def _cache_key(self):
        # We may be using multiple different credentials and changing our minds on TLS verification. This key
        # combination should be safe for caching.
        domain = get_domain(self.email)
        return domain, self.credentials

    @threaded_cached_property
    def resolver(self):
        resolver = dns.resolver.Resolver(**self.DNS_RESOLVER_KWARGS)
        for k, v in self.DNS_RESOLVER_ATTRS.items():
            setattr(resolver, k, v)
        return resolver

    def _build_response(self, ad_response):
        ews_url = ad_response.ews_url
        if not ews_url:
            raise AutoDiscoverFailed("Response is missing an 'ews_url' value")
        if not ad_response.autodiscover_smtp_address:
            # Autodiscover does not always return an email address. In that case, the requesting email should be used
            ad_response.user.autodiscover_smtp_address = self.email

        # Get the server version. Not all protocol entries have a server version so we cheat a bit and also look at the
        # other ones that point to the same endpoint.
        for protocol in ad_response.account.protocols:
            if not protocol.ews_url or not protocol.server_version:
                continue
            if protocol.ews_url.lower() == ews_url.lower():
                version = Version(build=protocol.server_version)
                break
        else:
            version = None
        # We may not want to use the auth_package hints in the AD response. It could be incorrect and we can just guess.
        protocol = Protocol(
            config=Configuration(
                service_endpoint=ews_url,
                credentials=self.credentials,
                version=version,
                auth_type=self.auth_type,
                retry_policy=self.retry_policy,
            )
        )
        return ad_response, protocol

    def _quick(self, protocol):
        # Reset auth type and retry policy if we requested non-default values
        if self.auth_type:
            protocol.config.auth_type = self.auth_type
        if self.retry_policy:
            protocol.config.retry_policy = self.retry_policy
        try:
            r = self._get_authenticated_response(protocol=protocol)
        except TransportError as e:
            raise AutoDiscoverFailed('Response error: %s' % e)
        if r.status_code == 200:
            try:
                ad = Autodiscover.from_bytes(bytes_content=r.content)
                return self._step_5(ad=ad)
            except ValueError as e:
                raise AutoDiscoverFailed('Invalid response: %s' % e)
        raise AutoDiscoverFailed('Invalid response code: %s' % r.status_code)

    def _redirect_url_is_valid(self, url):
        """Three separate responses can be “Redirect responses”:
        * An HTTP status code (301, 302) with a new URL
        * An HTTP status code of 200, but with a payload XML containing a redirect to a different URL
        * An HTTP status code of 200, but with a payload XML containing a different SMTP address as the target address

        We only handle the HTTP 302 redirects here. We validate the URL received in the redirect response to ensure that
        it does not redirect to non-SSL endpoints or SSL endpoints with invalid certificates, and that the redirect is
        not circular. Finally, we should fail after 10 redirects.

        :param url:
        :return:
        """
        if url.lower() in self._urls_visited:
            log.warning('We have already tried this URL: %s', url)
            return False

        if self._redirect_count >= self.MAX_REDIRECTS:
            log.warning('We reached max redirects at URL: %s', url)
            return False

        # We require TLS endpoints
        if not url.startswith('https://'):
            log.debug('Invalid scheme for URL: %s', url)
            return False

        # Quick test that the endpoint responds and that TLS handshake is OK
        try:
            self._get_unauthenticated_response(url, method='head')
        except TransportError as e:
            log.debug('Response error on redirect URL %s: %s', url, e)
            return False

        self._redirect_count += 1
        return True

    def _get_unauthenticated_response(self, url, method='post'):
        """Get auth type by tasting headers from the server. Do POST requests be default. HEAD is too error prone, and
        some servers are set up to redirect to OWA on all requests except POST to the autodiscover endpoint.

        :param url:
        :param method:  (Default value = 'post')
        :return:
        """
        # We are connecting to untrusted servers here, so take necessary precautions.
        hostname = urlparse(url).netloc
        if not self._is_valid_hostname(hostname):
            # 'requests' is really bad at reporting that a hostname cannot be resolved. Let's check this separately.
            # Don't retry on DNS errors. They will most likely be persistent.
            raise TransportError('%r has no DNS entry' % hostname)

        kwargs = dict(
            url=url, headers=DEFAULT_HEADERS.copy(), allow_redirects=False, timeout=AutodiscoverProtocol.TIMEOUT
        )
        if method == 'post':
            kwargs['data'] = Autodiscover.payload(email=self.email)
        retry = 0
        t_start = time.monotonic()
        while True:
            _back_off_if_needed(self.INITIAL_RETRY_POLICY.back_off_until)
            log.debug('Trying to get response from %s', url)
            with AutodiscoverProtocol.raw_session(url) as s:
                try:
                    r = getattr(s, method)(**kwargs)
                    r.close()  # Release memory
                    break
                except TLS_ERRORS as e:
                    # Don't retry on TLS errors. They will most likely be persistent.
                    raise TransportError(str(e))
                except CONNECTION_ERRORS as e:
                    r = DummyResponse(url=url, headers={}, request_headers=kwargs['headers'])
                    total_wait = time.monotonic() - t_start
                    if self.INITIAL_RETRY_POLICY.may_retry_on_error(response=r, wait=total_wait):
                        log.debug("Connection error on URL %s (retry %s, error: %s). Cool down", url, retry, e)
                        # Don't respect the 'Retry-After' header. We don't know if this is a useful endpoint, and we
                        # want autodiscover to be reasonably fast.
                        self.INITIAL_RETRY_POLICY.back_off(self.RETRY_WAIT)
                        retry += 1
                        continue
                    log.debug("Connection error on URL %s: %s", url, e)
                    raise TransportError(str(e))
        try:
            auth_type = get_auth_method_from_response(response=r)
        except UnauthorizedError:
            # Failed to guess the auth type
            auth_type = NOAUTH
        if r.status_code in (301, 302) and 'location' in r.headers:
            # Make the redirect URL absolute
            try:
                r.headers['location'] = get_redirect_url(r)
            except TransportError:
                del r.headers['location']
        return auth_type, r

    def _get_authenticated_response(self, protocol):
        """Get a response by using the credentials provided. We guess the auth type along the way.

        :param protocol:
        :return:
        """
        # Redo the request with the correct auth
        data = Autodiscover.payload(email=self.email)
        headers = DEFAULT_HEADERS.copy()
        session = protocol.get_session()
        if GSSAPI in AUTH_TYPE_MAP and isinstance(session.auth, AUTH_TYPE_MAP[GSSAPI]):
            # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/pox-autodiscover-request-for-exchange
            headers['X-ClientCanHandle'] = 'Negotiate'
        try:
            r, session = post_ratelimited(protocol=protocol, session=session, url=protocol.service_endpoint,
                                          headers=headers, data=data, allow_redirects=False, stream=False)
            protocol.release_session(session)
        except UnauthorizedError as e:
            # It's entirely possible for the endpoint to ask for login. We should continue if login fails because this
            # isn't necessarily the right endpoint to use.
            raise TransportError(str(e))
        except RedirectError as e:
            r = DummyResponse(url=protocol.service_endpoint, headers={'location': e.url}, request_headers=None,
                              status_code=302)
        return r

    def _attempt_response(self, url):
        """Return an (is_valid_response, response) tuple.

        :param url:
        :return:
        """
        self._urls_visited.append(url.lower())
        log.debug('Attempting to get a valid response from %s', url)
        try:
            auth_type, r = self._get_unauthenticated_response(url=url)
            if isinstance(self.credentials, OAuth2Credentials):
                # This type of credentials *must* use the OAuth auth type
                auth_type = OAUTH2
            elif self.credentials is None and auth_type in CREDENTIALS_REQUIRED:
                raise ValueError('Auth type %r was detected but no credentials were provided' % auth_type)
            ad_protocol = AutodiscoverProtocol(
                config=Configuration(
                    service_endpoint=url,
                    credentials=self.credentials,
                    auth_type=auth_type,
                    retry_policy=self.INITIAL_RETRY_POLICY,
                )
            )
            if auth_type != NOAUTH:
                r = self._get_authenticated_response(protocol=ad_protocol)
        except TransportError as e:
            log.debug('Failed to get a response: %s', e)
            return False, None
        if r.status_code in (301, 302) and 'location' in r.headers:
            redirect_url = get_redirect_url(r)
            if self._redirect_url_is_valid(url=redirect_url):
                # The protocol does not specify this explicitly, but by looking at how testconnectivity.microsoft.com
                # works, it seems that we should follow this URL now and try to get a valid response.
                return self._attempt_response(url=redirect_url)
        if r.status_code == 200:
            try:
                ad = Autodiscover.from_bytes(bytes_content=r.content)
                # We got a valid response. Unless this is a URL redirect response, we cache the result
                if ad.response is None or not ad.response.redirect_url:
                    cache_key = self._cache_key
                    log.debug('Adding cache entry for key %s: %s', cache_key, ad_protocol.service_endpoint)
                    autodiscover_cache[cache_key] = ad_protocol
                return True, ad
            except ValueError as e:
                log.debug('Invalid response: %s', e)
        return False, None

    def _is_valid_hostname(self, hostname):
        log.debug('Checking if %s can be looked up in DNS', hostname)
        try:
            self.resolver.resolve(hostname)
        except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
            return False
        return True

    def _get_srv_records(self, hostname):
        """Send a DNS query for SRV entries for the hostname.

        An SRV entry that has been formatted for autodiscovery will have the following format:

            canonical name = mail.example.com.
            service = 8 100 443 webmail.example.com.

        The first three numbers in the service line are: priority, weight, port

        :param hostname:
        :return:
        """
        log.debug('Attempting to get SRV records for %s', hostname)
        records = []
        try:
            answers = self.resolver.resolve('%s.' % hostname, 'SRV')
        except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) as e:
            log.debug('DNS lookup failure: %s', e)
            return records
        for rdata in answers:
            try:
                vals = rdata.to_text().strip().rstrip('.').split(' ')
                # Raise ValueError if the first three are not ints, and IndexError if there are less than 4 values
                priority, weight, port, srv = int(vals[0]), int(vals[1]), int(vals[2]), vals[3]
                record = SrvRecord(priority=priority, weight=weight, port=port, srv=srv)
                log.debug('Found SRV record %s ', record)
                records.append(record)
            except (ValueError, IndexError):
                log.debug('Incompatible SRV record for %s (%s)', hostname, rdata.to_text())
        return records

    def _step_1(self, hostname):
        """Perform step 1, where the client sends an Autodiscover request to
        https://example.com/autodiscover/autodiscover.xml and then does one of the following:
            * If the Autodiscover attempt succeeds, the client proceeds to step 5.
            * If the Autodiscover attempt fails, the client proceeds to step 2.

        :param hostname:
        :return:
        """
        url = 'https://%s/Autodiscover/Autodiscover.xml' % hostname
        log.info('Step 1: Trying autodiscover on %r with email %r', url, self.email)
        is_valid_response, ad = self._attempt_response(url=url)
        if is_valid_response:
            return self._step_5(ad=ad)
        return self._step_2(hostname=hostname)

    def _step_2(self, hostname):
        """Perform step 2, where the client sends an Autodiscover request to
        https://autodiscover.example.com/autodiscover/autodiscover.xml and then does one of the following:
            * If the Autodiscover attempt succeeds, the client proceeds to step 5.
            * If the Autodiscover attempt fails, the client proceeds to step 3.

        :param hostname:
        :return:
        """
        url = 'https://autodiscover.%s/Autodiscover/Autodiscover.xml' % hostname
        log.info('Step 2: Trying autodiscover on %r with email %r', url, self.email)
        is_valid_response, ad = self._attempt_response(url=url)
        if is_valid_response:
            return self._step_5(ad=ad)
        return self._step_3(hostname=hostname)

    def _step_3(self, hostname):
        """Perform step 3, where the client sends an unauth'ed GET method request to
        http://autodiscover.example.com/autodiscover/autodiscover.xml (Note that this is a non-HTTPS endpoint). The
        client then does one of the following:
            * If the GET request returns a 302 redirect response, it gets the redirection URL from the 'Location' HTTP
            header and validates it as described in the "Redirect responses" section. The client then does one of the
            following:
                * If the redirection URL is valid, the client tries the URL and then does one of the following:
                    * If the attempt succeeds, the client proceeds to step 5.
                    * If the attempt fails, the client proceeds to step 4.
                * If the redirection URL is not valid, the client proceeds to step 4.
            * If the GET request does not return a 302 redirect response, the client proceeds to step 4.

        :param hostname:
        :return:
        """
        url = 'http://autodiscover.%s/Autodiscover/Autodiscover.xml' % hostname
        log.info('Step 3: Trying autodiscover on %r with email %r', url, self.email)
        try:
            _, r = self._get_unauthenticated_response(url=url, method='get')
        except TransportError:
            r = DummyResponse(url=url, headers={}, request_headers={})
        if r.status_code in (301, 302) and 'location' in r.headers:
            redirect_url = get_redirect_url(r)
            if self._redirect_url_is_valid(url=redirect_url):
                is_valid_response, ad = self._attempt_response(url=redirect_url)
                if is_valid_response:
                    return self._step_5(ad=ad)
                return self._step_4(hostname=hostname)
            return self._step_4(hostname=hostname)
        return self._step_4(hostname=hostname)

    def _step_4(self, hostname):
        """Perform step 4, where the client performs a Domain Name System (DNS) query for an SRV record for
        _autodiscover._tcp.example.com. The query might return multiple records. The client selects only records that
        point to an SSL endpoint and that have the highest priority and weight. One of the following actions then
        occurs:
            * If no such records are returned, the client proceeds to step 6.
            * If records are returned, the application randomly chooses a record in the list and validates the endpoint
              that it points to by following the process described in the "Redirect Response" section. The client then
              does one of the following:
                * If the redirection URL is valid, the client tries the URL and then does one of the following:
                    * If the attempt succeeds, the client proceeds to step 5.
                    * If the attempt fails, the client proceeds to step 6.
                * If the redirection URL is not valid, the client proceeds to step 6.

        :param hostname:
        :return:
        """
        dns_hostname = '_autodiscover._tcp.%s' % hostname
        log.info('Step 4: Trying autodiscover on %r with email %r', dns_hostname, self.email)
        srv_records = self._get_srv_records(dns_hostname)
        try:
            srv_host = _select_srv_host(srv_records)
        except ValueError:
            srv_host = None
        if not srv_host:
            return self._step_6()
        redirect_url = 'https://%s/Autodiscover/Autodiscover.xml' % srv_host
        if self._redirect_url_is_valid(url=redirect_url):
            is_valid_response, ad = self._attempt_response(url=redirect_url)
            if is_valid_response:
                return self._step_5(ad=ad)
            return self._step_6()
        return self._step_6()

    def _step_5(self, ad):
        """Perform step 5. When a valid Autodiscover request succeeds, the following sequence occurs:
            * If the server responds with an HTTP 302 redirect, the client validates the redirection URL according to
              the process defined in the "Redirect responses" and then does one of the following:
                * If the redirection URL is valid, the client tries the URL and then does one of the following:
                    * If the attempt succeeds, the client repeats step 5 from the beginning.
                    * If the attempt fails, the client proceeds to step 6.
                * If the redirection URL is not valid, the client proceeds to step 6.
            * If the server responds with a valid Autodiscover response, the client does one of the following:
                * If the value of the Action element is "Redirect", the client gets the redirection email address from
                  the Redirect element and then returns to step 1, using this new email address.
                * If the value of the Action element is "Settings", the client has successfully received the requested
                  configuration settings for the specified user. The client does not need to proceed to step 6.

        :param ad:
        :return:
        """
        log.info('Step 5: Checking response')
        if ad.response is None:
            # This is not explicit in the protocol, but let's raise errors here
            ad.raise_errors()

        ad_response = ad.response
        if ad_response.redirect_url:
            log.debug('Got a redirect URL: %s', ad_response.redirect_url)
            # We are diverging a bit from the protocol here. We will never get an HTTP 302 since earlier steps already
            # followed the redirects where possible. Instead, we handle retirect responses here.
            if self._redirect_url_is_valid(url=ad_response.redirect_url):
                is_valid_response, ad = self._attempt_response(url=ad_response.redirect_url)
                if is_valid_response:
                    return self._step_5(ad=ad)
                return self._step_6()
            log.debug('Invalid redirect URL: %s', ad_response.redirect_url)
            return self._step_6()
        # This could be an email redirect. Let outer layer handle this
        return ad_response

    def _step_6(self):
        """Perform step 6. If the client cannot contact the Autodiscover service, the client should ask the user for
        the Exchange server name and use it to construct an Exchange EWS URL. The client should try to use this URL for
        future requests.
        """
        raise AutoDiscoverFailed(
            'All steps in the autodiscover protocol failed for email %r. If you think this is an error, consider doing '
            'an official test at https://testconnectivity.microsoft.com' % self.email)

Class variables

var DNS_RESOLVER_ATTRS
var DNS_RESOLVER_KWARGS
var INITIAL_RETRY_POLICY
var MAX_REDIRECTS
var RETRY_WAIT

Instance variables

var resolver
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))

Methods

def clear(self)
Expand source code
def clear(self):
    # This resets cached variables
    self._urls_visited = []
    self._redirect_count = 0
    self._emails_visited = []
def discover(self)
Expand source code
def discover(self):
    self._emails_visited.append(self.email.lower())

    # Check the autodiscover cache to see if we already know the autodiscover service endpoint for this email
    # domain. Use a lock to guard against multiple threads competing to cache information.
    log.debug('Waiting for autodiscover_cache lock')
    with autodiscover_cache:
        log.debug('autodiscover_cache lock acquired')
        cache_key = self._cache_key
        domain = get_domain(self.email)
        if cache_key in autodiscover_cache:
            ad_protocol = autodiscover_cache[cache_key]
            log.debug('Cache hit for key %s: %s', cache_key, ad_protocol.service_endpoint)
            try:
                ad_response = self._quick(protocol=ad_protocol)
            except AutoDiscoverFailed:
                # Autodiscover no longer works with this domain. Clear cache and try again after releasing the lock
                log.debug('AD request failure. Removing cache for key %s', cache_key)
                del autodiscover_cache[cache_key]
                ad_response = self._step_1(hostname=domain)
        else:
            # This will cache the result
            ad_response = self._step_1(hostname=domain)

    log.debug('Released autodiscover_cache_lock')
    if ad_response.redirect_address:
        log.debug('Got a redirect address: %s', ad_response.redirect_address)
        if ad_response.redirect_address.lower() in self._emails_visited:
            raise AutoDiscoverCircularRedirect('We were redirected to an email address we have already seen')

        # Start over, but with the new email address
        self.email = ad_response.redirect_address
        return self.discover()

    # We successfully received a response. Clear the cache of seen emails etc.
    self.clear()
    return self._build_response(ad_response=ad_response)
class SrvRecord (priority, weight, port, srv)

A container for autodiscover-related SRV records in DNS.

Expand source code
class SrvRecord:
    """A container for autodiscover-related SRV records in DNS."""

    def __init__(self, priority, weight, port, srv):
        self.priority = priority
        self.weight = weight
        self.port = port
        self.srv = srv

    def __eq__(self, other):
        for k in self.__dict__:
            if getattr(self, k) != getattr(other, k):
                return False
        return True
exchangelib-4.6.1/docs/exchangelib/autodiscover/index.html000066400000000000000000001541261414601472700237170ustar00rootroot00000000000000 exchangelib.autodiscover API documentation

Module exchangelib.autodiscover

Expand source code
from .cache import AutodiscoverCache, autodiscover_cache
from .discovery import Autodiscovery, discover
from .protocol import AutodiscoverProtocol


def close_connections():
    with autodiscover_cache:
        autodiscover_cache.close()


def clear_cache():
    with autodiscover_cache:
        autodiscover_cache.clear()


__all__ = [
    'AutodiscoverCache', 'AutodiscoverProtocol', 'Autodiscovery', 'discover', 'autodiscover_cache',
    'close_connections', 'clear_cache'
]

Sub-modules

exchangelib.autodiscover.cache
exchangelib.autodiscover.discovery
exchangelib.autodiscover.properties
exchangelib.autodiscover.protocol

Functions

def clear_cache()
Expand source code
def clear_cache():
    with autodiscover_cache:
        autodiscover_cache.clear()
def close_connections()
Expand source code
def close_connections():
    with autodiscover_cache:
        autodiscover_cache.close()
def discover(email, credentials=None, auth_type=None, retry_policy=None)
Expand source code
def discover(email, credentials=None, auth_type=None, retry_policy=None):
    return Autodiscovery(
        email=email, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
    ).discover()

Classes

class AutodiscoverCache

Stores the translation from (email domain, credentials) -> AutodiscoverProtocol object so we can re-use TCP connections to an autodiscover server within the same process. Also persists the email domain -> (autodiscover endpoint URL, auth_type) translation to the filesystem so the cache can be shared between multiple processes.

According to Microsoft, we may forever cache the (email domain -> autodiscover endpoint URL) mapping, or until it stops responding. My previous experience with Exchange products in mind, I'm not sure if I should trust that advice. But it could save some valuable seconds every time we start a new connection to a known server. In any case, the persistent storage must not contain any sensitive information since the cache could be readable by unprivileged users. Domain, endpoint and auth_type are OK to cache since this info is make publicly available on HTTP and DNS servers via the autodiscover protocol. Just don't persist any credentials info.

If an autodiscover lookup fails for any reason, the corresponding cache entry must be purged.

'shelve' is supposedly thread-safe and process-safe, which suits our needs.

Expand source code
class AutodiscoverCache:
    """Stores the translation from (email domain, credentials) -> AutodiscoverProtocol object so we can re-use TCP
    connections to an autodiscover server within the same process. Also persists the email domain -> (autodiscover
    endpoint URL, auth_type) translation to the filesystem so the cache can be shared between multiple processes.

    According to Microsoft, we may forever cache the (email domain -> autodiscover endpoint URL) mapping, or until
    it stops responding. My previous experience with Exchange products in mind, I'm not sure if I should trust that
    advice. But it could save some valuable seconds every time we start a new connection to a known server. In any
    case, the persistent storage must not contain any sensitive information since the cache could be readable by
    unprivileged users. Domain, endpoint and auth_type are OK to cache since this info is make publicly available on
    HTTP and DNS servers via the autodiscover protocol. Just don't persist any credentials info.

    If an autodiscover lookup fails for any reason, the corresponding cache entry must be purged.

    'shelve' is supposedly thread-safe and process-safe, which suits our needs.
    """

    def __init__(self):
        self._protocols = {}  # Mapping from (domain, credentials) to AutodiscoverProtocol
        self._lock = RLock()

    @property
    def _storage_file(self):
        return AUTODISCOVER_PERSISTENT_STORAGE

    def clear(self):
        # Wipe the entire cache
        with shelve_open_with_failover(self._storage_file) as db:
            db.clear()
        self._protocols.clear()

    def __len__(self):
        return len(self._protocols)

    def __contains__(self, key):
        domain = key[0]
        with shelve_open_with_failover(self._storage_file) as db:
            return str(domain) in db

    def __getitem__(self, key):
        protocol = self._protocols.get(key)
        if protocol:
            return protocol
        domain, credentials = key
        with shelve_open_with_failover(self._storage_file) as db:
            endpoint, auth_type, retry_policy = db[str(domain)]  # It's OK to fail with KeyError here
        protocol = AutodiscoverProtocol(config=Configuration(
            service_endpoint=endpoint, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
        ))
        self._protocols[key] = protocol
        return protocol

    def __setitem__(self, key, protocol):
        # Populate both local and persistent cache
        domain = key[0]
        with shelve_open_with_failover(self._storage_file) as db:
            # Don't change this payload without bumping the cache file version in shelve_filename()
            db[str(domain)] = (protocol.service_endpoint, protocol.auth_type, protocol.retry_policy)
        self._protocols[key] = protocol

    def __delitem__(self, key):
        # Empty both local and persistent cache. Don't fail on non-existing entries because we could end here
        # multiple times due to race conditions.
        domain = key[0]
        with shelve_open_with_failover(self._storage_file) as db:
            try:
                del db[str(domain)]
            except KeyError:
                pass
        try:
            del self._protocols[key]
        except KeyError:
            pass

    def close(self):
        # Close all open connections
        for (domain, _), protocol in self._protocols.items():
            log.debug('Domain %s: Closing sessions', domain)
            protocol.close()
            del protocol
        self._protocols.clear()

    def __enter__(self):
        self._lock.__enter__()

    def __exit__(self, *args, **kwargs):
        self._lock.__exit__(*args, **kwargs)

    def __del__(self):
        # pylint: disable=bare-except
        try:
            self.close()
        except Exception:  # nosec
            # __del__ should never fail
            pass

    def __str__(self):
        return str(self._protocols)

Methods

def clear(self)
Expand source code
def clear(self):
    # Wipe the entire cache
    with shelve_open_with_failover(self._storage_file) as db:
        db.clear()
    self._protocols.clear()
def close(self)
Expand source code
def close(self):
    # Close all open connections
    for (domain, _), protocol in self._protocols.items():
        log.debug('Domain %s: Closing sessions', domain)
        protocol.close()
        del protocol
    self._protocols.clear()
class AutodiscoverProtocol (config)

Protocol which implements the bare essentials for autodiscover.

Expand source code
class AutodiscoverProtocol(BaseProtocol):
    """Protocol which implements the bare essentials for autodiscover."""

    TIMEOUT = 10  # Seconds

    def __str__(self):
        return '''\
Autodiscover endpoint: %s
Auth type: %s''' % (
            self.service_endpoint,
            self.auth_type,
        )

Ancestors

Class variables

var TIMEOUT

Inherited members

class Autodiscovery (email, credentials=None, auth_type=None, retry_policy=None)

Autodiscover is a Microsoft protocol for automatically getting the endpoint of the Exchange server and other connection-related settings holding the email address using only the email address, and username and password of the user.

For a description of the protocol implemented, see "Autodiscover for Exchange ActiveSync developers":

https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-interoperability-guidance/hh352638%28v%3dexchg.140%29

Descriptions of the steps from the article are provided in their respective methods in this class.

For a description of how to handle autodiscover error messages, see:

https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/handling-autodiscover-error-messages

A tip from the article: The client can perform steps 1 through 4 in any order or in parallel to expedite the process, but it must wait for responses to finish at each step before proceeding. Given that many organizations prefer to use the URL in step 2 to set up the Autodiscover service, the client might try this step first.

Another possibly newer resource which has not yet been attempted is "Outlook 2016 Implementation of Autodiscover": https://support.microsoft.com/en-us/help/3211279/outlook-2016-implementation-of-autodiscover

WARNING: The autodiscover protocol is very complicated. If you have problems autodiscovering using this implementation, start by doing an official test at https://testconnectivity.microsoft.com

:param email: The email address to autodiscover :param credentials: Credentials with authorization to make autodiscover lookups for this Account (Default value = None) :param auth_type: (Default value = None) :param retry_policy: (Default value = None)

Expand source code
class Autodiscovery:
    """Autodiscover is a Microsoft protocol for automatically getting the endpoint of the Exchange server and other
    connection-related settings holding the email address using only the email address, and username and password of the
    user.

    For a description of the protocol implemented, see "Autodiscover for Exchange ActiveSync developers":

    https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-interoperability-guidance/hh352638%28v%3dexchg.140%29

    Descriptions of the steps from the article are provided in their respective methods in this class.

    For a description of how to handle autodiscover error messages, see:

    https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/handling-autodiscover-error-messages

    A tip from the article:
    The client can perform steps 1 through 4 in any order or in parallel to expedite the process, but it must wait for
    responses to finish at each step before proceeding. Given that many organizations prefer to use the URL in step 2 to
    set up the Autodiscover service, the client might try this step first.

    Another possibly newer resource which has not yet been attempted is "Outlook 2016 Implementation of Autodiscover":
    https://support.microsoft.com/en-us/help/3211279/outlook-2016-implementation-of-autodiscover

    WARNING: The autodiscover protocol is very complicated. If you have problems autodiscovering using this
    implementation, start by doing an official test at https://testconnectivity.microsoft.com
    """

    # When connecting to servers that may not be serving the correct endpoint, we should use a retry policy that does
    # not leave us hanging for a long time on each step in the protocol.
    INITIAL_RETRY_POLICY = FailFast()
    RETRY_WAIT = 10  # Seconds to wait before retry on connection errors
    MAX_REDIRECTS = 10  # Maximum number of URL redirects before we give up
    DNS_RESOLVER_KWARGS = {}
    DNS_RESOLVER_ATTRS = {
        'timeout': AutodiscoverProtocol.TIMEOUT,
    }

    def __init__(self, email, credentials=None, auth_type=None, retry_policy=None):
        """

        :param email: The email address to autodiscover
        :param credentials: Credentials with authorization to make autodiscover lookups for this Account
            (Default value = None)
        :param auth_type:  (Default value = None)
        :param retry_policy:  (Default value = None)
        """
        self.email = email
        self.credentials = credentials
        self.auth_type = auth_type  # The auth type that the resulting protocol instance should have
        self.retry_policy = retry_policy  # The retry policy that the resulting protocol instance should have
        self._urls_visited = []  # Collects HTTP and Autodiscover redirects
        self._redirect_count = 0
        self._emails_visited = []  # Collects Autodiscover email redirects

    def discover(self):
        self._emails_visited.append(self.email.lower())

        # Check the autodiscover cache to see if we already know the autodiscover service endpoint for this email
        # domain. Use a lock to guard against multiple threads competing to cache information.
        log.debug('Waiting for autodiscover_cache lock')
        with autodiscover_cache:
            log.debug('autodiscover_cache lock acquired')
            cache_key = self._cache_key
            domain = get_domain(self.email)
            if cache_key in autodiscover_cache:
                ad_protocol = autodiscover_cache[cache_key]
                log.debug('Cache hit for key %s: %s', cache_key, ad_protocol.service_endpoint)
                try:
                    ad_response = self._quick(protocol=ad_protocol)
                except AutoDiscoverFailed:
                    # Autodiscover no longer works with this domain. Clear cache and try again after releasing the lock
                    log.debug('AD request failure. Removing cache for key %s', cache_key)
                    del autodiscover_cache[cache_key]
                    ad_response = self._step_1(hostname=domain)
            else:
                # This will cache the result
                ad_response = self._step_1(hostname=domain)

        log.debug('Released autodiscover_cache_lock')
        if ad_response.redirect_address:
            log.debug('Got a redirect address: %s', ad_response.redirect_address)
            if ad_response.redirect_address.lower() in self._emails_visited:
                raise AutoDiscoverCircularRedirect('We were redirected to an email address we have already seen')

            # Start over, but with the new email address
            self.email = ad_response.redirect_address
            return self.discover()

        # We successfully received a response. Clear the cache of seen emails etc.
        self.clear()
        return self._build_response(ad_response=ad_response)

    def clear(self):
        # This resets cached variables
        self._urls_visited = []
        self._redirect_count = 0
        self._emails_visited = []

    @property
    def _cache_key(self):
        # We may be using multiple different credentials and changing our minds on TLS verification. This key
        # combination should be safe for caching.
        domain = get_domain(self.email)
        return domain, self.credentials

    @threaded_cached_property
    def resolver(self):
        resolver = dns.resolver.Resolver(**self.DNS_RESOLVER_KWARGS)
        for k, v in self.DNS_RESOLVER_ATTRS.items():
            setattr(resolver, k, v)
        return resolver

    def _build_response(self, ad_response):
        ews_url = ad_response.ews_url
        if not ews_url:
            raise AutoDiscoverFailed("Response is missing an 'ews_url' value")
        if not ad_response.autodiscover_smtp_address:
            # Autodiscover does not always return an email address. In that case, the requesting email should be used
            ad_response.user.autodiscover_smtp_address = self.email

        # Get the server version. Not all protocol entries have a server version so we cheat a bit and also look at the
        # other ones that point to the same endpoint.
        for protocol in ad_response.account.protocols:
            if not protocol.ews_url or not protocol.server_version:
                continue
            if protocol.ews_url.lower() == ews_url.lower():
                version = Version(build=protocol.server_version)
                break
        else:
            version = None
        # We may not want to use the auth_package hints in the AD response. It could be incorrect and we can just guess.
        protocol = Protocol(
            config=Configuration(
                service_endpoint=ews_url,
                credentials=self.credentials,
                version=version,
                auth_type=self.auth_type,
                retry_policy=self.retry_policy,
            )
        )
        return ad_response, protocol

    def _quick(self, protocol):
        # Reset auth type and retry policy if we requested non-default values
        if self.auth_type:
            protocol.config.auth_type = self.auth_type
        if self.retry_policy:
            protocol.config.retry_policy = self.retry_policy
        try:
            r = self._get_authenticated_response(protocol=protocol)
        except TransportError as e:
            raise AutoDiscoverFailed('Response error: %s' % e)
        if r.status_code == 200:
            try:
                ad = Autodiscover.from_bytes(bytes_content=r.content)
                return self._step_5(ad=ad)
            except ValueError as e:
                raise AutoDiscoverFailed('Invalid response: %s' % e)
        raise AutoDiscoverFailed('Invalid response code: %s' % r.status_code)

    def _redirect_url_is_valid(self, url):
        """Three separate responses can be “Redirect responses”:
        * An HTTP status code (301, 302) with a new URL
        * An HTTP status code of 200, but with a payload XML containing a redirect to a different URL
        * An HTTP status code of 200, but with a payload XML containing a different SMTP address as the target address

        We only handle the HTTP 302 redirects here. We validate the URL received in the redirect response to ensure that
        it does not redirect to non-SSL endpoints or SSL endpoints with invalid certificates, and that the redirect is
        not circular. Finally, we should fail after 10 redirects.

        :param url:
        :return:
        """
        if url.lower() in self._urls_visited:
            log.warning('We have already tried this URL: %s', url)
            return False

        if self._redirect_count >= self.MAX_REDIRECTS:
            log.warning('We reached max redirects at URL: %s', url)
            return False

        # We require TLS endpoints
        if not url.startswith('https://'):
            log.debug('Invalid scheme for URL: %s', url)
            return False

        # Quick test that the endpoint responds and that TLS handshake is OK
        try:
            self._get_unauthenticated_response(url, method='head')
        except TransportError as e:
            log.debug('Response error on redirect URL %s: %s', url, e)
            return False

        self._redirect_count += 1
        return True

    def _get_unauthenticated_response(self, url, method='post'):
        """Get auth type by tasting headers from the server. Do POST requests be default. HEAD is too error prone, and
        some servers are set up to redirect to OWA on all requests except POST to the autodiscover endpoint.

        :param url:
        :param method:  (Default value = 'post')
        :return:
        """
        # We are connecting to untrusted servers here, so take necessary precautions.
        hostname = urlparse(url).netloc
        if not self._is_valid_hostname(hostname):
            # 'requests' is really bad at reporting that a hostname cannot be resolved. Let's check this separately.
            # Don't retry on DNS errors. They will most likely be persistent.
            raise TransportError('%r has no DNS entry' % hostname)

        kwargs = dict(
            url=url, headers=DEFAULT_HEADERS.copy(), allow_redirects=False, timeout=AutodiscoverProtocol.TIMEOUT
        )
        if method == 'post':
            kwargs['data'] = Autodiscover.payload(email=self.email)
        retry = 0
        t_start = time.monotonic()
        while True:
            _back_off_if_needed(self.INITIAL_RETRY_POLICY.back_off_until)
            log.debug('Trying to get response from %s', url)
            with AutodiscoverProtocol.raw_session(url) as s:
                try:
                    r = getattr(s, method)(**kwargs)
                    r.close()  # Release memory
                    break
                except TLS_ERRORS as e:
                    # Don't retry on TLS errors. They will most likely be persistent.
                    raise TransportError(str(e))
                except CONNECTION_ERRORS as e:
                    r = DummyResponse(url=url, headers={}, request_headers=kwargs['headers'])
                    total_wait = time.monotonic() - t_start
                    if self.INITIAL_RETRY_POLICY.may_retry_on_error(response=r, wait=total_wait):
                        log.debug("Connection error on URL %s (retry %s, error: %s). Cool down", url, retry, e)
                        # Don't respect the 'Retry-After' header. We don't know if this is a useful endpoint, and we
                        # want autodiscover to be reasonably fast.
                        self.INITIAL_RETRY_POLICY.back_off(self.RETRY_WAIT)
                        retry += 1
                        continue
                    log.debug("Connection error on URL %s: %s", url, e)
                    raise TransportError(str(e))
        try:
            auth_type = get_auth_method_from_response(response=r)
        except UnauthorizedError:
            # Failed to guess the auth type
            auth_type = NOAUTH
        if r.status_code in (301, 302) and 'location' in r.headers:
            # Make the redirect URL absolute
            try:
                r.headers['location'] = get_redirect_url(r)
            except TransportError:
                del r.headers['location']
        return auth_type, r

    def _get_authenticated_response(self, protocol):
        """Get a response by using the credentials provided. We guess the auth type along the way.

        :param protocol:
        :return:
        """
        # Redo the request with the correct auth
        data = Autodiscover.payload(email=self.email)
        headers = DEFAULT_HEADERS.copy()
        session = protocol.get_session()
        if GSSAPI in AUTH_TYPE_MAP and isinstance(session.auth, AUTH_TYPE_MAP[GSSAPI]):
            # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/pox-autodiscover-request-for-exchange
            headers['X-ClientCanHandle'] = 'Negotiate'
        try:
            r, session = post_ratelimited(protocol=protocol, session=session, url=protocol.service_endpoint,
                                          headers=headers, data=data, allow_redirects=False, stream=False)
            protocol.release_session(session)
        except UnauthorizedError as e:
            # It's entirely possible for the endpoint to ask for login. We should continue if login fails because this
            # isn't necessarily the right endpoint to use.
            raise TransportError(str(e))
        except RedirectError as e:
            r = DummyResponse(url=protocol.service_endpoint, headers={'location': e.url}, request_headers=None,
                              status_code=302)
        return r

    def _attempt_response(self, url):
        """Return an (is_valid_response, response) tuple.

        :param url:
        :return:
        """
        self._urls_visited.append(url.lower())
        log.debug('Attempting to get a valid response from %s', url)
        try:
            auth_type, r = self._get_unauthenticated_response(url=url)
            if isinstance(self.credentials, OAuth2Credentials):
                # This type of credentials *must* use the OAuth auth type
                auth_type = OAUTH2
            elif self.credentials is None and auth_type in CREDENTIALS_REQUIRED:
                raise ValueError('Auth type %r was detected but no credentials were provided' % auth_type)
            ad_protocol = AutodiscoverProtocol(
                config=Configuration(
                    service_endpoint=url,
                    credentials=self.credentials,
                    auth_type=auth_type,
                    retry_policy=self.INITIAL_RETRY_POLICY,
                )
            )
            if auth_type != NOAUTH:
                r = self._get_authenticated_response(protocol=ad_protocol)
        except TransportError as e:
            log.debug('Failed to get a response: %s', e)
            return False, None
        if r.status_code in (301, 302) and 'location' in r.headers:
            redirect_url = get_redirect_url(r)
            if self._redirect_url_is_valid(url=redirect_url):
                # The protocol does not specify this explicitly, but by looking at how testconnectivity.microsoft.com
                # works, it seems that we should follow this URL now and try to get a valid response.
                return self._attempt_response(url=redirect_url)
        if r.status_code == 200:
            try:
                ad = Autodiscover.from_bytes(bytes_content=r.content)
                # We got a valid response. Unless this is a URL redirect response, we cache the result
                if ad.response is None or not ad.response.redirect_url:
                    cache_key = self._cache_key
                    log.debug('Adding cache entry for key %s: %s', cache_key, ad_protocol.service_endpoint)
                    autodiscover_cache[cache_key] = ad_protocol
                return True, ad
            except ValueError as e:
                log.debug('Invalid response: %s', e)
        return False, None

    def _is_valid_hostname(self, hostname):
        log.debug('Checking if %s can be looked up in DNS', hostname)
        try:
            self.resolver.resolve(hostname)
        except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
            return False
        return True

    def _get_srv_records(self, hostname):
        """Send a DNS query for SRV entries for the hostname.

        An SRV entry that has been formatted for autodiscovery will have the following format:

            canonical name = mail.example.com.
            service = 8 100 443 webmail.example.com.

        The first three numbers in the service line are: priority, weight, port

        :param hostname:
        :return:
        """
        log.debug('Attempting to get SRV records for %s', hostname)
        records = []
        try:
            answers = self.resolver.resolve('%s.' % hostname, 'SRV')
        except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) as e:
            log.debug('DNS lookup failure: %s', e)
            return records
        for rdata in answers:
            try:
                vals = rdata.to_text().strip().rstrip('.').split(' ')
                # Raise ValueError if the first three are not ints, and IndexError if there are less than 4 values
                priority, weight, port, srv = int(vals[0]), int(vals[1]), int(vals[2]), vals[3]
                record = SrvRecord(priority=priority, weight=weight, port=port, srv=srv)
                log.debug('Found SRV record %s ', record)
                records.append(record)
            except (ValueError, IndexError):
                log.debug('Incompatible SRV record for %s (%s)', hostname, rdata.to_text())
        return records

    def _step_1(self, hostname):
        """Perform step 1, where the client sends an Autodiscover request to
        https://example.com/autodiscover/autodiscover.xml and then does one of the following:
            * If the Autodiscover attempt succeeds, the client proceeds to step 5.
            * If the Autodiscover attempt fails, the client proceeds to step 2.

        :param hostname:
        :return:
        """
        url = 'https://%s/Autodiscover/Autodiscover.xml' % hostname
        log.info('Step 1: Trying autodiscover on %r with email %r', url, self.email)
        is_valid_response, ad = self._attempt_response(url=url)
        if is_valid_response:
            return self._step_5(ad=ad)
        return self._step_2(hostname=hostname)

    def _step_2(self, hostname):
        """Perform step 2, where the client sends an Autodiscover request to
        https://autodiscover.example.com/autodiscover/autodiscover.xml and then does one of the following:
            * If the Autodiscover attempt succeeds, the client proceeds to step 5.
            * If the Autodiscover attempt fails, the client proceeds to step 3.

        :param hostname:
        :return:
        """
        url = 'https://autodiscover.%s/Autodiscover/Autodiscover.xml' % hostname
        log.info('Step 2: Trying autodiscover on %r with email %r', url, self.email)
        is_valid_response, ad = self._attempt_response(url=url)
        if is_valid_response:
            return self._step_5(ad=ad)
        return self._step_3(hostname=hostname)

    def _step_3(self, hostname):
        """Perform step 3, where the client sends an unauth'ed GET method request to
        http://autodiscover.example.com/autodiscover/autodiscover.xml (Note that this is a non-HTTPS endpoint). The
        client then does one of the following:
            * If the GET request returns a 302 redirect response, it gets the redirection URL from the 'Location' HTTP
            header and validates it as described in the "Redirect responses" section. The client then does one of the
            following:
                * If the redirection URL is valid, the client tries the URL and then does one of the following:
                    * If the attempt succeeds, the client proceeds to step 5.
                    * If the attempt fails, the client proceeds to step 4.
                * If the redirection URL is not valid, the client proceeds to step 4.
            * If the GET request does not return a 302 redirect response, the client proceeds to step 4.

        :param hostname:
        :return:
        """
        url = 'http://autodiscover.%s/Autodiscover/Autodiscover.xml' % hostname
        log.info('Step 3: Trying autodiscover on %r with email %r', url, self.email)
        try:
            _, r = self._get_unauthenticated_response(url=url, method='get')
        except TransportError:
            r = DummyResponse(url=url, headers={}, request_headers={})
        if r.status_code in (301, 302) and 'location' in r.headers:
            redirect_url = get_redirect_url(r)
            if self._redirect_url_is_valid(url=redirect_url):
                is_valid_response, ad = self._attempt_response(url=redirect_url)
                if is_valid_response:
                    return self._step_5(ad=ad)
                return self._step_4(hostname=hostname)
            return self._step_4(hostname=hostname)
        return self._step_4(hostname=hostname)

    def _step_4(self, hostname):
        """Perform step 4, where the client performs a Domain Name System (DNS) query for an SRV record for
        _autodiscover._tcp.example.com. The query might return multiple records. The client selects only records that
        point to an SSL endpoint and that have the highest priority and weight. One of the following actions then
        occurs:
            * If no such records are returned, the client proceeds to step 6.
            * If records are returned, the application randomly chooses a record in the list and validates the endpoint
              that it points to by following the process described in the "Redirect Response" section. The client then
              does one of the following:
                * If the redirection URL is valid, the client tries the URL and then does one of the following:
                    * If the attempt succeeds, the client proceeds to step 5.
                    * If the attempt fails, the client proceeds to step 6.
                * If the redirection URL is not valid, the client proceeds to step 6.

        :param hostname:
        :return:
        """
        dns_hostname = '_autodiscover._tcp.%s' % hostname
        log.info('Step 4: Trying autodiscover on %r with email %r', dns_hostname, self.email)
        srv_records = self._get_srv_records(dns_hostname)
        try:
            srv_host = _select_srv_host(srv_records)
        except ValueError:
            srv_host = None
        if not srv_host:
            return self._step_6()
        redirect_url = 'https://%s/Autodiscover/Autodiscover.xml' % srv_host
        if self._redirect_url_is_valid(url=redirect_url):
            is_valid_response, ad = self._attempt_response(url=redirect_url)
            if is_valid_response:
                return self._step_5(ad=ad)
            return self._step_6()
        return self._step_6()

    def _step_5(self, ad):
        """Perform step 5. When a valid Autodiscover request succeeds, the following sequence occurs:
            * If the server responds with an HTTP 302 redirect, the client validates the redirection URL according to
              the process defined in the "Redirect responses" and then does one of the following:
                * If the redirection URL is valid, the client tries the URL and then does one of the following:
                    * If the attempt succeeds, the client repeats step 5 from the beginning.
                    * If the attempt fails, the client proceeds to step 6.
                * If the redirection URL is not valid, the client proceeds to step 6.
            * If the server responds with a valid Autodiscover response, the client does one of the following:
                * If the value of the Action element is "Redirect", the client gets the redirection email address from
                  the Redirect element and then returns to step 1, using this new email address.
                * If the value of the Action element is "Settings", the client has successfully received the requested
                  configuration settings for the specified user. The client does not need to proceed to step 6.

        :param ad:
        :return:
        """
        log.info('Step 5: Checking response')
        if ad.response is None:
            # This is not explicit in the protocol, but let's raise errors here
            ad.raise_errors()

        ad_response = ad.response
        if ad_response.redirect_url:
            log.debug('Got a redirect URL: %s', ad_response.redirect_url)
            # We are diverging a bit from the protocol here. We will never get an HTTP 302 since earlier steps already
            # followed the redirects where possible. Instead, we handle retirect responses here.
            if self._redirect_url_is_valid(url=ad_response.redirect_url):
                is_valid_response, ad = self._attempt_response(url=ad_response.redirect_url)
                if is_valid_response:
                    return self._step_5(ad=ad)
                return self._step_6()
            log.debug('Invalid redirect URL: %s', ad_response.redirect_url)
            return self._step_6()
        # This could be an email redirect. Let outer layer handle this
        return ad_response

    def _step_6(self):
        """Perform step 6. If the client cannot contact the Autodiscover service, the client should ask the user for
        the Exchange server name and use it to construct an Exchange EWS URL. The client should try to use this URL for
        future requests.
        """
        raise AutoDiscoverFailed(
            'All steps in the autodiscover protocol failed for email %r. If you think this is an error, consider doing '
            'an official test at https://testconnectivity.microsoft.com' % self.email)

Class variables

var DNS_RESOLVER_ATTRS
var DNS_RESOLVER_KWARGS
var INITIAL_RETRY_POLICY
var MAX_REDIRECTS
var RETRY_WAIT

Instance variables

var resolver
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))

Methods

def clear(self)
Expand source code
def clear(self):
    # This resets cached variables
    self._urls_visited = []
    self._redirect_count = 0
    self._emails_visited = []
def discover(self)
Expand source code
def discover(self):
    self._emails_visited.append(self.email.lower())

    # Check the autodiscover cache to see if we already know the autodiscover service endpoint for this email
    # domain. Use a lock to guard against multiple threads competing to cache information.
    log.debug('Waiting for autodiscover_cache lock')
    with autodiscover_cache:
        log.debug('autodiscover_cache lock acquired')
        cache_key = self._cache_key
        domain = get_domain(self.email)
        if cache_key in autodiscover_cache:
            ad_protocol = autodiscover_cache[cache_key]
            log.debug('Cache hit for key %s: %s', cache_key, ad_protocol.service_endpoint)
            try:
                ad_response = self._quick(protocol=ad_protocol)
            except AutoDiscoverFailed:
                # Autodiscover no longer works with this domain. Clear cache and try again after releasing the lock
                log.debug('AD request failure. Removing cache for key %s', cache_key)
                del autodiscover_cache[cache_key]
                ad_response = self._step_1(hostname=domain)
        else:
            # This will cache the result
            ad_response = self._step_1(hostname=domain)

    log.debug('Released autodiscover_cache_lock')
    if ad_response.redirect_address:
        log.debug('Got a redirect address: %s', ad_response.redirect_address)
        if ad_response.redirect_address.lower() in self._emails_visited:
            raise AutoDiscoverCircularRedirect('We were redirected to an email address we have already seen')

        # Start over, but with the new email address
        self.email = ad_response.redirect_address
        return self.discover()

    # We successfully received a response. Clear the cache of seen emails etc.
    self.clear()
    return self._build_response(ad_response=ad_response)
exchangelib-4.6.1/docs/exchangelib/autodiscover/properties.html000066400000000000000000003603671414601472700250120ustar00rootroot00000000000000 exchangelib.autodiscover.properties API documentation

Module exchangelib.autodiscover.properties

Expand source code
from ..errors import ErrorNonExistentMailbox, AutoDiscoverFailed
from ..fields import TextField, EmailAddressField, ChoiceField, Choice, EWSElementField, OnOffField, BooleanField, \
    IntegerField, BuildField, ProtocolListField
from ..properties import EWSElement
from ..transport import DEFAULT_ENCODING, NOAUTH, NTLM, BASIC, GSSAPI, SSPI, CBA
from ..util import create_element, add_xml_child, to_xml, is_xml, xml_to_str, AUTODISCOVER_REQUEST_NS, \
    AUTODISCOVER_BASE_NS, AUTODISCOVER_RESPONSE_NS as RNS, ParseError


class AutodiscoverBase(EWSElement):
    NAMESPACE = RNS


class User(AutodiscoverBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/user-pox"""

    ELEMENT_NAME = 'User'

    display_name = TextField(field_uri='DisplayName', namespace=RNS)
    legacy_dn = TextField(field_uri='LegacyDN', namespace=RNS)
    deployment_id = TextField(field_uri='DeploymentId', namespace=RNS)  # GUID format
    autodiscover_smtp_address = EmailAddressField(field_uri='AutoDiscoverSMTPAddress', namespace=RNS)


class IntExtUrlBase(AutodiscoverBase):
    external_url = TextField(field_uri='ExternalUrl', namespace=RNS)
    internal_url = TextField(field_uri='InternalUrl', namespace=RNS)


class AddressBook(IntExtUrlBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/addressbook-pox"""

    ELEMENT_NAME = 'AddressBook'


class MailStore(IntExtUrlBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailstore-pox"""

    ELEMENT_NAME = 'MailStore'


class NetworkRequirements(AutodiscoverBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/networkrequirements-pox"""

    ELEMENT_NAME = 'NetworkRequirements'

    ipv4_start = TextField(field_uri='IPv4Start', namespace=RNS)
    ipv4_end = TextField(field_uri='IPv4End', namespace=RNS)
    ipv6_start = TextField(field_uri='IPv6Start', namespace=RNS)
    ipv6_end = TextField(field_uri='IPv6End', namespace=RNS)


class SimpleProtocol(AutodiscoverBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox

    Used for the 'Internal' and 'External' elements that may contain a stripped-down version of the Protocol element.
    """

    ELEMENT_NAME = 'Protocol'
    WEB = 'WEB'
    EXCH = 'EXCH'
    EXPR = 'EXPR'
    EXHTTP = 'EXHTTP'
    TYPES = (WEB, EXCH, EXPR, EXHTTP)

    type = ChoiceField(field_uri='Type', choices={Choice(c) for c in TYPES}, namespace=RNS)
    as_url = TextField(field_uri='ASUrl', namespace=RNS)


class IntExtBase(AutodiscoverBase):
    # TODO: 'OWAUrl' also has an AuthenticationMethod enum-style XML attribute with values:
    #  WindowsIntegrated, FBA, NTLM, Digest, Basic
    owa_url = TextField(field_uri='OWAUrl', namespace=RNS)
    protocol = EWSElementField(value_cls=SimpleProtocol)


class Internal(IntExtBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/internal-pox"""

    ELEMENT_NAME = 'Internal'


class External(IntExtBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/external-pox"""

    ELEMENT_NAME = 'External'


class Protocol(SimpleProtocol):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox"""

    # Attribute 'Type' is ignored here. Has a name conflict with the child element and does not seem useful.
    version = TextField(field_uri='Version', is_attribute=True, namespace=RNS)
    internal = EWSElementField(value_cls=Internal)
    external = EWSElementField(value_cls=External)
    ttl = IntegerField(field_uri='TTL', namespace=RNS, default=1)  # TTL for this autodiscover response, in hours
    server = TextField(field_uri='Server', namespace=RNS)
    server_dn = TextField(field_uri='ServerDN', namespace=RNS)
    server_version = BuildField(field_uri='ServerVersion', namespace=RNS)
    mdb_dn = TextField(field_uri='MdbDN', namespace=RNS)
    public_folder_server = TextField(field_uri='PublicFolderServer', namespace=RNS)
    port = IntegerField(field_uri='Port', namespace=RNS, min=1, max=65535)
    directory_port = IntegerField(field_uri='DirectoryPort', namespace=RNS, min=1, max=65535)
    referral_port = IntegerField(field_uri='ReferralPort', namespace=RNS, min=1, max=65535)
    ews_url = TextField(field_uri='EwsUrl', namespace=RNS)
    emws_url = TextField(field_uri='EmwsUrl', namespace=RNS)
    sharing_url = TextField(field_uri='SharingUrl', namespace=RNS)
    ecp_url = TextField(field_uri='EcpUrl', namespace=RNS)
    ecp_url_um = TextField(field_uri='EcpUrl-um', namespace=RNS)
    ecp_url_aggr = TextField(field_uri='EcpUrl-aggr', namespace=RNS)
    ecp_url_mt = TextField(field_uri='EcpUrl-mt', namespace=RNS)
    ecp_url_ret = TextField(field_uri='EcpUrl-ret', namespace=RNS)
    ecp_url_sms = TextField(field_uri='EcpUrl-sms', namespace=RNS)
    ecp_url_publish = TextField(field_uri='EcpUrl-publish', namespace=RNS)
    ecp_url_photo = TextField(field_uri='EcpUrl-photo', namespace=RNS)
    ecp_url_tm = TextField(field_uri='EcpUrl-tm', namespace=RNS)
    ecp_url_tm_creating = TextField(field_uri='EcpUrl-tmCreating', namespace=RNS)
    ecp_url_tm_hiding = TextField(field_uri='EcpUrl-tmHiding', namespace=RNS)
    ecp_url_tm_editing = TextField(field_uri='EcpUrl-tmEditing', namespace=RNS)
    ecp_url_extinstall = TextField(field_uri='EcpUrl-extinstall', namespace=RNS)
    oof_url = TextField(field_uri='OOFUrl', namespace=RNS)
    oab_url = TextField(field_uri='OABUrl', namespace=RNS)
    um_url = TextField(field_uri='UMUrl', namespace=RNS)
    ews_partner_url = TextField(field_uri='EwsPartnerUrl', namespace=RNS)
    login_name = TextField(field_uri='LoginName', namespace=RNS)
    domain_required = OnOffField(field_uri='DomainRequired', namespace=RNS)
    domain_name = TextField(field_uri='DomainName', namespace=RNS)
    spa = OnOffField(field_uri='SPA', namespace=RNS, default=True)
    auth_package = ChoiceField(field_uri='AuthPackage', namespace=RNS, choices={
        Choice(c) for c in ('basic', 'kerb', 'kerbntlm', 'ntlm', 'certificate', 'negotiate', 'nego2')
    })
    cert_principal_name = TextField(field_uri='CertPrincipalName', namespace=RNS)
    ssl = OnOffField(field_uri='SSL', namespace=RNS, default=True)
    auth_required = OnOffField(field_uri='AuthRequired', namespace=RNS, default=True)
    use_pop_path = OnOffField(field_uri='UsePOPAuth', namespace=RNS)
    smtp_last = OnOffField(field_uri='SMTPLast', namespace=RNS, default=False)
    network_requirements = EWSElementField(value_cls=NetworkRequirements)
    address_book = EWSElementField(value_cls=AddressBook)
    mail_store = EWSElementField(value_cls=MailStore)

    @property
    def auth_type(self):
        # Translates 'auth_package' value to our own 'auth_type' enum vals
        if not self.auth_required:
            return NOAUTH
        return {
            # Missing in list are DIGEST and OAUTH2
            'basic': BASIC,
            'kerb': GSSAPI,
            'kerbntlm': NTLM,  # Means client can chose between NTLM and GSSAPI
            'ntlm': NTLM,
            'certificate': CBA,
            'negotiate': SSPI,  # Unsure about this one
            'nego2': GSSAPI,
            'anonymous': NOAUTH,  # Seen in some docs even though it's not mentioned in MSDN
        }.get(self.auth_package.lower(), NTLM)  # Default to NTLM


class Error(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/error-pox"""

    ELEMENT_NAME = 'Error'
    NAMESPACE = AUTODISCOVER_BASE_NS

    id = TextField(field_uri='Id', namespace=AUTODISCOVER_BASE_NS, is_attribute=True)
    time = TextField(field_uri='Time', namespace=AUTODISCOVER_BASE_NS, is_attribute=True)
    code = TextField(field_uri='ErrorCode', namespace=AUTODISCOVER_BASE_NS)
    message = TextField(field_uri='Message', namespace=AUTODISCOVER_BASE_NS)
    debug_data = TextField(field_uri='DebugData', namespace=AUTODISCOVER_BASE_NS)


class Account(AutodiscoverBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/account-pox"""

    ELEMENT_NAME = 'Account'
    REDIRECT_URL = 'redirectUrl'
    REDIRECT_ADDR = 'redirectAddr'
    SETTINGS = 'settings'
    ACTIONS = (REDIRECT_URL, REDIRECT_ADDR, SETTINGS)

    type = ChoiceField(field_uri='AccountType', namespace=RNS, choices={Choice('email')})
    action = ChoiceField(field_uri='Action', namespace=RNS, choices={Choice(p) for p in ACTIONS})
    microsoft_online = BooleanField(field_uri='MicrosoftOnline', namespace=RNS)
    redirect_url = TextField(field_uri='RedirectURL', namespace=RNS)
    redirect_address = EmailAddressField(field_uri='RedirectAddr', namespace=RNS)
    image = TextField(field_uri='Image', namespace=RNS)  # Path to image used for branding
    service_home = TextField(field_uri='ServiceHome', namespace=RNS)  # URL to website of ISP
    protocols = ProtocolListField()
    # 'SmtpAddress' is inside the 'PublicFolderInformation' element
    public_folder_smtp_address = TextField(field_uri='SmtpAddress', namespace=RNS)

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {}
        public_folder_information = elem.find('{%s}PublicFolderInformation' % cls.NAMESPACE)
        for f in cls.FIELDS:
            if f.name == 'public_folder_smtp_address':
                if public_folder_information is None:
                    continue
                kwargs[f.name] = f.from_xml(elem=public_folder_information, account=account)
                continue
            kwargs[f.name] = f.from_xml(elem=elem, account=account)
        cls._clear(elem)
        return cls(**kwargs)


class Response(AutodiscoverBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/response-pox"""

    ELEMENT_NAME = 'Response'

    user = EWSElementField(value_cls=User)
    account = EWSElementField(value_cls=Account)

    @property
    def redirect_address(self):
        try:
            if self.account.action != Account.REDIRECT_ADDR:
                return None
            return self.account.redirect_address
        except AttributeError:
            return None

    @property
    def redirect_url(self):
        try:
            if self.account.action != Account.REDIRECT_URL:
                return None
            return self.account.redirect_url
        except AttributeError:
            return None

    @property
    def autodiscover_smtp_address(self):
        # AutoDiscoverSMTPAddress might not be present in the XML. In this case, use the original email address.
        try:
            if self.account.action != Account.SETTINGS:
                return None
            return self.user.autodiscover_smtp_address
        except AttributeError:
            return None

    @property
    def ews_url(self):
        """Return the EWS URL contained in the response.

        A response may contain a number of possible protocol types. EXPR is meant for EWS. See
        https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16

        We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available.

        Additionally, some responses may contain and EXPR with no EWS URL. In that case, return the URL from EXCH, if
        available.
        """
        protocols = {p.type: p for p in self.account.protocols if p.ews_url}
        if Protocol.EXPR in protocols:
            return protocols[Protocol.EXPR].ews_url
        if Protocol.EXCH in protocols:
            return protocols[Protocol.EXCH].ews_url
        raise ValueError(
            'No EWS URL found in any of the available protocols: %s' % [str(p) for p in self.account.protocols]
        )


class ErrorResponse(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/response-pox

    Like 'Response', but with a different namespace.
    """

    ELEMENT_NAME = 'Response'
    NAMESPACE = AUTODISCOVER_BASE_NS

    error = EWSElementField(value_cls=Error)


class Autodiscover(EWSElement):
    ELEMENT_NAME = 'Autodiscover'
    NAMESPACE = AUTODISCOVER_BASE_NS

    response = EWSElementField(value_cls=Response)
    error_response = EWSElementField(value_cls=ErrorResponse)

    @staticmethod
    def _clear(elem):
        # Parent implementation also clears the parent, but this element doesn't have one.
        elem.clear()

    @classmethod
    def from_bytes(cls, bytes_content):
        """Create an instance from response bytes. An Autodiscover request and response example is available at:
        https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/pox-autodiscover-response-for-exchange

        :param bytes_content:
        :return:
        """
        if not is_xml(bytes_content) and not is_xml(bytes_content, expected_prefix=b'<Autodiscover '):
            raise ValueError('Response is not XML: %s' % bytes_content)
        try:
            root = to_xml(bytes_content).getroot()
        except ParseError:
            raise ValueError('Error parsing XML: %s' % bytes_content)
        if root.tag != cls.response_tag():
            raise ValueError('Unknown root element in XML: %s' % bytes_content)
        return cls.from_xml(elem=root, account=None)

    def raise_errors(self):
        # Find an error message in the response and raise the relevant exception
        try:
            errorcode = self.error_response.error.code
            message = self.error_response.error.message
            if message in ('The e-mail address cannot be found.', "The email address can't be found."):
                raise ErrorNonExistentMailbox('The SMTP address has no mailbox associated with it')
            raise AutoDiscoverFailed('Unknown error %s: %s' % (errorcode, message))
        except AttributeError:
            raise AutoDiscoverFailed('Unknown autodiscover error response: %s' % self)

    @staticmethod
    def payload(email):
        # Builds a full Autodiscover XML request
        payload = create_element('Autodiscover', attrs=dict(xmlns=AUTODISCOVER_REQUEST_NS))
        request = create_element('Request')
        add_xml_child(request, 'EMailAddress', email)
        add_xml_child(request, 'AcceptableResponseSchema', RNS)
        payload.append(request)
        return xml_to_str(payload, encoding=DEFAULT_ENCODING, xml_declaration=True)

Classes

class Account (**kwargs)
Expand source code
class Account(AutodiscoverBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/account-pox"""

    ELEMENT_NAME = 'Account'
    REDIRECT_URL = 'redirectUrl'
    REDIRECT_ADDR = 'redirectAddr'
    SETTINGS = 'settings'
    ACTIONS = (REDIRECT_URL, REDIRECT_ADDR, SETTINGS)

    type = ChoiceField(field_uri='AccountType', namespace=RNS, choices={Choice('email')})
    action = ChoiceField(field_uri='Action', namespace=RNS, choices={Choice(p) for p in ACTIONS})
    microsoft_online = BooleanField(field_uri='MicrosoftOnline', namespace=RNS)
    redirect_url = TextField(field_uri='RedirectURL', namespace=RNS)
    redirect_address = EmailAddressField(field_uri='RedirectAddr', namespace=RNS)
    image = TextField(field_uri='Image', namespace=RNS)  # Path to image used for branding
    service_home = TextField(field_uri='ServiceHome', namespace=RNS)  # URL to website of ISP
    protocols = ProtocolListField()
    # 'SmtpAddress' is inside the 'PublicFolderInformation' element
    public_folder_smtp_address = TextField(field_uri='SmtpAddress', namespace=RNS)

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {}
        public_folder_information = elem.find('{%s}PublicFolderInformation' % cls.NAMESPACE)
        for f in cls.FIELDS:
            if f.name == 'public_folder_smtp_address':
                if public_folder_information is None:
                    continue
                kwargs[f.name] = f.from_xml(elem=public_folder_information, account=account)
                continue
            kwargs[f.name] = f.from_xml(elem=elem, account=account)
        cls._clear(elem)
        return cls(**kwargs)

Ancestors

Class variables

var ACTIONS
var ELEMENT_NAME
var FIELDS
var REDIRECT_ADDR
var REDIRECT_URL
var SETTINGS

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    kwargs = {}
    public_folder_information = elem.find('{%s}PublicFolderInformation' % cls.NAMESPACE)
    for f in cls.FIELDS:
        if f.name == 'public_folder_smtp_address':
            if public_folder_information is None:
                continue
            kwargs[f.name] = f.from_xml(elem=public_folder_information, account=account)
            continue
        kwargs[f.name] = f.from_xml(elem=elem, account=account)
    cls._clear(elem)
    return cls(**kwargs)

Instance variables

var action
var image
var microsoft_online
var protocols
var public_folder_smtp_address
var redirect_address
var redirect_url
var service_home
var type

Inherited members

class AddressBook (**kwargs)
Expand source code
class AddressBook(IntExtUrlBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/addressbook-pox"""

    ELEMENT_NAME = 'AddressBook'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class Autodiscover (**kwargs)

Base class for all XML element implementations.

Expand source code
class Autodiscover(EWSElement):
    ELEMENT_NAME = 'Autodiscover'
    NAMESPACE = AUTODISCOVER_BASE_NS

    response = EWSElementField(value_cls=Response)
    error_response = EWSElementField(value_cls=ErrorResponse)

    @staticmethod
    def _clear(elem):
        # Parent implementation also clears the parent, but this element doesn't have one.
        elem.clear()

    @classmethod
    def from_bytes(cls, bytes_content):
        """Create an instance from response bytes. An Autodiscover request and response example is available at:
        https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/pox-autodiscover-response-for-exchange

        :param bytes_content:
        :return:
        """
        if not is_xml(bytes_content) and not is_xml(bytes_content, expected_prefix=b'<Autodiscover '):
            raise ValueError('Response is not XML: %s' % bytes_content)
        try:
            root = to_xml(bytes_content).getroot()
        except ParseError:
            raise ValueError('Error parsing XML: %s' % bytes_content)
        if root.tag != cls.response_tag():
            raise ValueError('Unknown root element in XML: %s' % bytes_content)
        return cls.from_xml(elem=root, account=None)

    def raise_errors(self):
        # Find an error message in the response and raise the relevant exception
        try:
            errorcode = self.error_response.error.code
            message = self.error_response.error.message
            if message in ('The e-mail address cannot be found.', "The email address can't be found."):
                raise ErrorNonExistentMailbox('The SMTP address has no mailbox associated with it')
            raise AutoDiscoverFailed('Unknown error %s: %s' % (errorcode, message))
        except AttributeError:
            raise AutoDiscoverFailed('Unknown autodiscover error response: %s' % self)

    @staticmethod
    def payload(email):
        # Builds a full Autodiscover XML request
        payload = create_element('Autodiscover', attrs=dict(xmlns=AUTODISCOVER_REQUEST_NS))
        request = create_element('Request')
        add_xml_child(request, 'EMailAddress', email)
        add_xml_child(request, 'AcceptableResponseSchema', RNS)
        payload.append(request)
        return xml_to_str(payload, encoding=DEFAULT_ENCODING, xml_declaration=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var NAMESPACE

Static methods

def from_bytes(bytes_content)

Create an instance from response bytes. An Autodiscover request and response example is available at: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/pox-autodiscover-response-for-exchange

:param bytes_content: :return:

Expand source code
@classmethod
def from_bytes(cls, bytes_content):
    """Create an instance from response bytes. An Autodiscover request and response example is available at:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/pox-autodiscover-response-for-exchange

    :param bytes_content:
    :return:
    """
    if not is_xml(bytes_content) and not is_xml(bytes_content, expected_prefix=b'<Autodiscover '):
        raise ValueError('Response is not XML: %s' % bytes_content)
    try:
        root = to_xml(bytes_content).getroot()
    except ParseError:
        raise ValueError('Error parsing XML: %s' % bytes_content)
    if root.tag != cls.response_tag():
        raise ValueError('Unknown root element in XML: %s' % bytes_content)
    return cls.from_xml(elem=root, account=None)
def payload(email)
Expand source code
@staticmethod
def payload(email):
    # Builds a full Autodiscover XML request
    payload = create_element('Autodiscover', attrs=dict(xmlns=AUTODISCOVER_REQUEST_NS))
    request = create_element('Request')
    add_xml_child(request, 'EMailAddress', email)
    add_xml_child(request, 'AcceptableResponseSchema', RNS)
    payload.append(request)
    return xml_to_str(payload, encoding=DEFAULT_ENCODING, xml_declaration=True)

Instance variables

var error_response
var response

Methods

def raise_errors(self)
Expand source code
def raise_errors(self):
    # Find an error message in the response and raise the relevant exception
    try:
        errorcode = self.error_response.error.code
        message = self.error_response.error.message
        if message in ('The e-mail address cannot be found.', "The email address can't be found."):
            raise ErrorNonExistentMailbox('The SMTP address has no mailbox associated with it')
        raise AutoDiscoverFailed('Unknown error %s: %s' % (errorcode, message))
    except AttributeError:
        raise AutoDiscoverFailed('Unknown autodiscover error response: %s' % self)

Inherited members

class AutodiscoverBase (**kwargs)

Base class for all XML element implementations.

Expand source code
class AutodiscoverBase(EWSElement):
    NAMESPACE = RNS

Ancestors

Subclasses

Class variables

var NAMESPACE

Inherited members

class Error (**kwargs)
Expand source code
class Error(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/error-pox"""

    ELEMENT_NAME = 'Error'
    NAMESPACE = AUTODISCOVER_BASE_NS

    id = TextField(field_uri='Id', namespace=AUTODISCOVER_BASE_NS, is_attribute=True)
    time = TextField(field_uri='Time', namespace=AUTODISCOVER_BASE_NS, is_attribute=True)
    code = TextField(field_uri='ErrorCode', namespace=AUTODISCOVER_BASE_NS)
    message = TextField(field_uri='Message', namespace=AUTODISCOVER_BASE_NS)
    debug_data = TextField(field_uri='DebugData', namespace=AUTODISCOVER_BASE_NS)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var NAMESPACE

Instance variables

var code
var debug_data
var id
var message
var time

Inherited members

class ErrorResponse (**kwargs)
Expand source code
class ErrorResponse(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/response-pox

    Like 'Response', but with a different namespace.
    """

    ELEMENT_NAME = 'Response'
    NAMESPACE = AUTODISCOVER_BASE_NS

    error = EWSElementField(value_cls=Error)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var NAMESPACE

Instance variables

var error

Inherited members

class External (**kwargs)
Expand source code
class External(IntExtBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/external-pox"""

    ELEMENT_NAME = 'External'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class IntExtBase (**kwargs)

Base class for all XML element implementations.

Expand source code
class IntExtBase(AutodiscoverBase):
    # TODO: 'OWAUrl' also has an AuthenticationMethod enum-style XML attribute with values:
    #  WindowsIntegrated, FBA, NTLM, Digest, Basic
    owa_url = TextField(field_uri='OWAUrl', namespace=RNS)
    protocol = EWSElementField(value_cls=SimpleProtocol)

Ancestors

Subclasses

Class variables

var FIELDS

Instance variables

var owa_url
var protocol

Inherited members

class IntExtUrlBase (**kwargs)

Base class for all XML element implementations.

Expand source code
class IntExtUrlBase(AutodiscoverBase):
    external_url = TextField(field_uri='ExternalUrl', namespace=RNS)
    internal_url = TextField(field_uri='InternalUrl', namespace=RNS)

Ancestors

Subclasses

Class variables

var FIELDS

Instance variables

var external_url
var internal_url

Inherited members

class Internal (**kwargs)
Expand source code
class Internal(IntExtBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/internal-pox"""

    ELEMENT_NAME = 'Internal'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class MailStore (**kwargs)
Expand source code
class MailStore(IntExtUrlBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailstore-pox"""

    ELEMENT_NAME = 'MailStore'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class NetworkRequirements (**kwargs)
Expand source code
class NetworkRequirements(AutodiscoverBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/networkrequirements-pox"""

    ELEMENT_NAME = 'NetworkRequirements'

    ipv4_start = TextField(field_uri='IPv4Start', namespace=RNS)
    ipv4_end = TextField(field_uri='IPv4End', namespace=RNS)
    ipv6_start = TextField(field_uri='IPv6Start', namespace=RNS)
    ipv6_end = TextField(field_uri='IPv6End', namespace=RNS)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var ipv4_end
var ipv4_start
var ipv6_end
var ipv6_start

Inherited members

class Protocol (**kwargs)
Expand source code
class Protocol(SimpleProtocol):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox"""

    # Attribute 'Type' is ignored here. Has a name conflict with the child element and does not seem useful.
    version = TextField(field_uri='Version', is_attribute=True, namespace=RNS)
    internal = EWSElementField(value_cls=Internal)
    external = EWSElementField(value_cls=External)
    ttl = IntegerField(field_uri='TTL', namespace=RNS, default=1)  # TTL for this autodiscover response, in hours
    server = TextField(field_uri='Server', namespace=RNS)
    server_dn = TextField(field_uri='ServerDN', namespace=RNS)
    server_version = BuildField(field_uri='ServerVersion', namespace=RNS)
    mdb_dn = TextField(field_uri='MdbDN', namespace=RNS)
    public_folder_server = TextField(field_uri='PublicFolderServer', namespace=RNS)
    port = IntegerField(field_uri='Port', namespace=RNS, min=1, max=65535)
    directory_port = IntegerField(field_uri='DirectoryPort', namespace=RNS, min=1, max=65535)
    referral_port = IntegerField(field_uri='ReferralPort', namespace=RNS, min=1, max=65535)
    ews_url = TextField(field_uri='EwsUrl', namespace=RNS)
    emws_url = TextField(field_uri='EmwsUrl', namespace=RNS)
    sharing_url = TextField(field_uri='SharingUrl', namespace=RNS)
    ecp_url = TextField(field_uri='EcpUrl', namespace=RNS)
    ecp_url_um = TextField(field_uri='EcpUrl-um', namespace=RNS)
    ecp_url_aggr = TextField(field_uri='EcpUrl-aggr', namespace=RNS)
    ecp_url_mt = TextField(field_uri='EcpUrl-mt', namespace=RNS)
    ecp_url_ret = TextField(field_uri='EcpUrl-ret', namespace=RNS)
    ecp_url_sms = TextField(field_uri='EcpUrl-sms', namespace=RNS)
    ecp_url_publish = TextField(field_uri='EcpUrl-publish', namespace=RNS)
    ecp_url_photo = TextField(field_uri='EcpUrl-photo', namespace=RNS)
    ecp_url_tm = TextField(field_uri='EcpUrl-tm', namespace=RNS)
    ecp_url_tm_creating = TextField(field_uri='EcpUrl-tmCreating', namespace=RNS)
    ecp_url_tm_hiding = TextField(field_uri='EcpUrl-tmHiding', namespace=RNS)
    ecp_url_tm_editing = TextField(field_uri='EcpUrl-tmEditing', namespace=RNS)
    ecp_url_extinstall = TextField(field_uri='EcpUrl-extinstall', namespace=RNS)
    oof_url = TextField(field_uri='OOFUrl', namespace=RNS)
    oab_url = TextField(field_uri='OABUrl', namespace=RNS)
    um_url = TextField(field_uri='UMUrl', namespace=RNS)
    ews_partner_url = TextField(field_uri='EwsPartnerUrl', namespace=RNS)
    login_name = TextField(field_uri='LoginName', namespace=RNS)
    domain_required = OnOffField(field_uri='DomainRequired', namespace=RNS)
    domain_name = TextField(field_uri='DomainName', namespace=RNS)
    spa = OnOffField(field_uri='SPA', namespace=RNS, default=True)
    auth_package = ChoiceField(field_uri='AuthPackage', namespace=RNS, choices={
        Choice(c) for c in ('basic', 'kerb', 'kerbntlm', 'ntlm', 'certificate', 'negotiate', 'nego2')
    })
    cert_principal_name = TextField(field_uri='CertPrincipalName', namespace=RNS)
    ssl = OnOffField(field_uri='SSL', namespace=RNS, default=True)
    auth_required = OnOffField(field_uri='AuthRequired', namespace=RNS, default=True)
    use_pop_path = OnOffField(field_uri='UsePOPAuth', namespace=RNS)
    smtp_last = OnOffField(field_uri='SMTPLast', namespace=RNS, default=False)
    network_requirements = EWSElementField(value_cls=NetworkRequirements)
    address_book = EWSElementField(value_cls=AddressBook)
    mail_store = EWSElementField(value_cls=MailStore)

    @property
    def auth_type(self):
        # Translates 'auth_package' value to our own 'auth_type' enum vals
        if not self.auth_required:
            return NOAUTH
        return {
            # Missing in list are DIGEST and OAUTH2
            'basic': BASIC,
            'kerb': GSSAPI,
            'kerbntlm': NTLM,  # Means client can chose between NTLM and GSSAPI
            'ntlm': NTLM,
            'certificate': CBA,
            'negotiate': SSPI,  # Unsure about this one
            'nego2': GSSAPI,
            'anonymous': NOAUTH,  # Seen in some docs even though it's not mentioned in MSDN
        }.get(self.auth_package.lower(), NTLM)  # Default to NTLM

Ancestors

Class variables

var FIELDS

Instance variables

var address_book
var auth_package
var auth_required
var auth_type
Expand source code
@property
def auth_type(self):
    # Translates 'auth_package' value to our own 'auth_type' enum vals
    if not self.auth_required:
        return NOAUTH
    return {
        # Missing in list are DIGEST and OAUTH2
        'basic': BASIC,
        'kerb': GSSAPI,
        'kerbntlm': NTLM,  # Means client can chose between NTLM and GSSAPI
        'ntlm': NTLM,
        'certificate': CBA,
        'negotiate': SSPI,  # Unsure about this one
        'nego2': GSSAPI,
        'anonymous': NOAUTH,  # Seen in some docs even though it's not mentioned in MSDN
    }.get(self.auth_package.lower(), NTLM)  # Default to NTLM
var cert_principal_name
var directory_port
var domain_name
var domain_required
var ecp_url
var ecp_url_aggr
var ecp_url_extinstall
var ecp_url_mt
var ecp_url_photo
var ecp_url_publish
var ecp_url_ret
var ecp_url_sms
var ecp_url_tm
var ecp_url_tm_creating
var ecp_url_tm_editing
var ecp_url_tm_hiding
var ecp_url_um
var emws_url
var ews_partner_url
var ews_url
var external
var internal
var login_name
var mail_store
var mdb_dn
var network_requirements
var oab_url
var oof_url
var port
var public_folder_server
var referral_port
var server
var server_dn
var server_version
var sharing_url
var smtp_last
var spa
var ssl
var ttl
var um_url
var use_pop_path
var version

Inherited members

class Response (**kwargs)
Expand source code
class Response(AutodiscoverBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/response-pox"""

    ELEMENT_NAME = 'Response'

    user = EWSElementField(value_cls=User)
    account = EWSElementField(value_cls=Account)

    @property
    def redirect_address(self):
        try:
            if self.account.action != Account.REDIRECT_ADDR:
                return None
            return self.account.redirect_address
        except AttributeError:
            return None

    @property
    def redirect_url(self):
        try:
            if self.account.action != Account.REDIRECT_URL:
                return None
            return self.account.redirect_url
        except AttributeError:
            return None

    @property
    def autodiscover_smtp_address(self):
        # AutoDiscoverSMTPAddress might not be present in the XML. In this case, use the original email address.
        try:
            if self.account.action != Account.SETTINGS:
                return None
            return self.user.autodiscover_smtp_address
        except AttributeError:
            return None

    @property
    def ews_url(self):
        """Return the EWS URL contained in the response.

        A response may contain a number of possible protocol types. EXPR is meant for EWS. See
        https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16

        We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available.

        Additionally, some responses may contain and EXPR with no EWS URL. In that case, return the URL from EXCH, if
        available.
        """
        protocols = {p.type: p for p in self.account.protocols if p.ews_url}
        if Protocol.EXPR in protocols:
            return protocols[Protocol.EXPR].ews_url
        if Protocol.EXCH in protocols:
            return protocols[Protocol.EXCH].ews_url
        raise ValueError(
            'No EWS URL found in any of the available protocols: %s' % [str(p) for p in self.account.protocols]
        )

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var account
var autodiscover_smtp_address
Expand source code
@property
def autodiscover_smtp_address(self):
    # AutoDiscoverSMTPAddress might not be present in the XML. In this case, use the original email address.
    try:
        if self.account.action != Account.SETTINGS:
            return None
        return self.user.autodiscover_smtp_address
    except AttributeError:
        return None
var ews_url

Return the EWS URL contained in the response.

A response may contain a number of possible protocol types. EXPR is meant for EWS. See https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16

We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available.

Additionally, some responses may contain and EXPR with no EWS URL. In that case, return the URL from EXCH, if available.

Expand source code
@property
def ews_url(self):
    """Return the EWS URL contained in the response.

    A response may contain a number of possible protocol types. EXPR is meant for EWS. See
    https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16

    We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available.

    Additionally, some responses may contain and EXPR with no EWS URL. In that case, return the URL from EXCH, if
    available.
    """
    protocols = {p.type: p for p in self.account.protocols if p.ews_url}
    if Protocol.EXPR in protocols:
        return protocols[Protocol.EXPR].ews_url
    if Protocol.EXCH in protocols:
        return protocols[Protocol.EXCH].ews_url
    raise ValueError(
        'No EWS URL found in any of the available protocols: %s' % [str(p) for p in self.account.protocols]
    )
var redirect_address
Expand source code
@property
def redirect_address(self):
    try:
        if self.account.action != Account.REDIRECT_ADDR:
            return None
        return self.account.redirect_address
    except AttributeError:
        return None
var redirect_url
Expand source code
@property
def redirect_url(self):
    try:
        if self.account.action != Account.REDIRECT_URL:
            return None
        return self.account.redirect_url
    except AttributeError:
        return None
var user

Inherited members

class SimpleProtocol (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox

Used for the 'Internal' and 'External' elements that may contain a stripped-down version of the Protocol element.

Expand source code
class SimpleProtocol(AutodiscoverBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox

    Used for the 'Internal' and 'External' elements that may contain a stripped-down version of the Protocol element.
    """

    ELEMENT_NAME = 'Protocol'
    WEB = 'WEB'
    EXCH = 'EXCH'
    EXPR = 'EXPR'
    EXHTTP = 'EXHTTP'
    TYPES = (WEB, EXCH, EXPR, EXHTTP)

    type = ChoiceField(field_uri='Type', choices={Choice(c) for c in TYPES}, namespace=RNS)
    as_url = TextField(field_uri='ASUrl', namespace=RNS)

Ancestors

Subclasses

Class variables

var ELEMENT_NAME
var EXCH
var EXHTTP
var EXPR
var FIELDS
var TYPES
var WEB

Instance variables

var as_url
var type

Inherited members

class User (**kwargs)
Expand source code
class User(AutodiscoverBase):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/user-pox"""

    ELEMENT_NAME = 'User'

    display_name = TextField(field_uri='DisplayName', namespace=RNS)
    legacy_dn = TextField(field_uri='LegacyDN', namespace=RNS)
    deployment_id = TextField(field_uri='DeploymentId', namespace=RNS)  # GUID format
    autodiscover_smtp_address = EmailAddressField(field_uri='AutoDiscoverSMTPAddress', namespace=RNS)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var autodiscover_smtp_address
var deployment_id
var display_name
var legacy_dn

Inherited members

exchangelib-4.6.1/docs/exchangelib/autodiscover/protocol.html000066400000000000000000000217451414601472700244510ustar00rootroot00000000000000 exchangelib.autodiscover.protocol API documentation

Module exchangelib.autodiscover.protocol

Expand source code
from ..protocol import BaseProtocol


class AutodiscoverProtocol(BaseProtocol):
    """Protocol which implements the bare essentials for autodiscover."""

    TIMEOUT = 10  # Seconds

    def __str__(self):
        return '''\
Autodiscover endpoint: %s
Auth type: %s''' % (
            self.service_endpoint,
            self.auth_type,
        )

Classes

class AutodiscoverProtocol (config)

Protocol which implements the bare essentials for autodiscover.

Expand source code
class AutodiscoverProtocol(BaseProtocol):
    """Protocol which implements the bare essentials for autodiscover."""

    TIMEOUT = 10  # Seconds

    def __str__(self):
        return '''\
Autodiscover endpoint: %s
Auth type: %s''' % (
            self.service_endpoint,
            self.auth_type,
        )

Ancestors

Class variables

var TIMEOUT

Inherited members

exchangelib-4.6.1/docs/exchangelib/configuration.html000066400000000000000000000434001414601472700227400ustar00rootroot00000000000000 exchangelib.configuration API documentation

Module exchangelib.configuration

Expand source code
import logging

from cached_property import threaded_cached_property

from .credentials import BaseCredentials, OAuth2Credentials
from .protocol import RetryPolicy, FailFast
from .transport import AUTH_TYPE_MAP, OAUTH2
from .util import split_url
from .version import Version

log = logging.getLogger(__name__)


class Configuration:
    """Contains information needed to create an authenticated connection to an EWS endpoint.

    The 'credentials' argument contains the credentials needed to authenticate with the server. Multiple credentials
    implementations are available in 'exchangelib.credentials'.

    config = Configuration(credentials=Credentials('john@example.com', 'MY_SECRET'), ...)

    The 'server' and 'service_endpoint' arguments are mutually exclusive. The former must contain only a domain name,
    the latter a full URL:

        config = Configuration(server='example.com', ...)
        config = Configuration(service_endpoint='https://mail.example.com/EWS/Exchange.asmx', ...)

    If you know which authentication type the server uses, you add that as a hint in 'auth_type'. Likewise, you can
    add the server version as a hint. This allows to skip the auth type and version guessing routines:

        config = Configuration(auth_type=NTLM, ...)
        config = Configuration(version=Version(build=Build(15, 1, 2, 3)), ...)

    You can use 'retry_policy' to define a custom retry policy for handling server connection failures:

        config = Configuration(retry_policy=FaultTolerance(max_wait=3600), ...)

    'max_connections' defines the max number of connections allowed for this server. This may be restricted by
    policies on the Exchange server.
    """

    def __init__(self, credentials=None, server=None, service_endpoint=None, auth_type=None, version=None,
                 retry_policy=None, max_connections=None):
        if not isinstance(credentials, (BaseCredentials, type(None))):
            raise ValueError("'credentials' %r must be a Credentials instance" % credentials)
        if isinstance(credentials, OAuth2Credentials) and auth_type is None:
            # This type of credentials *must* use the OAuth auth type
            auth_type = OAUTH2
        if server and service_endpoint:
            raise AttributeError("Only one of 'server' or 'service_endpoint' must be provided")
        if auth_type is not None and auth_type not in AUTH_TYPE_MAP:
            raise ValueError("'auth_type' %r must be one of %s"
                             % (auth_type, ', '.join("'%s'" % k for k in sorted(AUTH_TYPE_MAP))))
        if not retry_policy:
            retry_policy = FailFast()
        if not isinstance(version, (Version, type(None))):
            raise ValueError("'version' %r must be a Version instance" % version)
        if not isinstance(retry_policy, RetryPolicy):
            raise ValueError("'retry_policy' %r must be a RetryPolicy instance" % retry_policy)
        if not isinstance(max_connections, (int, type(None))):
            raise ValueError("'max_connections' must be an integer")
        self._credentials = credentials
        if server:
            self.service_endpoint = 'https://%s/EWS/Exchange.asmx' % server
        else:
            self.service_endpoint = service_endpoint
        self.auth_type = auth_type
        self.version = version
        self.retry_policy = retry_policy
        self.max_connections = max_connections

    @property
    def credentials(self):
        # Do not update credentials from this class. Instead, do it from Protocol
        return self._credentials

    @threaded_cached_property
    def server(self):
        if not self.service_endpoint:
            return None
        return split_url(self.service_endpoint)[1]

    def __repr__(self):
        return self.__class__.__name__ + '(%s)' % ', '.join('%s=%r' % (k, getattr(self, k)) for k in (
            'credentials', 'service_endpoint', 'auth_type', 'version', 'retry_policy'
        ))

Classes

class Configuration (credentials=None, server=None, service_endpoint=None, auth_type=None, version=None, retry_policy=None, max_connections=None)

Contains information needed to create an authenticated connection to an EWS endpoint.

The 'credentials' argument contains the credentials needed to authenticate with the server. Multiple credentials implementations are available in 'exchangelib.credentials'.

config = Configuration(credentials=Credentials('john@example.com', 'MY_SECRET'), …)

The 'server' and 'service_endpoint' arguments are mutually exclusive. The former must contain only a domain name, the latter a full URL:

config = Configuration(server='example.com', ...)
config = Configuration(service_endpoint='https://mail.example.com/EWS/Exchange.asmx', ...)

If you know which authentication type the server uses, you add that as a hint in 'auth_type'. Likewise, you can add the server version as a hint. This allows to skip the auth type and version guessing routines:

config = Configuration(auth_type=NTLM, ...)
config = Configuration(version=Version(build=Build(15, 1, 2, 3)), ...)

You can use 'retry_policy' to define a custom retry policy for handling server connection failures:

config = Configuration(retry_policy=FaultTolerance(max_wait=3600), ...)

'max_connections' defines the max number of connections allowed for this server. This may be restricted by policies on the Exchange server.

Expand source code
class Configuration:
    """Contains information needed to create an authenticated connection to an EWS endpoint.

    The 'credentials' argument contains the credentials needed to authenticate with the server. Multiple credentials
    implementations are available in 'exchangelib.credentials'.

    config = Configuration(credentials=Credentials('john@example.com', 'MY_SECRET'), ...)

    The 'server' and 'service_endpoint' arguments are mutually exclusive. The former must contain only a domain name,
    the latter a full URL:

        config = Configuration(server='example.com', ...)
        config = Configuration(service_endpoint='https://mail.example.com/EWS/Exchange.asmx', ...)

    If you know which authentication type the server uses, you add that as a hint in 'auth_type'. Likewise, you can
    add the server version as a hint. This allows to skip the auth type and version guessing routines:

        config = Configuration(auth_type=NTLM, ...)
        config = Configuration(version=Version(build=Build(15, 1, 2, 3)), ...)

    You can use 'retry_policy' to define a custom retry policy for handling server connection failures:

        config = Configuration(retry_policy=FaultTolerance(max_wait=3600), ...)

    'max_connections' defines the max number of connections allowed for this server. This may be restricted by
    policies on the Exchange server.
    """

    def __init__(self, credentials=None, server=None, service_endpoint=None, auth_type=None, version=None,
                 retry_policy=None, max_connections=None):
        if not isinstance(credentials, (BaseCredentials, type(None))):
            raise ValueError("'credentials' %r must be a Credentials instance" % credentials)
        if isinstance(credentials, OAuth2Credentials) and auth_type is None:
            # This type of credentials *must* use the OAuth auth type
            auth_type = OAUTH2
        if server and service_endpoint:
            raise AttributeError("Only one of 'server' or 'service_endpoint' must be provided")
        if auth_type is not None and auth_type not in AUTH_TYPE_MAP:
            raise ValueError("'auth_type' %r must be one of %s"
                             % (auth_type, ', '.join("'%s'" % k for k in sorted(AUTH_TYPE_MAP))))
        if not retry_policy:
            retry_policy = FailFast()
        if not isinstance(version, (Version, type(None))):
            raise ValueError("'version' %r must be a Version instance" % version)
        if not isinstance(retry_policy, RetryPolicy):
            raise ValueError("'retry_policy' %r must be a RetryPolicy instance" % retry_policy)
        if not isinstance(max_connections, (int, type(None))):
            raise ValueError("'max_connections' must be an integer")
        self._credentials = credentials
        if server:
            self.service_endpoint = 'https://%s/EWS/Exchange.asmx' % server
        else:
            self.service_endpoint = service_endpoint
        self.auth_type = auth_type
        self.version = version
        self.retry_policy = retry_policy
        self.max_connections = max_connections

    @property
    def credentials(self):
        # Do not update credentials from this class. Instead, do it from Protocol
        return self._credentials

    @threaded_cached_property
    def server(self):
        if not self.service_endpoint:
            return None
        return split_url(self.service_endpoint)[1]

    def __repr__(self):
        return self.__class__.__name__ + '(%s)' % ', '.join('%s=%r' % (k, getattr(self, k)) for k in (
            'credentials', 'service_endpoint', 'auth_type', 'version', 'retry_policy'
        ))

Instance variables

var credentials
Expand source code
@property
def credentials(self):
    # Do not update credentials from this class. Instead, do it from Protocol
    return self._credentials
var server
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))
exchangelib-4.6.1/docs/exchangelib/credentials.html000066400000000000000000001140751414601472700223750ustar00rootroot00000000000000 exchangelib.credentials API documentation

Module exchangelib.credentials

Implements an Exchange user object and access types. Exchange provides two different ways of granting access for a login to a specific account. Impersonation is used mainly for service accounts that connect via EWS. Delegate is used for ad-hoc access e.g. granted manually by the user. See http://blogs.msdn.com/b/exchangedev/archive/2009/06/15/exchange-impersonation-vs-delegate-access.aspx

Expand source code
"""
Implements an Exchange user object and access types. Exchange provides two different ways of granting access for a
login to a specific account. Impersonation is used mainly for service accounts that connect via EWS. Delegate is used
for ad-hoc access e.g. granted manually by the user.
See http://blogs.msdn.com/b/exchangedev/archive/2009/06/15/exchange-impersonation-vs-delegate-access.aspx
"""
import abc
import logging
from threading import RLock

log = logging.getLogger(__name__)

IMPERSONATION = 'impersonation'
DELEGATE = 'delegate'
ACCESS_TYPES = (IMPERSONATION, DELEGATE)


class BaseCredentials(metaclass=abc.ABCMeta):
    """Base for credential storage.

    Establishes a method for refreshing credentials (mostly useful with OAuth, which expires tokens relatively
    frequently) and provides a lock for synchronizing access to the object around refreshes.
    """

    def __init__(self):
        self._lock = RLock()

    @property
    def lock(self):
        return self._lock

    @abc.abstractmethod
    def refresh(self, session):
        """Obtain a new set of valid credentials. This is mostly intended to support OAuth token refreshing, which can
        happen in long- running applications or those that cache access tokens and so might start with a token close to
        expiration.

        :param session: requests session asking for refreshed credentials
        :return:
        """

    def _get_hash_values(self):
        return (getattr(self, k) for k in self.__dict__ if k != '_lock')

    def __eq__(self, other):
        for k in self.__dict__:
            if k == '_lock':
                continue
            if getattr(self, k) != getattr(other, k):
                return False
        return True

    def __hash__(self):
        return hash(tuple(self._get_hash_values()))

    def __getstate__(self):
        # The lock cannot be pickled
        state = self.__dict__.copy()
        del state['_lock']
        return state

    def __setstate__(self, state):
        # Restore the lock
        self.__dict__.update(state)
        self._lock = RLock()


class Credentials(BaseCredentials):
    r"""Keeps login info the way Exchange likes it.

    Usernames for authentication are of one of these forms:
    * PrimarySMTPAddress
    * WINDOMAIN\username
    * User Principal Name (UPN)
      password: Clear-text password
    """

    EMAIL = 'email'
    DOMAIN = 'domain'
    UPN = 'upn'

    def __init__(self, username, password):
        super().__init__()
        if username.count('@') == 1:
            self.type = self.EMAIL
        elif username.count('\\') == 1:
            self.type = self.DOMAIN
        else:
            self.type = self.UPN
        self.username = username
        self.password = password

    def refresh(self, session):
        pass

    def __repr__(self):
        return self.__class__.__name__ + repr((self.username, '********'))

    def __str__(self):
        return self.username


class OAuth2Credentials(BaseCredentials):
    """Login info for OAuth 2.0 client credentials authentication, as well as a base for other OAuth 2.0 grant types.

    This is primarily useful for in-house applications accessing data from a single Microsoft account. For applications
    that will access multiple tenants' data, the client credentials flow does not give the application enough
    information to restrict end users' access to the appropriate account. Use OAuth2AuthorizationCodeCredentials and
    the associated auth code grant type for multi-tenant applications.
    """

    def __init__(self, client_id, client_secret, tenant_id=None, identity=None):
        """

        :param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing
        :param client_secret: Secret associated with the OAuth application
        :param tenant_id: Microsoft tenant ID of the account to access
        :param identity: An Identity object representing the account that these credentials are connected to.
        """
        super().__init__()
        self.client_id = client_id
        self.client_secret = client_secret
        self.tenant_id = tenant_id
        self.identity = identity
        # When set, access_token is a dict (or an oauthlib.oauth2.OAuth2Token, which is also a dict)
        self.access_token = None

    def refresh(self, session):
        # Creating a new session gets a new access token, so there's no work here to refresh the credentials. This
        # implementation just makes sure we don't raise a NotImplementedError.
        pass

    def on_token_auto_refreshed(self, access_token):
        """Set the access_token. Called after the access token is refreshed (requests-oauthlib can automatically
        refresh tokens if given an OAuth client ID and secret, so this is how our copy of the token stays up-to-date).
        Applications that cache access tokens can override this to store the new token - just remember to call the
        super() method.

        :param access_token: New token obtained by refreshing
        """
        # Ensure we don't update the object in the middle of a new session being created, which could cause a race.
        if not isinstance(access_token, dict):
            raise ValueError("'access_token' must be an OAuth2Token")
        with self.lock:
            log.debug('%s auth token for %s', 'Refreshing' if self.access_token else 'Setting', self.client_id)
            self.access_token = access_token

    def _get_hash_values(self):
        # 'access_token' may be refreshed once in a while. This should not affect the hash signature.
        # 'identity' is just informational and should also not affect the hash signature.
        return (getattr(self, k) for k in self.__dict__ if k not in ('_lock', 'identity', 'access_token'))

    def sig(self):
        # Like hash(self), but pulls in the access token. Protocol.refresh_credentials() uses this to find out
        # if the access_token needs to be refreshed.
        res = []
        for k in self.__dict__:
            if k in ('_lock', 'identity'):
                continue
            if k == 'access_token':
                res.append(self.access_token['access_token'] if self.access_token else None)
                continue
            res.append(getattr(self, k))
        return hash(tuple(res))

    def __repr__(self):
        return self.__class__.__name__ + repr((self.client_id, '********'))

    def __str__(self):
        return self.client_id


class OAuth2AuthorizationCodeCredentials(OAuth2Credentials):
    """Login info for OAuth 2.0 authentication using the authorization code grant type. This can be used in one of
    several ways:
    * Given an authorization code, client ID, and client secret, fetch a token ourselves and refresh it as needed if
      supplied with a refresh token.
    * Given an existing access token, refresh token, client ID, and client secret, use the access token until it
      expires and then refresh it as needed.
    * Given only an existing access token, use it until it expires. This can be used to let the calling application
      refresh tokens itself by subclassing and implementing refresh().

    Unlike the base (client credentials) grant, authorization code credentials don't require a Microsoft tenant ID
    because each access token (and the authorization code used to get the access token) is restricted to a single
    tenant.
    """

    def __init__(self, authorization_code=None, access_token=None, **kwargs):
        """

        :param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing
        :param client_secret: Secret associated with the OAuth application
        :param tenant_id: Microsoft tenant ID of the account to access
        :param identity: An Identity object representing the account that these credentials are connected to.
        :param authorization_code: Code obtained when authorizing the application to access an account. In combination
          with client_id and client_secret, will be used to obtain an access token.
        :param access_token: Previously-obtained access token. If a token exists and the application will handle
          refreshing by itself (or opts not to handle it), this parameter alone is sufficient.
        """
        super().__init__(**kwargs)
        self.authorization_code = authorization_code
        if access_token is not None and not isinstance(access_token, dict):
            raise ValueError("'access_token' must be an OAuth2Token")
        self.access_token = access_token

    def __repr__(self):
        return self.__class__.__name__ + repr(
            (self.client_id, '[client_secret]', '[authorization_code]', '[access_token]')
        )

    def __str__(self):
        client_id = self.client_id
        credential = '[access_token]' if self.access_token is not None else \
            ('[authorization_code]' if self.authorization_code is not None else None)
        description = ' '.join(filter(None, [client_id, credential]))
        return description or '[underspecified credentials]'

Classes

class BaseCredentials

Base for credential storage.

Establishes a method for refreshing credentials (mostly useful with OAuth, which expires tokens relatively frequently) and provides a lock for synchronizing access to the object around refreshes.

Expand source code
class BaseCredentials(metaclass=abc.ABCMeta):
    """Base for credential storage.

    Establishes a method for refreshing credentials (mostly useful with OAuth, which expires tokens relatively
    frequently) and provides a lock for synchronizing access to the object around refreshes.
    """

    def __init__(self):
        self._lock = RLock()

    @property
    def lock(self):
        return self._lock

    @abc.abstractmethod
    def refresh(self, session):
        """Obtain a new set of valid credentials. This is mostly intended to support OAuth token refreshing, which can
        happen in long- running applications or those that cache access tokens and so might start with a token close to
        expiration.

        :param session: requests session asking for refreshed credentials
        :return:
        """

    def _get_hash_values(self):
        return (getattr(self, k) for k in self.__dict__ if k != '_lock')

    def __eq__(self, other):
        for k in self.__dict__:
            if k == '_lock':
                continue
            if getattr(self, k) != getattr(other, k):
                return False
        return True

    def __hash__(self):
        return hash(tuple(self._get_hash_values()))

    def __getstate__(self):
        # The lock cannot be pickled
        state = self.__dict__.copy()
        del state['_lock']
        return state

    def __setstate__(self, state):
        # Restore the lock
        self.__dict__.update(state)
        self._lock = RLock()

Subclasses

Instance variables

var lock
Expand source code
@property
def lock(self):
    return self._lock

Methods

def refresh(self, session)

Obtain a new set of valid credentials. This is mostly intended to support OAuth token refreshing, which can happen in long- running applications or those that cache access tokens and so might start with a token close to expiration.

:param session: requests session asking for refreshed credentials :return:

Expand source code
@abc.abstractmethod
def refresh(self, session):
    """Obtain a new set of valid credentials. This is mostly intended to support OAuth token refreshing, which can
    happen in long- running applications or those that cache access tokens and so might start with a token close to
    expiration.

    :param session: requests session asking for refreshed credentials
    :return:
    """
class Credentials (username, password)

Keeps login info the way Exchange likes it.

Usernames for authentication are of one of these forms: * PrimarySMTPAddress * WINDOMAIN\username * User Principal Name (UPN) password: Clear-text password

Expand source code
class Credentials(BaseCredentials):
    r"""Keeps login info the way Exchange likes it.

    Usernames for authentication are of one of these forms:
    * PrimarySMTPAddress
    * WINDOMAIN\username
    * User Principal Name (UPN)
      password: Clear-text password
    """

    EMAIL = 'email'
    DOMAIN = 'domain'
    UPN = 'upn'

    def __init__(self, username, password):
        super().__init__()
        if username.count('@') == 1:
            self.type = self.EMAIL
        elif username.count('\\') == 1:
            self.type = self.DOMAIN
        else:
            self.type = self.UPN
        self.username = username
        self.password = password

    def refresh(self, session):
        pass

    def __repr__(self):
        return self.__class__.__name__ + repr((self.username, '********'))

    def __str__(self):
        return self.username

Ancestors

Class variables

var DOMAIN
var EMAIL
var UPN

Inherited members

class OAuth2AuthorizationCodeCredentials (authorization_code=None, access_token=None, **kwargs)

Login info for OAuth 2.0 authentication using the authorization code grant type. This can be used in one of several ways: * Given an authorization code, client ID, and client secret, fetch a token ourselves and refresh it as needed if supplied with a refresh token. * Given an existing access token, refresh token, client ID, and client secret, use the access token until it expires and then refresh it as needed. * Given only an existing access token, use it until it expires. This can be used to let the calling application refresh tokens itself by subclassing and implementing refresh().

Unlike the base (client credentials) grant, authorization code credentials don't require a Microsoft tenant ID because each access token (and the authorization code used to get the access token) is restricted to a single tenant.

:param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing :param client_secret: Secret associated with the OAuth application :param tenant_id: Microsoft tenant ID of the account to access :param identity: An Identity object representing the account that these credentials are connected to. :param authorization_code: Code obtained when authorizing the application to access an account. In combination with client_id and client_secret, will be used to obtain an access token. :param access_token: Previously-obtained access token. If a token exists and the application will handle refreshing by itself (or opts not to handle it), this parameter alone is sufficient.

Expand source code
class OAuth2AuthorizationCodeCredentials(OAuth2Credentials):
    """Login info for OAuth 2.0 authentication using the authorization code grant type. This can be used in one of
    several ways:
    * Given an authorization code, client ID, and client secret, fetch a token ourselves and refresh it as needed if
      supplied with a refresh token.
    * Given an existing access token, refresh token, client ID, and client secret, use the access token until it
      expires and then refresh it as needed.
    * Given only an existing access token, use it until it expires. This can be used to let the calling application
      refresh tokens itself by subclassing and implementing refresh().

    Unlike the base (client credentials) grant, authorization code credentials don't require a Microsoft tenant ID
    because each access token (and the authorization code used to get the access token) is restricted to a single
    tenant.
    """

    def __init__(self, authorization_code=None, access_token=None, **kwargs):
        """

        :param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing
        :param client_secret: Secret associated with the OAuth application
        :param tenant_id: Microsoft tenant ID of the account to access
        :param identity: An Identity object representing the account that these credentials are connected to.
        :param authorization_code: Code obtained when authorizing the application to access an account. In combination
          with client_id and client_secret, will be used to obtain an access token.
        :param access_token: Previously-obtained access token. If a token exists and the application will handle
          refreshing by itself (or opts not to handle it), this parameter alone is sufficient.
        """
        super().__init__(**kwargs)
        self.authorization_code = authorization_code
        if access_token is not None and not isinstance(access_token, dict):
            raise ValueError("'access_token' must be an OAuth2Token")
        self.access_token = access_token

    def __repr__(self):
        return self.__class__.__name__ + repr(
            (self.client_id, '[client_secret]', '[authorization_code]', '[access_token]')
        )

    def __str__(self):
        client_id = self.client_id
        credential = '[access_token]' if self.access_token is not None else \
            ('[authorization_code]' if self.authorization_code is not None else None)
        description = ' '.join(filter(None, [client_id, credential]))
        return description or '[underspecified credentials]'

Ancestors

Inherited members

class OAuth2Credentials (client_id, client_secret, tenant_id=None, identity=None)

Login info for OAuth 2.0 client credentials authentication, as well as a base for other OAuth 2.0 grant types.

This is primarily useful for in-house applications accessing data from a single Microsoft account. For applications that will access multiple tenants' data, the client credentials flow does not give the application enough information to restrict end users' access to the appropriate account. Use OAuth2AuthorizationCodeCredentials and the associated auth code grant type for multi-tenant applications.

:param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing :param client_secret: Secret associated with the OAuth application :param tenant_id: Microsoft tenant ID of the account to access :param identity: An Identity object representing the account that these credentials are connected to.

Expand source code
class OAuth2Credentials(BaseCredentials):
    """Login info for OAuth 2.0 client credentials authentication, as well as a base for other OAuth 2.0 grant types.

    This is primarily useful for in-house applications accessing data from a single Microsoft account. For applications
    that will access multiple tenants' data, the client credentials flow does not give the application enough
    information to restrict end users' access to the appropriate account. Use OAuth2AuthorizationCodeCredentials and
    the associated auth code grant type for multi-tenant applications.
    """

    def __init__(self, client_id, client_secret, tenant_id=None, identity=None):
        """

        :param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing
        :param client_secret: Secret associated with the OAuth application
        :param tenant_id: Microsoft tenant ID of the account to access
        :param identity: An Identity object representing the account that these credentials are connected to.
        """
        super().__init__()
        self.client_id = client_id
        self.client_secret = client_secret
        self.tenant_id = tenant_id
        self.identity = identity
        # When set, access_token is a dict (or an oauthlib.oauth2.OAuth2Token, which is also a dict)
        self.access_token = None

    def refresh(self, session):
        # Creating a new session gets a new access token, so there's no work here to refresh the credentials. This
        # implementation just makes sure we don't raise a NotImplementedError.
        pass

    def on_token_auto_refreshed(self, access_token):
        """Set the access_token. Called after the access token is refreshed (requests-oauthlib can automatically
        refresh tokens if given an OAuth client ID and secret, so this is how our copy of the token stays up-to-date).
        Applications that cache access tokens can override this to store the new token - just remember to call the
        super() method.

        :param access_token: New token obtained by refreshing
        """
        # Ensure we don't update the object in the middle of a new session being created, which could cause a race.
        if not isinstance(access_token, dict):
            raise ValueError("'access_token' must be an OAuth2Token")
        with self.lock:
            log.debug('%s auth token for %s', 'Refreshing' if self.access_token else 'Setting', self.client_id)
            self.access_token = access_token

    def _get_hash_values(self):
        # 'access_token' may be refreshed once in a while. This should not affect the hash signature.
        # 'identity' is just informational and should also not affect the hash signature.
        return (getattr(self, k) for k in self.__dict__ if k not in ('_lock', 'identity', 'access_token'))

    def sig(self):
        # Like hash(self), but pulls in the access token. Protocol.refresh_credentials() uses this to find out
        # if the access_token needs to be refreshed.
        res = []
        for k in self.__dict__:
            if k in ('_lock', 'identity'):
                continue
            if k == 'access_token':
                res.append(self.access_token['access_token'] if self.access_token else None)
                continue
            res.append(getattr(self, k))
        return hash(tuple(res))

    def __repr__(self):
        return self.__class__.__name__ + repr((self.client_id, '********'))

    def __str__(self):
        return self.client_id

Ancestors

Subclasses

Methods

def on_token_auto_refreshed(self, access_token)

Set the access_token. Called after the access token is refreshed (requests-oauthlib can automatically refresh tokens if given an OAuth client ID and secret, so this is how our copy of the token stays up-to-date). Applications that cache access tokens can override this to store the new token - just remember to call the super() method.

:param access_token: New token obtained by refreshing

Expand source code
def on_token_auto_refreshed(self, access_token):
    """Set the access_token. Called after the access token is refreshed (requests-oauthlib can automatically
    refresh tokens if given an OAuth client ID and secret, so this is how our copy of the token stays up-to-date).
    Applications that cache access tokens can override this to store the new token - just remember to call the
    super() method.

    :param access_token: New token obtained by refreshing
    """
    # Ensure we don't update the object in the middle of a new session being created, which could cause a race.
    if not isinstance(access_token, dict):
        raise ValueError("'access_token' must be an OAuth2Token")
    with self.lock:
        log.debug('%s auth token for %s', 'Refreshing' if self.access_token else 'Setting', self.client_id)
        self.access_token = access_token
def sig(self)
Expand source code
def sig(self):
    # Like hash(self), but pulls in the access token. Protocol.refresh_credentials() uses this to find out
    # if the access_token needs to be refreshed.
    res = []
    for k in self.__dict__:
        if k in ('_lock', 'identity'):
            continue
        if k == 'access_token':
            res.append(self.access_token['access_token'] if self.access_token else None)
            continue
        res.append(getattr(self, k))
    return hash(tuple(res))

Inherited members

exchangelib-4.6.1/docs/exchangelib/errors.html000066400000000000000000020633361414601472700214210ustar00rootroot00000000000000 exchangelib.errors API documentation

Module exchangelib.errors

Stores errors specific to this package, and mirrors all the possible errors that EWS can return.

Expand source code
# flake8: noqa
"""Stores errors specific to this package, and mirrors all the possible errors that EWS can return."""
from urllib.parse import urlparse


class MultipleObjectsReturned(Exception):
    pass


class DoesNotExist(Exception):
    pass


class EWSError(Exception):
    """Global error type within this module."""

    def __init__(self, value):
        super().__init__(value)
        self.value = value

    def __str__(self):
        return str(self.value)

    def __eq__(self, other):
        return repr(self) == repr(other)


# Warnings
class EWSWarning(EWSError):
    pass


# Misc errors
class TransportError(EWSError):
    pass


class RateLimitError(TransportError):
    def __init__(self, value, url, status_code, total_wait):
        super().__init__(value)
        self.url = url
        self.status_code = status_code
        self.total_wait = total_wait

    def __str__(self):
        return str(
            '{value} (gave up after {total_wait:.3f} seconds. URL {url} returned status code {status_code})'.format(
                value=self.value, url=self.url, status_code=self.status_code, total_wait=self.total_wait)
        )


class SOAPError(TransportError):
    pass


class MalformedResponseError(TransportError):
    pass


class UnauthorizedError(EWSError):
    pass


class RedirectError(TransportError):
    def __init__(self, url):
        parsed_url = urlparse(url)
        self.url = url
        self.server = parsed_url.hostname.lower()
        self.has_ssl = parsed_url.scheme == 'https'
        super().__init__(str(self))

    def __str__(self):
        return 'We were redirected to %s' % self.url


class RelativeRedirect(TransportError):
    pass


class AutoDiscoverError(TransportError):
    pass


class AutoDiscoverFailed(AutoDiscoverError):
    pass


class AutoDiscoverCircularRedirect(AutoDiscoverError):
    pass


class AutoDiscoverRedirect(AutoDiscoverError):
    def __init__(self, redirect_email):
        self.redirect_email = redirect_email
        super().__init__(str(self))

    def __str__(self):
        return 'AutoDiscover redirects to %s' % self.redirect_email


class NaiveDateTimeNotAllowed(ValueError):
    def __init__(self, local_dt):
        super().__init__()
        from .ewsdatetime import EWSDateTime
        if not isinstance(local_dt, EWSDateTime):
            raise ValueError("'local_dt' value %r must be an EWSDateTime" % local_dt)
        self.local_dt = local_dt


class UnknownTimeZone(EWSError):
    pass


class TimezoneDefinitionInvalidForYear(EWSError):
    pass


class SessionPoolMinSizeReached(EWSError):
    pass


class SessionPoolMaxSizeReached(EWSError):
    pass


class ResponseMessageError(TransportError):
    pass


class CASError(EWSError):
    """EWS will sometimes return an error message in an 'X-CasErrorCode' custom HTTP header in an HTTP 500 error code.
    This exception is for those cases. The caller may want to do something with the original response, so store that.
    """

    def __init__(self, cas_error, response):
        self.cas_error = cas_error
        self.response = response
        super().__init__(str(self))

    def __str__(self):
        return 'CAS error: %s' % self.cas_error


# Somewhat-authoritative list of possible response message error types from EWS. See full list at
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/responsecode
#
class ErrorAccessDenied(ResponseMessageError): pass
class ErrorAccessModeSpecified(ResponseMessageError): pass
class ErrorAccountDisabled(ResponseMessageError): pass
class ErrorAddDelegatesFailed(ResponseMessageError): pass
class ErrorAddressSpaceNotFound(ResponseMessageError): pass
class ErrorADOperation(ResponseMessageError): pass
class ErrorADSessionFilter(ResponseMessageError): pass
class ErrorADUnavailable(ResponseMessageError): pass
class ErrorAffectedTaskOccurrencesRequired(ResponseMessageError): pass
class ErrorApplyConversationActionFailed(ResponseMessageError): pass
class ErrorAttachmentSizeLimitExceeded(ResponseMessageError): pass
class ErrorAutoDiscoverFailed(ResponseMessageError): pass
class ErrorAvailabilityConfigNotFound(ResponseMessageError): pass
class ErrorBatchProcessingStopped(ResponseMessageError): pass
class ErrorCalendarCannotMoveOrCopyOccurrence(ResponseMessageError): pass
class ErrorCalendarCannotUpdateDeletedItem(ResponseMessageError): pass
class ErrorCalendarCannotUseIdForOccurrenceId(ResponseMessageError): pass
class ErrorCalendarCannotUseIdForRecurringMasterId(ResponseMessageError): pass
class ErrorCalendarDurationIsTooLong(ResponseMessageError): pass
class ErrorCalendarEndDateIsEarlierThanStartDate(ResponseMessageError): pass
class ErrorCalendarFolderIsInvalidForCalendarView(ResponseMessageError): pass
class ErrorCalendarInvalidAttributeValue(ResponseMessageError): pass
class ErrorCalendarInvalidDayForTimeChangePattern(ResponseMessageError): pass
class ErrorCalendarInvalidDayForWeeklyRecurrence(ResponseMessageError): pass
class ErrorCalendarInvalidPropertyState(ResponseMessageError): pass
class ErrorCalendarInvalidPropertyValue(ResponseMessageError): pass
class ErrorCalendarInvalidRecurrence(ResponseMessageError): pass
class ErrorCalendarInvalidTimeZone(ResponseMessageError): pass
class ErrorCalendarIsCancelledForAccept(ResponseMessageError): pass
class ErrorCalendarIsCancelledForDecline(ResponseMessageError): pass
class ErrorCalendarIsCancelledForRemove(ResponseMessageError): pass
class ErrorCalendarIsCancelledForTentative(ResponseMessageError): pass
class ErrorCalendarIsDelegatedForAccept(ResponseMessageError): pass
class ErrorCalendarIsDelegatedForDecline(ResponseMessageError): pass
class ErrorCalendarIsDelegatedForRemove(ResponseMessageError): pass
class ErrorCalendarIsDelegatedForTentative(ResponseMessageError): pass
class ErrorCalendarIsNotOrganizer(ResponseMessageError): pass
class ErrorCalendarIsOrganizerForAccept(ResponseMessageError): pass
class ErrorCalendarIsOrganizerForDecline(ResponseMessageError): pass
class ErrorCalendarIsOrganizerForRemove(ResponseMessageError): pass
class ErrorCalendarIsOrganizerForTentative(ResponseMessageError): pass
class ErrorCalendarMeetingRequestIsOutOfDate(ResponseMessageError): pass
class ErrorCalendarOccurrenceIndexIsOutOfRecurrenceRange(ResponseMessageError): pass
class ErrorCalendarOccurrenceIsDeletedFromRecurrence(ResponseMessageError): pass
class ErrorCalendarOutOfRange(ResponseMessageError): pass
class ErrorCalendarViewRangeTooBig(ResponseMessageError): pass
class ErrorCallerIsInvalidADAccount(ResponseMessageError): pass
class ErrorCannotCreateCalendarItemInNonCalendarFolder(ResponseMessageError): pass
class ErrorCannotCreateContactInNonContactFolder(ResponseMessageError): pass
class ErrorCannotCreatePostItemInNonMailFolder(ResponseMessageError): pass
class ErrorCannotCreateTaskInNonTaskFolder(ResponseMessageError): pass
class ErrorCannotDeleteObject(ResponseMessageError): pass
class ErrorCannotDeleteTaskOccurrence(ResponseMessageError): pass
class ErrorCannotEmptyFolder(ResponseMessageError): pass
class ErrorCannotOpenFileAttachment(ResponseMessageError): pass
class ErrorCannotSetCalendarPermissionOnNonCalendarFolder(ResponseMessageError): pass
class ErrorCannotSetNonCalendarPermissionOnCalendarFolder(ResponseMessageError): pass
class ErrorCannotSetPermissionUnknownEntries(ResponseMessageError): pass
class ErrorCannotUseFolderIdForItemId(ResponseMessageError): pass
class ErrorCannotUseItemIdForFolderId(ResponseMessageError): pass
class ErrorChangeKeyRequired(ResponseMessageError): pass
class ErrorChangeKeyRequiredForWriteOperations(ResponseMessageError): pass
class ErrorClientDisconnected(ResponseMessageError): pass
class ErrorConnectionFailed(ResponseMessageError): pass
class ErrorConnectionFailedTransientError(ResponseMessageError): pass
class ErrorContainsFilterWrongType(ResponseMessageError): pass
class ErrorContentConversionFailed(ResponseMessageError): pass
class ErrorCorruptData(ResponseMessageError): pass
class ErrorCreateItemAccessDenied(ResponseMessageError): pass
class ErrorCreateManagedFolderPartialCompletion(ResponseMessageError): pass
class ErrorCreateSubfolderAccessDenied(ResponseMessageError): pass
class ErrorCrossMailboxMoveCopy(ResponseMessageError): pass
class ErrorCrossSiteRequest(ResponseMessageError): pass
class ErrorDataSizeLimitExceeded(ResponseMessageError): pass
class ErrorDataSourceOperation(ResponseMessageError): pass
class ErrorDelegateAlreadyExists(ResponseMessageError): pass
class ErrorDelegateCannotAddOwner(ResponseMessageError): pass
class ErrorDelegateMissingConfiguration(ResponseMessageError): pass
class ErrorDelegateNoUser(ResponseMessageError): pass
class ErrorDelegateValidationFailed(ResponseMessageError): pass
class ErrorDeleteDistinguishedFolder(ResponseMessageError): pass
class ErrorDeleteItemsFailed(ResponseMessageError): pass
class ErrorDistinguishedUserNotSupported(ResponseMessageError): pass
class ErrorDistributionListMemberNotExist(ResponseMessageError): pass
class ErrorDuplicateInputFolderNames(ResponseMessageError): pass
class ErrorDuplicateSOAPHeader(ResponseMessageError): pass
class ErrorDuplicateUserIdsSpecified(ResponseMessageError): pass
class ErrorEmailAddressMismatch(ResponseMessageError): pass
class ErrorEventNotFound(ResponseMessageError): pass
class ErrorExceededConnectionCount(ResponseMessageError): pass
class ErrorExceededFindCountLimit(ResponseMessageError): pass
class ErrorExceededSubscriptionCount(ResponseMessageError): pass
class ErrorExpiredSubscription(ResponseMessageError): pass
class ErrorFolderCorrupt(ResponseMessageError): pass
class ErrorFolderExists(ResponseMessageError): pass
class ErrorFolderNotFound(ResponseMessageError): pass
class ErrorFolderPropertyRequestFailed(ResponseMessageError): pass
class ErrorFolderSave(ResponseMessageError): pass
class ErrorFolderSaveFailed(ResponseMessageError): pass
class ErrorFolderSavePropertyError(ResponseMessageError): pass
class ErrorFreeBusyDLLimitReached(ResponseMessageError): pass
class ErrorFreeBusyGenerationFailed(ResponseMessageError): pass
class ErrorGetServerSecurityDescriptorFailed(ResponseMessageError): pass
class ErrorImpersonateUserDenied(ResponseMessageError): pass
class ErrorImpersonationDenied(ResponseMessageError): pass
class ErrorImpersonationFailed(ResponseMessageError): pass
class ErrorInboxRulesValidationError(ResponseMessageError): pass
class ErrorIncorrectSchemaVersion(ResponseMessageError): pass
class ErrorIncorrectUpdatePropertyCount(ResponseMessageError): pass
class ErrorIndividualMailboxLimitReached(ResponseMessageError): pass
class ErrorInsufficientResources(ResponseMessageError): pass
class ErrorInternalServerError(ResponseMessageError): pass
class ErrorInternalServerTransientError(ResponseMessageError): pass
class ErrorInvalidAccessLevel(ResponseMessageError): pass
class ErrorInvalidArgument(ResponseMessageError): pass
class ErrorInvalidAttachmentId(ResponseMessageError): pass
class ErrorInvalidAttachmentSubfilter(ResponseMessageError): pass
class ErrorInvalidAttachmentSubfilterTextFilter(ResponseMessageError): pass
class ErrorInvalidAuthorizationContext(ResponseMessageError): pass
class ErrorInvalidChangeKey(ResponseMessageError): pass
class ErrorInvalidClientSecurityContext(ResponseMessageError): pass
class ErrorInvalidCompleteDate(ResponseMessageError): pass
class ErrorInvalidContactEmailAddress(ResponseMessageError): pass
class ErrorInvalidContactEmailIndex(ResponseMessageError): pass
class ErrorInvalidCrossForestCredentials(ResponseMessageError): pass
class ErrorInvalidDelegatePermission(ResponseMessageError): pass
class ErrorInvalidDelegateUserId(ResponseMessageError): pass
class ErrorInvalidExchangeImpersonationHeaderData(ResponseMessageError): pass
class ErrorInvalidExcludesRestriction(ResponseMessageError): pass
class ErrorInvalidExpressionTypeForSubFilter(ResponseMessageError): pass
class ErrorInvalidExtendedProperty(ResponseMessageError): pass
class ErrorInvalidExtendedPropertyValue(ResponseMessageError): pass
class ErrorInvalidExternalSharingInitiator(ResponseMessageError): pass
class ErrorInvalidExternalSharingSubscriber(ResponseMessageError): pass
class ErrorInvalidFederatedOrganizationId(ResponseMessageError): pass
class ErrorInvalidFolderId(ResponseMessageError): pass
class ErrorInvalidFolderTypeForOperation(ResponseMessageError): pass
class ErrorInvalidFractionalPagingParameters(ResponseMessageError): pass
class ErrorInvalidFreeBusyViewType(ResponseMessageError): pass
class ErrorInvalidGetSharingFolderRequest(ResponseMessageError): pass
class ErrorInvalidId(ResponseMessageError): pass
class ErrorInvalidIdEmpty(ResponseMessageError): pass
class ErrorInvalidIdMalformed(ResponseMessageError): pass
class ErrorInvalidIdMalformedEwsLegacyIdFormat(ResponseMessageError): pass
class ErrorInvalidIdMonikerTooLong(ResponseMessageError): pass
class ErrorInvalidIdNotAnItemAttachmentId(ResponseMessageError): pass
class ErrorInvalidIdReturnedByResolveNames(ResponseMessageError): pass
class ErrorInvalidIdStoreObjectIdTooLong(ResponseMessageError): pass
class ErrorInvalidIdTooManyAttachmentLevels(ResponseMessageError): pass
class ErrorInvalidIdXml(ResponseMessageError): pass
class ErrorInvalidIndexedPagingParameters(ResponseMessageError): pass
class ErrorInvalidInternetHeaderChildNodes(ResponseMessageError): pass
class ErrorInvalidItemForOperationAcceptItem(ResponseMessageError): pass
class ErrorInvalidItemForOperationCancelItem(ResponseMessageError): pass
class ErrorInvalidItemForOperationCreateItem(ResponseMessageError): pass
class ErrorInvalidItemForOperationCreateItemAttachment(ResponseMessageError): pass
class ErrorInvalidItemForOperationDeclineItem(ResponseMessageError): pass
class ErrorInvalidItemForOperationExpandDL(ResponseMessageError): pass
class ErrorInvalidItemForOperationRemoveItem(ResponseMessageError): pass
class ErrorInvalidItemForOperationSendItem(ResponseMessageError): pass
class ErrorInvalidItemForOperationTentative(ResponseMessageError): pass
class ErrorInvalidLicense(ResponseMessageError): pass
class ErrorInvalidLogonType(ResponseMessageError): pass
class ErrorInvalidMailbox(ResponseMessageError): pass
class ErrorInvalidManagedFolderProperty(ResponseMessageError): pass
class ErrorInvalidManagedFolderQuota(ResponseMessageError): pass
class ErrorInvalidManagedFolderSize(ResponseMessageError): pass
class ErrorInvalidMergedFreeBusyInterval(ResponseMessageError): pass
class ErrorInvalidNameForNameResolution(ResponseMessageError): pass
class ErrorInvalidNetworkServiceContext(ResponseMessageError): pass
class ErrorInvalidOofParameter(ResponseMessageError): pass
class ErrorInvalidOperation(ResponseMessageError): pass
class ErrorInvalidOrganizationRelationshipForFreeBusy(ResponseMessageError): pass
class ErrorInvalidPagingMaxRows(ResponseMessageError): pass
class ErrorInvalidParentFolder(ResponseMessageError): pass
class ErrorInvalidPercentCompleteValue(ResponseMessageError): pass
class ErrorInvalidPermissionSettings(ResponseMessageError): pass
class ErrorInvalidPhoneCallId(ResponseMessageError): pass
class ErrorInvalidPhoneNumber(ResponseMessageError): pass
class ErrorInvalidPropertyAppend(ResponseMessageError): pass
class ErrorInvalidPropertyDelete(ResponseMessageError): pass
class ErrorInvalidPropertyForExists(ResponseMessageError): pass
class ErrorInvalidPropertyForOperation(ResponseMessageError): pass
class ErrorInvalidPropertyRequest(ResponseMessageError): pass
class ErrorInvalidPropertySet(ResponseMessageError): pass
class ErrorInvalidPropertyUpdateSentMessage(ResponseMessageError): pass
class ErrorInvalidProxySecurityContext(ResponseMessageError): pass
class ErrorInvalidPullSubscriptionId(ResponseMessageError): pass
class ErrorInvalidPushSubscriptionUrl(ResponseMessageError): pass
class ErrorInvalidRecipients(ResponseMessageError): pass
class ErrorInvalidRecipientSubfilter(ResponseMessageError): pass
class ErrorInvalidRecipientSubfilterComparison(ResponseMessageError): pass
class ErrorInvalidRecipientSubfilterOrder(ResponseMessageError): pass
class ErrorInvalidRecipientSubfilterTextFilter(ResponseMessageError): pass
class ErrorInvalidReferenceItem(ResponseMessageError): pass
class ErrorInvalidRequest(ResponseMessageError): pass
class ErrorInvalidRestriction(ResponseMessageError): pass
class ErrorInvalidRoutingType(ResponseMessageError): pass
class ErrorInvalidScheduledOofDuration(ResponseMessageError): pass
class ErrorInvalidSchemaVersionForMailboxVersion(ResponseMessageError): pass
class ErrorInvalidSecurityDescriptor(ResponseMessageError): pass
class ErrorInvalidSendItemSaveSettings(ResponseMessageError): pass
class ErrorInvalidSerializedAccessToken(ResponseMessageError): pass
class ErrorInvalidServerVersion(ResponseMessageError): pass
class ErrorInvalidSharingData(ResponseMessageError): pass
class ErrorInvalidSharingMessage(ResponseMessageError): pass
class ErrorInvalidSid(ResponseMessageError): pass
class ErrorInvalidSIPUri(ResponseMessageError): pass
class ErrorInvalidSmtpAddress(ResponseMessageError): pass
class ErrorInvalidSubfilterType(ResponseMessageError): pass
class ErrorInvalidSubfilterTypeNotAttendeeType(ResponseMessageError): pass
class ErrorInvalidSubfilterTypeNotRecipientType(ResponseMessageError): pass
class ErrorInvalidSubscription(ResponseMessageError): pass
class ErrorInvalidSubscriptionRequest(ResponseMessageError): pass
class ErrorInvalidSyncStateData(ResponseMessageError): pass
class ErrorInvalidTimeInterval(ResponseMessageError): pass
class ErrorInvalidUserInfo(ResponseMessageError): pass
class ErrorInvalidUserOofSettings(ResponseMessageError): pass
class ErrorInvalidUserPrincipalName(ResponseMessageError): pass
class ErrorInvalidUserSid(ResponseMessageError): pass
class ErrorInvalidUserSidMissingUPN(ResponseMessageError): pass
class ErrorInvalidValueForProperty(ResponseMessageError): pass
class ErrorInvalidWatermark(ResponseMessageError): pass
class ErrorIPGatewayNotFound(ResponseMessageError): pass
class ErrorIrresolvableConflict(ResponseMessageError): pass
class ErrorItemCorrupt(ResponseMessageError): pass
class ErrorItemNotFound(ResponseMessageError): pass
class ErrorItemPropertyRequestFailed(ResponseMessageError): pass
class ErrorItemSave(ResponseMessageError): pass
class ErrorItemSavePropertyError(ResponseMessageError): pass
class ErrorLegacyMailboxFreeBusyViewTypeNotMerged(ResponseMessageError): pass
class ErrorLocalServerObjectNotFound(ResponseMessageError): pass
class ErrorLogonAsNetworkServiceFailed(ResponseMessageError): pass
class ErrorMailboxConfiguration(ResponseMessageError): pass
class ErrorMailboxDataArrayEmpty(ResponseMessageError): pass
class ErrorMailboxDataArrayTooBig(ResponseMessageError): pass
class ErrorMailboxFailover(ResponseMessageError): pass
class ErrorMailboxLogonFailed(ResponseMessageError): pass
class ErrorMailboxMoveInProgress(ResponseMessageError): pass
class ErrorMailboxStoreUnavailable(ResponseMessageError): pass
class ErrorMailRecipientNotFound(ResponseMessageError): pass
class ErrorMailTipsDisabled(ResponseMessageError): pass
class ErrorManagedFolderAlreadyExists(ResponseMessageError): pass
class ErrorManagedFolderNotFound(ResponseMessageError): pass
class ErrorManagedFoldersRootFailure(ResponseMessageError): pass
class ErrorMeetingSuggestionGenerationFailed(ResponseMessageError): pass
class ErrorMessageDispositionRequired(ResponseMessageError): pass
class ErrorMessageSizeExceeded(ResponseMessageError): pass
class ErrorMessageTrackingNoSuchDomain(ResponseMessageError): pass
class ErrorMessageTrackingPermanentError(ResponseMessageError): pass
class ErrorMessageTrackingTransientError(ResponseMessageError): pass
class ErrorMimeContentConversionFailed(ResponseMessageError): pass
class ErrorMimeContentInvalid(ResponseMessageError): pass
class ErrorMimeContentInvalidBase64String(ResponseMessageError): pass
class ErrorMissedNotificationEvents(ResponseMessageError): pass
class ErrorMissingArgument(ResponseMessageError): pass
class ErrorMissingEmailAddress(ResponseMessageError): pass
class ErrorMissingEmailAddressForManagedFolder(ResponseMessageError): pass
class ErrorMissingInformationEmailAddress(ResponseMessageError): pass
class ErrorMissingInformationReferenceItemId(ResponseMessageError): pass
class ErrorMissingInformationSharingFolderId(ResponseMessageError): pass
class ErrorMissingItemForCreateItemAttachment(ResponseMessageError): pass
class ErrorMissingManagedFolderId(ResponseMessageError): pass
class ErrorMissingRecipients(ResponseMessageError): pass
class ErrorMissingUserIdInformation(ResponseMessageError): pass
class ErrorMoreThanOneAccessModeSpecified(ResponseMessageError): pass
class ErrorMoveCopyFailed(ResponseMessageError): pass
class ErrorMoveDistinguishedFolder(ResponseMessageError): pass
class ErrorNameResolutionMultipleResults(ResponseMessageError): pass
class ErrorNameResolutionNoMailbox(ResponseMessageError): pass
class ErrorNameResolutionNoResults(ResponseMessageError): pass
class ErrorNewEventStreamConnectionOpened(ResponseMessageError): pass
class ErrorNoApplicableProxyCASServersAvailable(ResponseMessageError): pass
class ErrorNoCalendar(ResponseMessageError): pass
class ErrorNoDestinationCASDueToKerberosRequirements(ResponseMessageError): pass
class ErrorNoDestinationCASDueToSSLRequirements(ResponseMessageError): pass
class ErrorNoDestinationCASDueToVersionMismatch(ResponseMessageError): pass
class ErrorNoFolderClassOverride(ResponseMessageError): pass
class ErrorNoFreeBusyAccess(ResponseMessageError): pass
class ErrorNonExistentMailbox(ResponseMessageError): pass
class ErrorNonPrimarySmtpAddress(ResponseMessageError): pass
class ErrorNoPropertyTagForCustomProperties(ResponseMessageError): pass
class ErrorNoPublicFolderReplicaAvailable(ResponseMessageError): pass
class ErrorNoPublicFolderServerAvailable(ResponseMessageError): pass
class ErrorNoRespondingCASInDestinationSite(ResponseMessageError): pass
class ErrorNotAllowedExternalSharingByPolicy(ResponseMessageError): pass
class ErrorNotDelegate(ResponseMessageError): pass
class ErrorNotEnoughMemory(ResponseMessageError): pass
class ErrorNotSupportedSharingMessage(ResponseMessageError): pass
class ErrorObjectTypeChanged(ResponseMessageError): pass
class ErrorOccurrenceCrossingBoundary(ResponseMessageError): pass
class ErrorOccurrenceTimeSpanTooBig(ResponseMessageError): pass
class ErrorOperationNotAllowedWithPublicFolderRoot(ResponseMessageError): pass
class ErrorOrganizationNotFederated(ResponseMessageError): pass
class ErrorOutlookRuleBlobExists(ResponseMessageError): pass
class ErrorParentFolderIdRequired(ResponseMessageError): pass
class ErrorParentFolderNotFound(ResponseMessageError): pass
class ErrorPasswordChangeRequired(ResponseMessageError): pass
class ErrorPasswordExpired(ResponseMessageError): pass
class ErrorPermissionNotAllowedByPolicy(ResponseMessageError): pass
class ErrorPhoneNumberNotDialable(ResponseMessageError): pass
class ErrorPropertyUpdate(ResponseMessageError): pass
class ErrorPropertyValidationFailure(ResponseMessageError): pass
class ErrorProxiedSubscriptionCallFailure(ResponseMessageError): pass
class ErrorProxyCallFailed(ResponseMessageError): pass
class ErrorProxyGroupSidLimitExceeded(ResponseMessageError): pass
class ErrorProxyRequestNotAllowed(ResponseMessageError): pass
class ErrorProxyRequestProcessingFailed(ResponseMessageError): pass
class ErrorProxyServiceDiscoveryFailed(ResponseMessageError): pass
class ErrorProxyTokenExpired(ResponseMessageError): pass
class ErrorPublicFolderRequestProcessingFailed(ResponseMessageError): pass
class ErrorPublicFolderServerNotFound(ResponseMessageError): pass
class ErrorQueryFilterTooLong(ResponseMessageError): pass
class ErrorQuotaExceeded(ResponseMessageError): pass
class ErrorReadEventsFailed(ResponseMessageError): pass
class ErrorReadReceiptNotPending(ResponseMessageError): pass
class ErrorRecurrenceEndDateTooBig(ResponseMessageError): pass
class ErrorRecurrenceHasNoOccurrence(ResponseMessageError): pass
class ErrorRemoveDelegatesFailed(ResponseMessageError): pass
class ErrorRequestAborted(ResponseMessageError): pass
class ErrorRequestStreamTooBig(ResponseMessageError): pass
class ErrorRequiredPropertyMissing(ResponseMessageError): pass
class ErrorResolveNamesInvalidFolderType(ResponseMessageError): pass
class ErrorResolveNamesOnlyOneContactsFolderAllowed(ResponseMessageError): pass
class ErrorResponseSchemaValidation(ResponseMessageError): pass
class ErrorRestrictionTooComplex(ResponseMessageError): pass
class ErrorRestrictionTooLong(ResponseMessageError): pass
class ErrorResultSetTooBig(ResponseMessageError): pass
class ErrorRulesOverQuota(ResponseMessageError): pass
class ErrorSavedItemFolderNotFound(ResponseMessageError): pass
class ErrorSchemaValidation(ResponseMessageError): pass
class ErrorSearchFolderNotInitialized(ResponseMessageError): pass
class ErrorSendAsDenied(ResponseMessageError): pass
class ErrorSendMeetingCancellationsRequired(ResponseMessageError): pass
class ErrorSendMeetingInvitationsOrCancellationsRequired(ResponseMessageError): pass
class ErrorSendMeetingInvitationsRequired(ResponseMessageError): pass
class ErrorSentMeetingRequestUpdate(ResponseMessageError): pass
class ErrorSentTaskRequestUpdate(ResponseMessageError): pass


class ErrorServerBusy(ResponseMessageError):
    def __init__(self, *args, **kwargs):
        self.back_off = kwargs.pop('back_off', None)  # Requested back off value in seconds
        super().__init__(*args, **kwargs)


class ErrorServiceDiscoveryFailed(ResponseMessageError): pass
class ErrorSharingNoExternalEwsAvailable(ResponseMessageError): pass
class ErrorSharingSynchronizationFailed(ResponseMessageError): pass
class ErrorStaleObject(ResponseMessageError): pass
class ErrorSubmissionQuotaExceeded(ResponseMessageError): pass
class ErrorSubscriptionAccessDenied(ResponseMessageError): pass
class ErrorSubscriptionDelegateAccessNotSupported(ResponseMessageError): pass
class ErrorSubscriptionNotFound(ResponseMessageError): pass
class ErrorSubscriptionUnsubsribed(ResponseMessageError): pass
class ErrorSyncFolderNotFound(ResponseMessageError): pass
class ErrorTimeIntervalTooBig(ResponseMessageError): pass
class ErrorTimeoutExpired(ResponseMessageError): pass
class ErrorTimeZone(ResponseMessageError): pass
class ErrorToFolderNotFound(ResponseMessageError): pass
class ErrorTokenSerializationDenied(ResponseMessageError): pass
class ErrorTooManyObjectsOpened(ResponseMessageError): pass
class ErrorUnableToGetUserOofSettings(ResponseMessageError): pass
class ErrorUnifiedMessagingDialPlanNotFound(ResponseMessageError): pass
class ErrorUnifiedMessagingRequestFailed(ResponseMessageError): pass
class ErrorUnifiedMessagingServerNotFound(ResponseMessageError): pass
class ErrorUnsupportedCulture(ResponseMessageError): pass
class ErrorUnsupportedMapiPropertyType(ResponseMessageError): pass
class ErrorUnsupportedMimeConversion(ResponseMessageError): pass
class ErrorUnsupportedPathForQuery(ResponseMessageError): pass
class ErrorUnsupportedPathForSortGroup(ResponseMessageError): pass
class ErrorUnsupportedPropertyDefinition(ResponseMessageError): pass
class ErrorUnsupportedQueryFilter(ResponseMessageError): pass
class ErrorUnsupportedRecurrence(ResponseMessageError): pass
class ErrorUnsupportedSubFilter(ResponseMessageError): pass
class ErrorUnsupportedTypeForConversion(ResponseMessageError): pass
class ErrorUpdateDelegatesFailed(ResponseMessageError): pass
class ErrorUpdatePropertyMismatch(ResponseMessageError): pass
class ErrorUserNotAllowedByPolicy(ResponseMessageError): pass
class ErrorUserNotUnifiedMessagingEnabled(ResponseMessageError): pass
class ErrorUserWithoutFederatedProxyAddress(ResponseMessageError): pass
class ErrorValueOutOfRange(ResponseMessageError): pass
class ErrorVirusDetected(ResponseMessageError): pass
class ErrorVirusMessageDeleted(ResponseMessageError): pass
class ErrorVoiceMailNotImplemented(ResponseMessageError): pass
class ErrorWebRequestInInvalidState(ResponseMessageError): pass
class ErrorWin32InteropError(ResponseMessageError): pass
class ErrorWorkingHoursSaveFailed(ResponseMessageError): pass
class ErrorWorkingHoursXmlMalformed(ResponseMessageError): pass
class ErrorWrongServerVersion(ResponseMessageError): pass
class ErrorWrongServerVersionDelegate(ResponseMessageError): pass


# Microsoft recommends to cache the autodiscover data around 24 hours and perform autodiscover
# immediately following certain error responses from EWS. See more at
# http://blogs.msdn.com/b/mstehle/archive/2010/11/09/ews-best-practices-use-autodiscover.aspx

ERRORS_REQUIRING_REAUTODISCOVER = (
    ErrorAutoDiscoverFailed,
    ErrorConnectionFailed,
    ErrorIncorrectSchemaVersion,
    ErrorInvalidCrossForestCredentials,
    ErrorInvalidIdReturnedByResolveNames,
    ErrorInvalidNetworkServiceContext,
    ErrorMailboxMoveInProgress,
    ErrorMailboxMoveInProgress,
    ErrorMailboxStoreUnavailable,
    ErrorNameResolutionNoMailbox,
    ErrorNameResolutionNoResults,
    ErrorNotEnoughMemory,
)

Classes

class AutoDiscoverCircularRedirect (value)

Global error type within this module.

Expand source code
class AutoDiscoverCircularRedirect(AutoDiscoverError):
    pass

Ancestors

class AutoDiscoverError (value)

Global error type within this module.

Expand source code
class AutoDiscoverError(TransportError):
    pass

Ancestors

Subclasses

class AutoDiscoverFailed (value)

Global error type within this module.

Expand source code
class AutoDiscoverFailed(AutoDiscoverError):
    pass

Ancestors

class AutoDiscoverRedirect (redirect_email)

Global error type within this module.

Expand source code
class AutoDiscoverRedirect(AutoDiscoverError):
    def __init__(self, redirect_email):
        self.redirect_email = redirect_email
        super().__init__(str(self))

    def __str__(self):
        return 'AutoDiscover redirects to %s' % self.redirect_email

Ancestors

class CASError (cas_error, response)

EWS will sometimes return an error message in an 'X-CasErrorCode' custom HTTP header in an HTTP 500 error code. This exception is for those cases. The caller may want to do something with the original response, so store that.

Expand source code
class CASError(EWSError):
    """EWS will sometimes return an error message in an 'X-CasErrorCode' custom HTTP header in an HTTP 500 error code.
    This exception is for those cases. The caller may want to do something with the original response, so store that.
    """

    def __init__(self, cas_error, response):
        self.cas_error = cas_error
        self.response = response
        super().__init__(str(self))

    def __str__(self):
        return 'CAS error: %s' % self.cas_error

Ancestors

  • EWSError
  • builtins.Exception
  • builtins.BaseException
class DoesNotExist (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code
class DoesNotExist(Exception):
    pass

Ancestors

  • builtins.Exception
  • builtins.BaseException
class EWSError (value)

Global error type within this module.

Expand source code
class EWSError(Exception):
    """Global error type within this module."""

    def __init__(self, value):
        super().__init__(value)
        self.value = value

    def __str__(self):
        return str(self.value)

    def __eq__(self, other):
        return repr(self) == repr(other)

Ancestors

  • builtins.Exception
  • builtins.BaseException

Subclasses

class EWSWarning (value)

Global error type within this module.

Expand source code
class EWSWarning(EWSError):
    pass

Ancestors

  • EWSError
  • builtins.Exception
  • builtins.BaseException
class ErrorADOperation (value)

Global error type within this module.

Expand source code
class ErrorADOperation(ResponseMessageError): pass

Ancestors

class ErrorADSessionFilter (value)

Global error type within this module.

Expand source code
class ErrorADSessionFilter(ResponseMessageError): pass

Ancestors

class ErrorADUnavailable (value)

Global error type within this module.

Expand source code
class ErrorADUnavailable(ResponseMessageError): pass

Ancestors

class ErrorAccessDenied (value)

Global error type within this module.

Expand source code
class ErrorAccessDenied(ResponseMessageError): pass

Ancestors

class ErrorAccessModeSpecified (value)

Global error type within this module.

Expand source code
class ErrorAccessModeSpecified(ResponseMessageError): pass

Ancestors

class ErrorAccountDisabled (value)

Global error type within this module.

Expand source code
class ErrorAccountDisabled(ResponseMessageError): pass

Ancestors

class ErrorAddDelegatesFailed (value)

Global error type within this module.

Expand source code
class ErrorAddDelegatesFailed(ResponseMessageError): pass

Ancestors

class ErrorAddressSpaceNotFound (value)

Global error type within this module.

Expand source code
class ErrorAddressSpaceNotFound(ResponseMessageError): pass

Ancestors

class ErrorAffectedTaskOccurrencesRequired (value)

Global error type within this module.

Expand source code
class ErrorAffectedTaskOccurrencesRequired(ResponseMessageError): pass

Ancestors

class ErrorApplyConversationActionFailed (value)

Global error type within this module.

Expand source code
class ErrorApplyConversationActionFailed(ResponseMessageError): pass

Ancestors

class ErrorAttachmentSizeLimitExceeded (value)

Global error type within this module.

Expand source code
class ErrorAttachmentSizeLimitExceeded(ResponseMessageError): pass

Ancestors

class ErrorAutoDiscoverFailed (value)

Global error type within this module.

Expand source code
class ErrorAutoDiscoverFailed(ResponseMessageError): pass

Ancestors

class ErrorAvailabilityConfigNotFound (value)

Global error type within this module.

Expand source code
class ErrorAvailabilityConfigNotFound(ResponseMessageError): pass

Ancestors

class ErrorBatchProcessingStopped (value)

Global error type within this module.

Expand source code
class ErrorBatchProcessingStopped(ResponseMessageError): pass

Ancestors

class ErrorCalendarCannotMoveOrCopyOccurrence (value)

Global error type within this module.

Expand source code
class ErrorCalendarCannotMoveOrCopyOccurrence(ResponseMessageError): pass

Ancestors

class ErrorCalendarCannotUpdateDeletedItem (value)

Global error type within this module.

Expand source code
class ErrorCalendarCannotUpdateDeletedItem(ResponseMessageError): pass

Ancestors

class ErrorCalendarCannotUseIdForOccurrenceId (value)

Global error type within this module.

Expand source code
class ErrorCalendarCannotUseIdForOccurrenceId(ResponseMessageError): pass

Ancestors

class ErrorCalendarCannotUseIdForRecurringMasterId (value)

Global error type within this module.

Expand source code
class ErrorCalendarCannotUseIdForRecurringMasterId(ResponseMessageError): pass

Ancestors

class ErrorCalendarDurationIsTooLong (value)

Global error type within this module.

Expand source code
class ErrorCalendarDurationIsTooLong(ResponseMessageError): pass

Ancestors

class ErrorCalendarEndDateIsEarlierThanStartDate (value)

Global error type within this module.

Expand source code
class ErrorCalendarEndDateIsEarlierThanStartDate(ResponseMessageError): pass

Ancestors

class ErrorCalendarFolderIsInvalidForCalendarView (value)

Global error type within this module.

Expand source code
class ErrorCalendarFolderIsInvalidForCalendarView(ResponseMessageError): pass

Ancestors

class ErrorCalendarInvalidAttributeValue (value)

Global error type within this module.

Expand source code
class ErrorCalendarInvalidAttributeValue(ResponseMessageError): pass

Ancestors

class ErrorCalendarInvalidDayForTimeChangePattern (value)

Global error type within this module.

Expand source code
class ErrorCalendarInvalidDayForTimeChangePattern(ResponseMessageError): pass

Ancestors

class ErrorCalendarInvalidDayForWeeklyRecurrence (value)

Global error type within this module.

Expand source code
class ErrorCalendarInvalidDayForWeeklyRecurrence(ResponseMessageError): pass

Ancestors

class ErrorCalendarInvalidPropertyState (value)

Global error type within this module.

Expand source code
class ErrorCalendarInvalidPropertyState(ResponseMessageError): pass

Ancestors

class ErrorCalendarInvalidPropertyValue (value)

Global error type within this module.

Expand source code
class ErrorCalendarInvalidPropertyValue(ResponseMessageError): pass

Ancestors

class ErrorCalendarInvalidRecurrence (value)

Global error type within this module.

Expand source code
class ErrorCalendarInvalidRecurrence(ResponseMessageError): pass

Ancestors

class ErrorCalendarInvalidTimeZone (value)

Global error type within this module.

Expand source code
class ErrorCalendarInvalidTimeZone(ResponseMessageError): pass

Ancestors

class ErrorCalendarIsCancelledForAccept (value)

Global error type within this module.

Expand source code
class ErrorCalendarIsCancelledForAccept(ResponseMessageError): pass

Ancestors

class ErrorCalendarIsCancelledForDecline (value)

Global error type within this module.

Expand source code
class ErrorCalendarIsCancelledForDecline(ResponseMessageError): pass

Ancestors

class ErrorCalendarIsCancelledForRemove (value)

Global error type within this module.

Expand source code
class ErrorCalendarIsCancelledForRemove(ResponseMessageError): pass

Ancestors

class ErrorCalendarIsCancelledForTentative (value)

Global error type within this module.

Expand source code
class ErrorCalendarIsCancelledForTentative(ResponseMessageError): pass

Ancestors

class ErrorCalendarIsDelegatedForAccept (value)

Global error type within this module.

Expand source code
class ErrorCalendarIsDelegatedForAccept(ResponseMessageError): pass

Ancestors

class ErrorCalendarIsDelegatedForDecline (value)

Global error type within this module.

Expand source code
class ErrorCalendarIsDelegatedForDecline(ResponseMessageError): pass

Ancestors

class ErrorCalendarIsDelegatedForRemove (value)

Global error type within this module.

Expand source code
class ErrorCalendarIsDelegatedForRemove(ResponseMessageError): pass

Ancestors

class ErrorCalendarIsDelegatedForTentative (value)

Global error type within this module.

Expand source code
class ErrorCalendarIsDelegatedForTentative(ResponseMessageError): pass

Ancestors

class ErrorCalendarIsNotOrganizer (value)

Global error type within this module.

Expand source code
class ErrorCalendarIsNotOrganizer(ResponseMessageError): pass

Ancestors

class ErrorCalendarIsOrganizerForAccept (value)

Global error type within this module.

Expand source code
class ErrorCalendarIsOrganizerForAccept(ResponseMessageError): pass

Ancestors

class ErrorCalendarIsOrganizerForDecline (value)

Global error type within this module.

Expand source code
class ErrorCalendarIsOrganizerForDecline(ResponseMessageError): pass

Ancestors

class ErrorCalendarIsOrganizerForRemove (value)

Global error type within this module.

Expand source code
class ErrorCalendarIsOrganizerForRemove(ResponseMessageError): pass

Ancestors

class ErrorCalendarIsOrganizerForTentative (value)

Global error type within this module.

Expand source code
class ErrorCalendarIsOrganizerForTentative(ResponseMessageError): pass

Ancestors

class ErrorCalendarMeetingRequestIsOutOfDate (value)

Global error type within this module.

Expand source code
class ErrorCalendarMeetingRequestIsOutOfDate(ResponseMessageError): pass

Ancestors

class ErrorCalendarOccurrenceIndexIsOutOfRecurrenceRange (value)

Global error type within this module.

Expand source code
class ErrorCalendarOccurrenceIndexIsOutOfRecurrenceRange(ResponseMessageError): pass

Ancestors

class ErrorCalendarOccurrenceIsDeletedFromRecurrence (value)

Global error type within this module.

Expand source code
class ErrorCalendarOccurrenceIsDeletedFromRecurrence(ResponseMessageError): pass

Ancestors

class ErrorCalendarOutOfRange (value)

Global error type within this module.

Expand source code
class ErrorCalendarOutOfRange(ResponseMessageError): pass

Ancestors

class ErrorCalendarViewRangeTooBig (value)

Global error type within this module.

Expand source code
class ErrorCalendarViewRangeTooBig(ResponseMessageError): pass

Ancestors

class ErrorCallerIsInvalidADAccount (value)

Global error type within this module.

Expand source code
class ErrorCallerIsInvalidADAccount(ResponseMessageError): pass

Ancestors

class ErrorCannotCreateCalendarItemInNonCalendarFolder (value)

Global error type within this module.

Expand source code
class ErrorCannotCreateCalendarItemInNonCalendarFolder(ResponseMessageError): pass

Ancestors

class ErrorCannotCreateContactInNonContactFolder (value)

Global error type within this module.

Expand source code
class ErrorCannotCreateContactInNonContactFolder(ResponseMessageError): pass

Ancestors

class ErrorCannotCreatePostItemInNonMailFolder (value)

Global error type within this module.

Expand source code
class ErrorCannotCreatePostItemInNonMailFolder(ResponseMessageError): pass

Ancestors

class ErrorCannotCreateTaskInNonTaskFolder (value)

Global error type within this module.

Expand source code
class ErrorCannotCreateTaskInNonTaskFolder(ResponseMessageError): pass

Ancestors

class ErrorCannotDeleteObject (value)

Global error type within this module.

Expand source code
class ErrorCannotDeleteObject(ResponseMessageError): pass

Ancestors

class ErrorCannotDeleteTaskOccurrence (value)

Global error type within this module.

Expand source code
class ErrorCannotDeleteTaskOccurrence(ResponseMessageError): pass

Ancestors

class ErrorCannotEmptyFolder (value)

Global error type within this module.

Expand source code
class ErrorCannotEmptyFolder(ResponseMessageError): pass

Ancestors

class ErrorCannotOpenFileAttachment (value)

Global error type within this module.

Expand source code
class ErrorCannotOpenFileAttachment(ResponseMessageError): pass

Ancestors

class ErrorCannotSetCalendarPermissionOnNonCalendarFolder (value)

Global error type within this module.

Expand source code
class ErrorCannotSetCalendarPermissionOnNonCalendarFolder(ResponseMessageError): pass

Ancestors

class ErrorCannotSetNonCalendarPermissionOnCalendarFolder (value)

Global error type within this module.

Expand source code
class ErrorCannotSetNonCalendarPermissionOnCalendarFolder(ResponseMessageError): pass

Ancestors

class ErrorCannotSetPermissionUnknownEntries (value)

Global error type within this module.

Expand source code
class ErrorCannotSetPermissionUnknownEntries(ResponseMessageError): pass

Ancestors

class ErrorCannotUseFolderIdForItemId (value)

Global error type within this module.

Expand source code
class ErrorCannotUseFolderIdForItemId(ResponseMessageError): pass

Ancestors

class ErrorCannotUseItemIdForFolderId (value)

Global error type within this module.

Expand source code
class ErrorCannotUseItemIdForFolderId(ResponseMessageError): pass

Ancestors

class ErrorChangeKeyRequired (value)

Global error type within this module.

Expand source code
class ErrorChangeKeyRequired(ResponseMessageError): pass

Ancestors

class ErrorChangeKeyRequiredForWriteOperations (value)

Global error type within this module.

Expand source code
class ErrorChangeKeyRequiredForWriteOperations(ResponseMessageError): pass

Ancestors

class ErrorClientDisconnected (value)

Global error type within this module.

Expand source code
class ErrorClientDisconnected(ResponseMessageError): pass

Ancestors

class ErrorConnectionFailed (value)

Global error type within this module.

Expand source code
class ErrorConnectionFailed(ResponseMessageError): pass

Ancestors

class ErrorConnectionFailedTransientError (value)

Global error type within this module.

Expand source code
class ErrorConnectionFailedTransientError(ResponseMessageError): pass

Ancestors

class ErrorContainsFilterWrongType (value)

Global error type within this module.

Expand source code
class ErrorContainsFilterWrongType(ResponseMessageError): pass

Ancestors

class ErrorContentConversionFailed (value)

Global error type within this module.

Expand source code
class ErrorContentConversionFailed(ResponseMessageError): pass

Ancestors

class ErrorCorruptData (value)

Global error type within this module.

Expand source code
class ErrorCorruptData(ResponseMessageError): pass

Ancestors

class ErrorCreateItemAccessDenied (value)

Global error type within this module.

Expand source code
class ErrorCreateItemAccessDenied(ResponseMessageError): pass

Ancestors

class ErrorCreateManagedFolderPartialCompletion (value)

Global error type within this module.

Expand source code
class ErrorCreateManagedFolderPartialCompletion(ResponseMessageError): pass

Ancestors

class ErrorCreateSubfolderAccessDenied (value)

Global error type within this module.

Expand source code
class ErrorCreateSubfolderAccessDenied(ResponseMessageError): pass

Ancestors

class ErrorCrossMailboxMoveCopy (value)

Global error type within this module.

Expand source code
class ErrorCrossMailboxMoveCopy(ResponseMessageError): pass

Ancestors

class ErrorCrossSiteRequest (value)

Global error type within this module.

Expand source code
class ErrorCrossSiteRequest(ResponseMessageError): pass

Ancestors

class ErrorDataSizeLimitExceeded (value)

Global error type within this module.

Expand source code
class ErrorDataSizeLimitExceeded(ResponseMessageError): pass

Ancestors

class ErrorDataSourceOperation (value)

Global error type within this module.

Expand source code
class ErrorDataSourceOperation(ResponseMessageError): pass

Ancestors

class ErrorDelegateAlreadyExists (value)

Global error type within this module.

Expand source code
class ErrorDelegateAlreadyExists(ResponseMessageError): pass

Ancestors

class ErrorDelegateCannotAddOwner (value)

Global error type within this module.

Expand source code
class ErrorDelegateCannotAddOwner(ResponseMessageError): pass

Ancestors

class ErrorDelegateMissingConfiguration (value)

Global error type within this module.

Expand source code
class ErrorDelegateMissingConfiguration(ResponseMessageError): pass

Ancestors

class ErrorDelegateNoUser (value)

Global error type within this module.

Expand source code
class ErrorDelegateNoUser(ResponseMessageError): pass

Ancestors

class ErrorDelegateValidationFailed (value)

Global error type within this module.

Expand source code
class ErrorDelegateValidationFailed(ResponseMessageError): pass

Ancestors

class ErrorDeleteDistinguishedFolder (value)

Global error type within this module.

Expand source code
class ErrorDeleteDistinguishedFolder(ResponseMessageError): pass

Ancestors

class ErrorDeleteItemsFailed (value)

Global error type within this module.

Expand source code
class ErrorDeleteItemsFailed(ResponseMessageError): pass

Ancestors

class ErrorDistinguishedUserNotSupported (value)

Global error type within this module.

Expand source code
class ErrorDistinguishedUserNotSupported(ResponseMessageError): pass

Ancestors

class ErrorDistributionListMemberNotExist (value)

Global error type within this module.

Expand source code
class ErrorDistributionListMemberNotExist(ResponseMessageError): pass

Ancestors

class ErrorDuplicateInputFolderNames (value)

Global error type within this module.

Expand source code
class ErrorDuplicateInputFolderNames(ResponseMessageError): pass

Ancestors

class ErrorDuplicateSOAPHeader (value)

Global error type within this module.

Expand source code
class ErrorDuplicateSOAPHeader(ResponseMessageError): pass

Ancestors

class ErrorDuplicateUserIdsSpecified (value)

Global error type within this module.

Expand source code
class ErrorDuplicateUserIdsSpecified(ResponseMessageError): pass

Ancestors

class ErrorEmailAddressMismatch (value)

Global error type within this module.

Expand source code
class ErrorEmailAddressMismatch(ResponseMessageError): pass

Ancestors

class ErrorEventNotFound (value)

Global error type within this module.

Expand source code
class ErrorEventNotFound(ResponseMessageError): pass

Ancestors

class ErrorExceededConnectionCount (value)

Global error type within this module.

Expand source code
class ErrorExceededConnectionCount(ResponseMessageError): pass

Ancestors

class ErrorExceededFindCountLimit (value)

Global error type within this module.

Expand source code
class ErrorExceededFindCountLimit(ResponseMessageError): pass

Ancestors

class ErrorExceededSubscriptionCount (value)

Global error type within this module.

Expand source code
class ErrorExceededSubscriptionCount(ResponseMessageError): pass

Ancestors

class ErrorExpiredSubscription (value)

Global error type within this module.

Expand source code
class ErrorExpiredSubscription(ResponseMessageError): pass

Ancestors

class ErrorFolderCorrupt (value)

Global error type within this module.

Expand source code
class ErrorFolderCorrupt(ResponseMessageError): pass

Ancestors

class ErrorFolderExists (value)

Global error type within this module.

Expand source code
class ErrorFolderExists(ResponseMessageError): pass

Ancestors

class ErrorFolderNotFound (value)

Global error type within this module.

Expand source code
class ErrorFolderNotFound(ResponseMessageError): pass

Ancestors

class ErrorFolderPropertyRequestFailed (value)

Global error type within this module.

Expand source code
class ErrorFolderPropertyRequestFailed(ResponseMessageError): pass

Ancestors

class ErrorFolderSave (value)

Global error type within this module.

Expand source code
class ErrorFolderSave(ResponseMessageError): pass

Ancestors

class ErrorFolderSaveFailed (value)

Global error type within this module.

Expand source code
class ErrorFolderSaveFailed(ResponseMessageError): pass

Ancestors

class ErrorFolderSavePropertyError (value)

Global error type within this module.

Expand source code
class ErrorFolderSavePropertyError(ResponseMessageError): pass

Ancestors

class ErrorFreeBusyDLLimitReached (value)

Global error type within this module.

Expand source code
class ErrorFreeBusyDLLimitReached(ResponseMessageError): pass

Ancestors

class ErrorFreeBusyGenerationFailed (value)

Global error type within this module.

Expand source code
class ErrorFreeBusyGenerationFailed(ResponseMessageError): pass

Ancestors

class ErrorGetServerSecurityDescriptorFailed (value)

Global error type within this module.

Expand source code
class ErrorGetServerSecurityDescriptorFailed(ResponseMessageError): pass

Ancestors

class ErrorIPGatewayNotFound (value)

Global error type within this module.

Expand source code
class ErrorIPGatewayNotFound(ResponseMessageError): pass

Ancestors

class ErrorImpersonateUserDenied (value)

Global error type within this module.

Expand source code
class ErrorImpersonateUserDenied(ResponseMessageError): pass

Ancestors

class ErrorImpersonationDenied (value)

Global error type within this module.

Expand source code
class ErrorImpersonationDenied(ResponseMessageError): pass

Ancestors

class ErrorImpersonationFailed (value)

Global error type within this module.

Expand source code
class ErrorImpersonationFailed(ResponseMessageError): pass

Ancestors

class ErrorInboxRulesValidationError (value)

Global error type within this module.

Expand source code
class ErrorInboxRulesValidationError(ResponseMessageError): pass

Ancestors

class ErrorIncorrectSchemaVersion (value)

Global error type within this module.

Expand source code
class ErrorIncorrectSchemaVersion(ResponseMessageError): pass

Ancestors

class ErrorIncorrectUpdatePropertyCount (value)

Global error type within this module.

Expand source code
class ErrorIncorrectUpdatePropertyCount(ResponseMessageError): pass

Ancestors

class ErrorIndividualMailboxLimitReached (value)

Global error type within this module.

Expand source code
class ErrorIndividualMailboxLimitReached(ResponseMessageError): pass

Ancestors

class ErrorInsufficientResources (value)

Global error type within this module.

Expand source code
class ErrorInsufficientResources(ResponseMessageError): pass

Ancestors

class ErrorInternalServerError (value)

Global error type within this module.

Expand source code
class ErrorInternalServerError(ResponseMessageError): pass

Ancestors

class ErrorInternalServerTransientError (value)

Global error type within this module.

Expand source code
class ErrorInternalServerTransientError(ResponseMessageError): pass

Ancestors

class ErrorInvalidAccessLevel (value)

Global error type within this module.

Expand source code
class ErrorInvalidAccessLevel(ResponseMessageError): pass

Ancestors

class ErrorInvalidArgument (value)

Global error type within this module.

Expand source code
class ErrorInvalidArgument(ResponseMessageError): pass

Ancestors

class ErrorInvalidAttachmentId (value)

Global error type within this module.

Expand source code
class ErrorInvalidAttachmentId(ResponseMessageError): pass

Ancestors

class ErrorInvalidAttachmentSubfilter (value)

Global error type within this module.

Expand source code
class ErrorInvalidAttachmentSubfilter(ResponseMessageError): pass

Ancestors

class ErrorInvalidAttachmentSubfilterTextFilter (value)

Global error type within this module.

Expand source code
class ErrorInvalidAttachmentSubfilterTextFilter(ResponseMessageError): pass

Ancestors

class ErrorInvalidAuthorizationContext (value)

Global error type within this module.

Expand source code
class ErrorInvalidAuthorizationContext(ResponseMessageError): pass

Ancestors

class ErrorInvalidChangeKey (value)

Global error type within this module.

Expand source code
class ErrorInvalidChangeKey(ResponseMessageError): pass

Ancestors

class ErrorInvalidClientSecurityContext (value)

Global error type within this module.

Expand source code
class ErrorInvalidClientSecurityContext(ResponseMessageError): pass

Ancestors

class ErrorInvalidCompleteDate (value)

Global error type within this module.

Expand source code
class ErrorInvalidCompleteDate(ResponseMessageError): pass

Ancestors

class ErrorInvalidContactEmailAddress (value)

Global error type within this module.

Expand source code
class ErrorInvalidContactEmailAddress(ResponseMessageError): pass

Ancestors

class ErrorInvalidContactEmailIndex (value)

Global error type within this module.

Expand source code
class ErrorInvalidContactEmailIndex(ResponseMessageError): pass

Ancestors

class ErrorInvalidCrossForestCredentials (value)

Global error type within this module.

Expand source code
class ErrorInvalidCrossForestCredentials(ResponseMessageError): pass

Ancestors

class ErrorInvalidDelegatePermission (value)

Global error type within this module.

Expand source code
class ErrorInvalidDelegatePermission(ResponseMessageError): pass

Ancestors

class ErrorInvalidDelegateUserId (value)

Global error type within this module.

Expand source code
class ErrorInvalidDelegateUserId(ResponseMessageError): pass

Ancestors

class ErrorInvalidExchangeImpersonationHeaderData (value)

Global error type within this module.

Expand source code
class ErrorInvalidExchangeImpersonationHeaderData(ResponseMessageError): pass

Ancestors

class ErrorInvalidExcludesRestriction (value)

Global error type within this module.

Expand source code
class ErrorInvalidExcludesRestriction(ResponseMessageError): pass

Ancestors

class ErrorInvalidExpressionTypeForSubFilter (value)

Global error type within this module.

Expand source code
class ErrorInvalidExpressionTypeForSubFilter(ResponseMessageError): pass

Ancestors

class ErrorInvalidExtendedProperty (value)

Global error type within this module.

Expand source code
class ErrorInvalidExtendedProperty(ResponseMessageError): pass

Ancestors

class ErrorInvalidExtendedPropertyValue (value)

Global error type within this module.

Expand source code
class ErrorInvalidExtendedPropertyValue(ResponseMessageError): pass

Ancestors

class ErrorInvalidExternalSharingInitiator (value)

Global error type within this module.

Expand source code
class ErrorInvalidExternalSharingInitiator(ResponseMessageError): pass

Ancestors

class ErrorInvalidExternalSharingSubscriber (value)

Global error type within this module.

Expand source code
class ErrorInvalidExternalSharingSubscriber(ResponseMessageError): pass

Ancestors

class ErrorInvalidFederatedOrganizationId (value)

Global error type within this module.

Expand source code
class ErrorInvalidFederatedOrganizationId(ResponseMessageError): pass

Ancestors

class ErrorInvalidFolderId (value)

Global error type within this module.

Expand source code
class ErrorInvalidFolderId(ResponseMessageError): pass

Ancestors

class ErrorInvalidFolderTypeForOperation (value)

Global error type within this module.

Expand source code
class ErrorInvalidFolderTypeForOperation(ResponseMessageError): pass

Ancestors

class ErrorInvalidFractionalPagingParameters (value)

Global error type within this module.

Expand source code
class ErrorInvalidFractionalPagingParameters(ResponseMessageError): pass

Ancestors

class ErrorInvalidFreeBusyViewType (value)

Global error type within this module.

Expand source code
class ErrorInvalidFreeBusyViewType(ResponseMessageError): pass

Ancestors

class ErrorInvalidGetSharingFolderRequest (value)

Global error type within this module.

Expand source code
class ErrorInvalidGetSharingFolderRequest(ResponseMessageError): pass

Ancestors

class ErrorInvalidId (value)

Global error type within this module.

Expand source code
class ErrorInvalidId(ResponseMessageError): pass

Ancestors

class ErrorInvalidIdEmpty (value)

Global error type within this module.

Expand source code
class ErrorInvalidIdEmpty(ResponseMessageError): pass

Ancestors

class ErrorInvalidIdMalformed (value)

Global error type within this module.

Expand source code
class ErrorInvalidIdMalformed(ResponseMessageError): pass

Ancestors

class ErrorInvalidIdMalformedEwsLegacyIdFormat (value)

Global error type within this module.

Expand source code
class ErrorInvalidIdMalformedEwsLegacyIdFormat(ResponseMessageError): pass

Ancestors

class ErrorInvalidIdMonikerTooLong (value)

Global error type within this module.

Expand source code
class ErrorInvalidIdMonikerTooLong(ResponseMessageError): pass

Ancestors

class ErrorInvalidIdNotAnItemAttachmentId (value)

Global error type within this module.

Expand source code
class ErrorInvalidIdNotAnItemAttachmentId(ResponseMessageError): pass

Ancestors

class ErrorInvalidIdReturnedByResolveNames (value)

Global error type within this module.

Expand source code
class ErrorInvalidIdReturnedByResolveNames(ResponseMessageError): pass

Ancestors

class ErrorInvalidIdStoreObjectIdTooLong (value)

Global error type within this module.

Expand source code
class ErrorInvalidIdStoreObjectIdTooLong(ResponseMessageError): pass

Ancestors

class ErrorInvalidIdTooManyAttachmentLevels (value)

Global error type within this module.

Expand source code
class ErrorInvalidIdTooManyAttachmentLevels(ResponseMessageError): pass

Ancestors

class ErrorInvalidIdXml (value)

Global error type within this module.

Expand source code
class ErrorInvalidIdXml(ResponseMessageError): pass

Ancestors

class ErrorInvalidIndexedPagingParameters (value)

Global error type within this module.

Expand source code
class ErrorInvalidIndexedPagingParameters(ResponseMessageError): pass

Ancestors

class ErrorInvalidInternetHeaderChildNodes (value)

Global error type within this module.

Expand source code
class ErrorInvalidInternetHeaderChildNodes(ResponseMessageError): pass

Ancestors

class ErrorInvalidItemForOperationAcceptItem (value)

Global error type within this module.

Expand source code
class ErrorInvalidItemForOperationAcceptItem(ResponseMessageError): pass

Ancestors

class ErrorInvalidItemForOperationCancelItem (value)

Global error type within this module.

Expand source code
class ErrorInvalidItemForOperationCancelItem(ResponseMessageError): pass

Ancestors

class ErrorInvalidItemForOperationCreateItem (value)

Global error type within this module.

Expand source code
class ErrorInvalidItemForOperationCreateItem(ResponseMessageError): pass

Ancestors

class ErrorInvalidItemForOperationCreateItemAttachment (value)

Global error type within this module.

Expand source code
class ErrorInvalidItemForOperationCreateItemAttachment(ResponseMessageError): pass

Ancestors

class ErrorInvalidItemForOperationDeclineItem (value)

Global error type within this module.

Expand source code
class ErrorInvalidItemForOperationDeclineItem(ResponseMessageError): pass

Ancestors

class ErrorInvalidItemForOperationExpandDL (value)

Global error type within this module.

Expand source code
class ErrorInvalidItemForOperationExpandDL(ResponseMessageError): pass

Ancestors

class ErrorInvalidItemForOperationRemoveItem (value)

Global error type within this module.

Expand source code
class ErrorInvalidItemForOperationRemoveItem(ResponseMessageError): pass

Ancestors

class ErrorInvalidItemForOperationSendItem (value)

Global error type within this module.

Expand source code
class ErrorInvalidItemForOperationSendItem(ResponseMessageError): pass

Ancestors

class ErrorInvalidItemForOperationTentative (value)

Global error type within this module.

Expand source code
class ErrorInvalidItemForOperationTentative(ResponseMessageError): pass

Ancestors

class ErrorInvalidLicense (value)

Global error type within this module.

Expand source code
class ErrorInvalidLicense(ResponseMessageError): pass

Ancestors

class ErrorInvalidLogonType (value)

Global error type within this module.

Expand source code
class ErrorInvalidLogonType(ResponseMessageError): pass

Ancestors

class ErrorInvalidMailbox (value)

Global error type within this module.

Expand source code
class ErrorInvalidMailbox(ResponseMessageError): pass

Ancestors

class ErrorInvalidManagedFolderProperty (value)

Global error type within this module.

Expand source code
class ErrorInvalidManagedFolderProperty(ResponseMessageError): pass

Ancestors

class ErrorInvalidManagedFolderQuota (value)

Global error type within this module.

Expand source code
class ErrorInvalidManagedFolderQuota(ResponseMessageError): pass

Ancestors

class ErrorInvalidManagedFolderSize (value)

Global error type within this module.

Expand source code
class ErrorInvalidManagedFolderSize(ResponseMessageError): pass

Ancestors

class ErrorInvalidMergedFreeBusyInterval (value)

Global error type within this module.

Expand source code
class ErrorInvalidMergedFreeBusyInterval(ResponseMessageError): pass

Ancestors

class ErrorInvalidNameForNameResolution (value)

Global error type within this module.

Expand source code
class ErrorInvalidNameForNameResolution(ResponseMessageError): pass

Ancestors

class ErrorInvalidNetworkServiceContext (value)

Global error type within this module.

Expand source code
class ErrorInvalidNetworkServiceContext(ResponseMessageError): pass

Ancestors

class ErrorInvalidOofParameter (value)

Global error type within this module.

Expand source code
class ErrorInvalidOofParameter(ResponseMessageError): pass

Ancestors

class ErrorInvalidOperation (value)

Global error type within this module.

Expand source code
class ErrorInvalidOperation(ResponseMessageError): pass

Ancestors

class ErrorInvalidOrganizationRelationshipForFreeBusy (value)

Global error type within this module.

Expand source code
class ErrorInvalidOrganizationRelationshipForFreeBusy(ResponseMessageError): pass

Ancestors

class ErrorInvalidPagingMaxRows (value)

Global error type within this module.

Expand source code
class ErrorInvalidPagingMaxRows(ResponseMessageError): pass

Ancestors

class ErrorInvalidParentFolder (value)

Global error type within this module.

Expand source code
class ErrorInvalidParentFolder(ResponseMessageError): pass

Ancestors

class ErrorInvalidPercentCompleteValue (value)

Global error type within this module.

Expand source code
class ErrorInvalidPercentCompleteValue(ResponseMessageError): pass

Ancestors

class ErrorInvalidPermissionSettings (value)

Global error type within this module.

Expand source code
class ErrorInvalidPermissionSettings(ResponseMessageError): pass

Ancestors

class ErrorInvalidPhoneCallId (value)

Global error type within this module.

Expand source code
class ErrorInvalidPhoneCallId(ResponseMessageError): pass

Ancestors

class ErrorInvalidPhoneNumber (value)

Global error type within this module.

Expand source code
class ErrorInvalidPhoneNumber(ResponseMessageError): pass

Ancestors

class ErrorInvalidPropertyAppend (value)

Global error type within this module.

Expand source code
class ErrorInvalidPropertyAppend(ResponseMessageError): pass

Ancestors

class ErrorInvalidPropertyDelete (value)

Global error type within this module.

Expand source code
class ErrorInvalidPropertyDelete(ResponseMessageError): pass

Ancestors

class ErrorInvalidPropertyForExists (value)

Global error type within this module.

Expand source code
class ErrorInvalidPropertyForExists(ResponseMessageError): pass

Ancestors

class ErrorInvalidPropertyForOperation (value)

Global error type within this module.

Expand source code
class ErrorInvalidPropertyForOperation(ResponseMessageError): pass

Ancestors

class ErrorInvalidPropertyRequest (value)

Global error type within this module.

Expand source code
class ErrorInvalidPropertyRequest(ResponseMessageError): pass

Ancestors

class ErrorInvalidPropertySet (value)

Global error type within this module.

Expand source code
class ErrorInvalidPropertySet(ResponseMessageError): pass

Ancestors

class ErrorInvalidPropertyUpdateSentMessage (value)

Global error type within this module.

Expand source code
class ErrorInvalidPropertyUpdateSentMessage(ResponseMessageError): pass

Ancestors

class ErrorInvalidProxySecurityContext (value)

Global error type within this module.

Expand source code
class ErrorInvalidProxySecurityContext(ResponseMessageError): pass

Ancestors

class ErrorInvalidPullSubscriptionId (value)

Global error type within this module.

Expand source code
class ErrorInvalidPullSubscriptionId(ResponseMessageError): pass

Ancestors

class ErrorInvalidPushSubscriptionUrl (value)

Global error type within this module.

Expand source code
class ErrorInvalidPushSubscriptionUrl(ResponseMessageError): pass

Ancestors

class ErrorInvalidRecipientSubfilter (value)

Global error type within this module.

Expand source code
class ErrorInvalidRecipientSubfilter(ResponseMessageError): pass

Ancestors

class ErrorInvalidRecipientSubfilterComparison (value)

Global error type within this module.

Expand source code
class ErrorInvalidRecipientSubfilterComparison(ResponseMessageError): pass

Ancestors

class ErrorInvalidRecipientSubfilterOrder (value)

Global error type within this module.

Expand source code
class ErrorInvalidRecipientSubfilterOrder(ResponseMessageError): pass

Ancestors

class ErrorInvalidRecipientSubfilterTextFilter (value)

Global error type within this module.

Expand source code
class ErrorInvalidRecipientSubfilterTextFilter(ResponseMessageError): pass

Ancestors

class ErrorInvalidRecipients (value)

Global error type within this module.

Expand source code
class ErrorInvalidRecipients(ResponseMessageError): pass

Ancestors

class ErrorInvalidReferenceItem (value)

Global error type within this module.

Expand source code
class ErrorInvalidReferenceItem(ResponseMessageError): pass

Ancestors

class ErrorInvalidRequest (value)

Global error type within this module.

Expand source code
class ErrorInvalidRequest(ResponseMessageError): pass

Ancestors

class ErrorInvalidRestriction (value)

Global error type within this module.

Expand source code
class ErrorInvalidRestriction(ResponseMessageError): pass

Ancestors

class ErrorInvalidRoutingType (value)

Global error type within this module.

Expand source code
class ErrorInvalidRoutingType(ResponseMessageError): pass

Ancestors

class ErrorInvalidSIPUri (value)

Global error type within this module.

Expand source code
class ErrorInvalidSIPUri(ResponseMessageError): pass

Ancestors

class ErrorInvalidScheduledOofDuration (value)

Global error type within this module.

Expand source code
class ErrorInvalidScheduledOofDuration(ResponseMessageError): pass

Ancestors

class ErrorInvalidSchemaVersionForMailboxVersion (value)

Global error type within this module.

Expand source code
class ErrorInvalidSchemaVersionForMailboxVersion(ResponseMessageError): pass

Ancestors

class ErrorInvalidSecurityDescriptor (value)

Global error type within this module.

Expand source code
class ErrorInvalidSecurityDescriptor(ResponseMessageError): pass

Ancestors

class ErrorInvalidSendItemSaveSettings (value)

Global error type within this module.

Expand source code
class ErrorInvalidSendItemSaveSettings(ResponseMessageError): pass

Ancestors

class ErrorInvalidSerializedAccessToken (value)

Global error type within this module.

Expand source code
class ErrorInvalidSerializedAccessToken(ResponseMessageError): pass

Ancestors

class ErrorInvalidServerVersion (value)

Global error type within this module.

Expand source code
class ErrorInvalidServerVersion(ResponseMessageError): pass

Ancestors

class ErrorInvalidSharingData (value)

Global error type within this module.

Expand source code
class ErrorInvalidSharingData(ResponseMessageError): pass

Ancestors

class ErrorInvalidSharingMessage (value)

Global error type within this module.

Expand source code
class ErrorInvalidSharingMessage(ResponseMessageError): pass

Ancestors

class ErrorInvalidSid (value)

Global error type within this module.

Expand source code
class ErrorInvalidSid(ResponseMessageError): pass

Ancestors

class ErrorInvalidSmtpAddress (value)

Global error type within this module.

Expand source code
class ErrorInvalidSmtpAddress(ResponseMessageError): pass

Ancestors

class ErrorInvalidSubfilterType (value)

Global error type within this module.

Expand source code
class ErrorInvalidSubfilterType(ResponseMessageError): pass

Ancestors

class ErrorInvalidSubfilterTypeNotAttendeeType (value)

Global error type within this module.

Expand source code
class ErrorInvalidSubfilterTypeNotAttendeeType(ResponseMessageError): pass

Ancestors

class ErrorInvalidSubfilterTypeNotRecipientType (value)

Global error type within this module.

Expand source code
class ErrorInvalidSubfilterTypeNotRecipientType(ResponseMessageError): pass

Ancestors

class ErrorInvalidSubscription (value)

Global error type within this module.

Expand source code
class ErrorInvalidSubscription(ResponseMessageError): pass

Ancestors

class ErrorInvalidSubscriptionRequest (value)

Global error type within this module.

Expand source code
class ErrorInvalidSubscriptionRequest(ResponseMessageError): pass

Ancestors

class ErrorInvalidSyncStateData (value)

Global error type within this module.

Expand source code
class ErrorInvalidSyncStateData(ResponseMessageError): pass

Ancestors

class ErrorInvalidTimeInterval (value)

Global error type within this module.

Expand source code
class ErrorInvalidTimeInterval(ResponseMessageError): pass

Ancestors

class ErrorInvalidUserInfo (value)

Global error type within this module.

Expand source code
class ErrorInvalidUserInfo(ResponseMessageError): pass

Ancestors

class ErrorInvalidUserOofSettings (value)

Global error type within this module.

Expand source code
class ErrorInvalidUserOofSettings(ResponseMessageError): pass

Ancestors

class ErrorInvalidUserPrincipalName (value)

Global error type within this module.

Expand source code
class ErrorInvalidUserPrincipalName(ResponseMessageError): pass

Ancestors

class ErrorInvalidUserSid (value)

Global error type within this module.

Expand source code
class ErrorInvalidUserSid(ResponseMessageError): pass

Ancestors

class ErrorInvalidUserSidMissingUPN (value)

Global error type within this module.

Expand source code
class ErrorInvalidUserSidMissingUPN(ResponseMessageError): pass

Ancestors

class ErrorInvalidValueForProperty (value)

Global error type within this module.

Expand source code
class ErrorInvalidValueForProperty(ResponseMessageError): pass

Ancestors

class ErrorInvalidWatermark (value)

Global error type within this module.

Expand source code
class ErrorInvalidWatermark(ResponseMessageError): pass

Ancestors

class ErrorIrresolvableConflict (value)

Global error type within this module.

Expand source code
class ErrorIrresolvableConflict(ResponseMessageError): pass

Ancestors

class ErrorItemCorrupt (value)

Global error type within this module.

Expand source code
class ErrorItemCorrupt(ResponseMessageError): pass

Ancestors

class ErrorItemNotFound (value)

Global error type within this module.

Expand source code
class ErrorItemNotFound(ResponseMessageError): pass

Ancestors

class ErrorItemPropertyRequestFailed (value)

Global error type within this module.

Expand source code
class ErrorItemPropertyRequestFailed(ResponseMessageError): pass

Ancestors

class ErrorItemSave (value)

Global error type within this module.

Expand source code
class ErrorItemSave(ResponseMessageError): pass

Ancestors

class ErrorItemSavePropertyError (value)

Global error type within this module.

Expand source code
class ErrorItemSavePropertyError(ResponseMessageError): pass

Ancestors

class ErrorLegacyMailboxFreeBusyViewTypeNotMerged (value)

Global error type within this module.

Expand source code
class ErrorLegacyMailboxFreeBusyViewTypeNotMerged(ResponseMessageError): pass

Ancestors

class ErrorLocalServerObjectNotFound (value)

Global error type within this module.

Expand source code
class ErrorLocalServerObjectNotFound(ResponseMessageError): pass

Ancestors

class ErrorLogonAsNetworkServiceFailed (value)

Global error type within this module.

Expand source code
class ErrorLogonAsNetworkServiceFailed(ResponseMessageError): pass

Ancestors

class ErrorMailRecipientNotFound (value)

Global error type within this module.

Expand source code
class ErrorMailRecipientNotFound(ResponseMessageError): pass

Ancestors

class ErrorMailTipsDisabled (value)

Global error type within this module.

Expand source code
class ErrorMailTipsDisabled(ResponseMessageError): pass

Ancestors

class ErrorMailboxConfiguration (value)

Global error type within this module.

Expand source code
class ErrorMailboxConfiguration(ResponseMessageError): pass

Ancestors

class ErrorMailboxDataArrayEmpty (value)

Global error type within this module.

Expand source code
class ErrorMailboxDataArrayEmpty(ResponseMessageError): pass

Ancestors

class ErrorMailboxDataArrayTooBig (value)

Global error type within this module.

Expand source code
class ErrorMailboxDataArrayTooBig(ResponseMessageError): pass

Ancestors

class ErrorMailboxFailover (value)

Global error type within this module.

Expand source code
class ErrorMailboxFailover(ResponseMessageError): pass

Ancestors

class ErrorMailboxLogonFailed (value)

Global error type within this module.

Expand source code
class ErrorMailboxLogonFailed(ResponseMessageError): pass

Ancestors

class ErrorMailboxMoveInProgress (value)

Global error type within this module.

Expand source code
class ErrorMailboxMoveInProgress(ResponseMessageError): pass

Ancestors

class ErrorMailboxStoreUnavailable (value)

Global error type within this module.

Expand source code
class ErrorMailboxStoreUnavailable(ResponseMessageError): pass

Ancestors

class ErrorManagedFolderAlreadyExists (value)

Global error type within this module.

Expand source code
class ErrorManagedFolderAlreadyExists(ResponseMessageError): pass

Ancestors

class ErrorManagedFolderNotFound (value)

Global error type within this module.

Expand source code
class ErrorManagedFolderNotFound(ResponseMessageError): pass

Ancestors

class ErrorManagedFoldersRootFailure (value)

Global error type within this module.

Expand source code
class ErrorManagedFoldersRootFailure(ResponseMessageError): pass

Ancestors

class ErrorMeetingSuggestionGenerationFailed (value)

Global error type within this module.

Expand source code
class ErrorMeetingSuggestionGenerationFailed(ResponseMessageError): pass

Ancestors

class ErrorMessageDispositionRequired (value)

Global error type within this module.

Expand source code
class ErrorMessageDispositionRequired(ResponseMessageError): pass

Ancestors

class ErrorMessageSizeExceeded (value)

Global error type within this module.

Expand source code
class ErrorMessageSizeExceeded(ResponseMessageError): pass

Ancestors

class ErrorMessageTrackingNoSuchDomain (value)

Global error type within this module.

Expand source code
class ErrorMessageTrackingNoSuchDomain(ResponseMessageError): pass

Ancestors

class ErrorMessageTrackingPermanentError (value)

Global error type within this module.

Expand source code
class ErrorMessageTrackingPermanentError(ResponseMessageError): pass

Ancestors

class ErrorMessageTrackingTransientError (value)

Global error type within this module.

Expand source code
class ErrorMessageTrackingTransientError(ResponseMessageError): pass

Ancestors

class ErrorMimeContentConversionFailed (value)

Global error type within this module.

Expand source code
class ErrorMimeContentConversionFailed(ResponseMessageError): pass

Ancestors

class ErrorMimeContentInvalid (value)

Global error type within this module.

Expand source code
class ErrorMimeContentInvalid(ResponseMessageError): pass

Ancestors

class ErrorMimeContentInvalidBase64String (value)

Global error type within this module.

Expand source code
class ErrorMimeContentInvalidBase64String(ResponseMessageError): pass

Ancestors

class ErrorMissedNotificationEvents (value)

Global error type within this module.

Expand source code
class ErrorMissedNotificationEvents(ResponseMessageError): pass

Ancestors

class ErrorMissingArgument (value)

Global error type within this module.

Expand source code
class ErrorMissingArgument(ResponseMessageError): pass

Ancestors

class ErrorMissingEmailAddress (value)

Global error type within this module.

Expand source code
class ErrorMissingEmailAddress(ResponseMessageError): pass

Ancestors

class ErrorMissingEmailAddressForManagedFolder (value)

Global error type within this module.

Expand source code
class ErrorMissingEmailAddressForManagedFolder(ResponseMessageError): pass

Ancestors

class ErrorMissingInformationEmailAddress (value)

Global error type within this module.

Expand source code
class ErrorMissingInformationEmailAddress(ResponseMessageError): pass

Ancestors

class ErrorMissingInformationReferenceItemId (value)

Global error type within this module.

Expand source code
class ErrorMissingInformationReferenceItemId(ResponseMessageError): pass

Ancestors

class ErrorMissingInformationSharingFolderId (value)

Global error type within this module.

Expand source code
class ErrorMissingInformationSharingFolderId(ResponseMessageError): pass

Ancestors

class ErrorMissingItemForCreateItemAttachment (value)

Global error type within this module.

Expand source code
class ErrorMissingItemForCreateItemAttachment(ResponseMessageError): pass

Ancestors

class ErrorMissingManagedFolderId (value)

Global error type within this module.

Expand source code
class ErrorMissingManagedFolderId(ResponseMessageError): pass

Ancestors

class ErrorMissingRecipients (value)

Global error type within this module.

Expand source code
class ErrorMissingRecipients(ResponseMessageError): pass

Ancestors

class ErrorMissingUserIdInformation (value)

Global error type within this module.

Expand source code
class ErrorMissingUserIdInformation(ResponseMessageError): pass

Ancestors

class ErrorMoreThanOneAccessModeSpecified (value)

Global error type within this module.

Expand source code
class ErrorMoreThanOneAccessModeSpecified(ResponseMessageError): pass

Ancestors

class ErrorMoveCopyFailed (value)

Global error type within this module.

Expand source code
class ErrorMoveCopyFailed(ResponseMessageError): pass

Ancestors

class ErrorMoveDistinguishedFolder (value)

Global error type within this module.

Expand source code
class ErrorMoveDistinguishedFolder(ResponseMessageError): pass

Ancestors

class ErrorNameResolutionMultipleResults (value)

Global error type within this module.

Expand source code
class ErrorNameResolutionMultipleResults(ResponseMessageError): pass

Ancestors

class ErrorNameResolutionNoMailbox (value)

Global error type within this module.

Expand source code
class ErrorNameResolutionNoMailbox(ResponseMessageError): pass

Ancestors

class ErrorNameResolutionNoResults (value)

Global error type within this module.

Expand source code
class ErrorNameResolutionNoResults(ResponseMessageError): pass

Ancestors

class ErrorNewEventStreamConnectionOpened (value)

Global error type within this module.

Expand source code
class ErrorNewEventStreamConnectionOpened(ResponseMessageError): pass

Ancestors

class ErrorNoApplicableProxyCASServersAvailable (value)

Global error type within this module.

Expand source code
class ErrorNoApplicableProxyCASServersAvailable(ResponseMessageError): pass

Ancestors

class ErrorNoCalendar (value)

Global error type within this module.

Expand source code
class ErrorNoCalendar(ResponseMessageError): pass

Ancestors

class ErrorNoDestinationCASDueToKerberosRequirements (value)

Global error type within this module.

Expand source code
class ErrorNoDestinationCASDueToKerberosRequirements(ResponseMessageError): pass

Ancestors

class ErrorNoDestinationCASDueToSSLRequirements (value)

Global error type within this module.

Expand source code
class ErrorNoDestinationCASDueToSSLRequirements(ResponseMessageError): pass

Ancestors

class ErrorNoDestinationCASDueToVersionMismatch (value)

Global error type within this module.

Expand source code
class ErrorNoDestinationCASDueToVersionMismatch(ResponseMessageError): pass

Ancestors

class ErrorNoFolderClassOverride (value)

Global error type within this module.

Expand source code
class ErrorNoFolderClassOverride(ResponseMessageError): pass

Ancestors

class ErrorNoFreeBusyAccess (value)

Global error type within this module.

Expand source code
class ErrorNoFreeBusyAccess(ResponseMessageError): pass

Ancestors

class ErrorNoPropertyTagForCustomProperties (value)

Global error type within this module.

Expand source code
class ErrorNoPropertyTagForCustomProperties(ResponseMessageError): pass

Ancestors

class ErrorNoPublicFolderReplicaAvailable (value)

Global error type within this module.

Expand source code
class ErrorNoPublicFolderReplicaAvailable(ResponseMessageError): pass

Ancestors

class ErrorNoPublicFolderServerAvailable (value)

Global error type within this module.

Expand source code
class ErrorNoPublicFolderServerAvailable(ResponseMessageError): pass

Ancestors

class ErrorNoRespondingCASInDestinationSite (value)

Global error type within this module.

Expand source code
class ErrorNoRespondingCASInDestinationSite(ResponseMessageError): pass

Ancestors

class ErrorNonExistentMailbox (value)

Global error type within this module.

Expand source code
class ErrorNonExistentMailbox(ResponseMessageError): pass

Ancestors

class ErrorNonPrimarySmtpAddress (value)

Global error type within this module.

Expand source code
class ErrorNonPrimarySmtpAddress(ResponseMessageError): pass

Ancestors

class ErrorNotAllowedExternalSharingByPolicy (value)

Global error type within this module.

Expand source code
class ErrorNotAllowedExternalSharingByPolicy(ResponseMessageError): pass

Ancestors

class ErrorNotDelegate (value)

Global error type within this module.

Expand source code
class ErrorNotDelegate(ResponseMessageError): pass

Ancestors

class ErrorNotEnoughMemory (value)

Global error type within this module.

Expand source code
class ErrorNotEnoughMemory(ResponseMessageError): pass

Ancestors

class ErrorNotSupportedSharingMessage (value)

Global error type within this module.

Expand source code
class ErrorNotSupportedSharingMessage(ResponseMessageError): pass

Ancestors

class ErrorObjectTypeChanged (value)

Global error type within this module.

Expand source code
class ErrorObjectTypeChanged(ResponseMessageError): pass

Ancestors

class ErrorOccurrenceCrossingBoundary (value)

Global error type within this module.

Expand source code
class ErrorOccurrenceCrossingBoundary(ResponseMessageError): pass

Ancestors

class ErrorOccurrenceTimeSpanTooBig (value)

Global error type within this module.

Expand source code
class ErrorOccurrenceTimeSpanTooBig(ResponseMessageError): pass

Ancestors

class ErrorOperationNotAllowedWithPublicFolderRoot (value)

Global error type within this module.

Expand source code
class ErrorOperationNotAllowedWithPublicFolderRoot(ResponseMessageError): pass

Ancestors

class ErrorOrganizationNotFederated (value)

Global error type within this module.

Expand source code
class ErrorOrganizationNotFederated(ResponseMessageError): pass

Ancestors

class ErrorOutlookRuleBlobExists (value)

Global error type within this module.

Expand source code
class ErrorOutlookRuleBlobExists(ResponseMessageError): pass

Ancestors

class ErrorParentFolderIdRequired (value)

Global error type within this module.

Expand source code
class ErrorParentFolderIdRequired(ResponseMessageError): pass

Ancestors

class ErrorParentFolderNotFound (value)

Global error type within this module.

Expand source code
class ErrorParentFolderNotFound(ResponseMessageError): pass

Ancestors

class ErrorPasswordChangeRequired (value)

Global error type within this module.

Expand source code
class ErrorPasswordChangeRequired(ResponseMessageError): pass

Ancestors

class ErrorPasswordExpired (value)

Global error type within this module.

Expand source code
class ErrorPasswordExpired(ResponseMessageError): pass

Ancestors

class ErrorPermissionNotAllowedByPolicy (value)

Global error type within this module.

Expand source code
class ErrorPermissionNotAllowedByPolicy(ResponseMessageError): pass

Ancestors

class ErrorPhoneNumberNotDialable (value)

Global error type within this module.

Expand source code
class ErrorPhoneNumberNotDialable(ResponseMessageError): pass

Ancestors

class ErrorPropertyUpdate (value)

Global error type within this module.

Expand source code
class ErrorPropertyUpdate(ResponseMessageError): pass

Ancestors

class ErrorPropertyValidationFailure (value)

Global error type within this module.

Expand source code
class ErrorPropertyValidationFailure(ResponseMessageError): pass

Ancestors

class ErrorProxiedSubscriptionCallFailure (value)

Global error type within this module.

Expand source code
class ErrorProxiedSubscriptionCallFailure(ResponseMessageError): pass

Ancestors

class ErrorProxyCallFailed (value)

Global error type within this module.

Expand source code
class ErrorProxyCallFailed(ResponseMessageError): pass

Ancestors

class ErrorProxyGroupSidLimitExceeded (value)

Global error type within this module.

Expand source code
class ErrorProxyGroupSidLimitExceeded(ResponseMessageError): pass

Ancestors

class ErrorProxyRequestNotAllowed (value)

Global error type within this module.

Expand source code
class ErrorProxyRequestNotAllowed(ResponseMessageError): pass

Ancestors

class ErrorProxyRequestProcessingFailed (value)

Global error type within this module.

Expand source code
class ErrorProxyRequestProcessingFailed(ResponseMessageError): pass

Ancestors

class ErrorProxyServiceDiscoveryFailed (value)

Global error type within this module.

Expand source code
class ErrorProxyServiceDiscoveryFailed(ResponseMessageError): pass

Ancestors

class ErrorProxyTokenExpired (value)

Global error type within this module.

Expand source code
class ErrorProxyTokenExpired(ResponseMessageError): pass

Ancestors

class ErrorPublicFolderRequestProcessingFailed (value)

Global error type within this module.

Expand source code
class ErrorPublicFolderRequestProcessingFailed(ResponseMessageError): pass

Ancestors

class ErrorPublicFolderServerNotFound (value)

Global error type within this module.

Expand source code
class ErrorPublicFolderServerNotFound(ResponseMessageError): pass

Ancestors

class ErrorQueryFilterTooLong (value)

Global error type within this module.

Expand source code
class ErrorQueryFilterTooLong(ResponseMessageError): pass

Ancestors

class ErrorQuotaExceeded (value)

Global error type within this module.

Expand source code
class ErrorQuotaExceeded(ResponseMessageError): pass

Ancestors

class ErrorReadEventsFailed (value)

Global error type within this module.

Expand source code
class ErrorReadEventsFailed(ResponseMessageError): pass

Ancestors

class ErrorReadReceiptNotPending (value)

Global error type within this module.

Expand source code
class ErrorReadReceiptNotPending(ResponseMessageError): pass

Ancestors

class ErrorRecurrenceEndDateTooBig (value)

Global error type within this module.

Expand source code
class ErrorRecurrenceEndDateTooBig(ResponseMessageError): pass

Ancestors

class ErrorRecurrenceHasNoOccurrence (value)

Global error type within this module.

Expand source code
class ErrorRecurrenceHasNoOccurrence(ResponseMessageError): pass

Ancestors

class ErrorRemoveDelegatesFailed (value)

Global error type within this module.

Expand source code
class ErrorRemoveDelegatesFailed(ResponseMessageError): pass

Ancestors

class ErrorRequestAborted (value)

Global error type within this module.

Expand source code
class ErrorRequestAborted(ResponseMessageError): pass

Ancestors

class ErrorRequestStreamTooBig (value)

Global error type within this module.

Expand source code
class ErrorRequestStreamTooBig(ResponseMessageError): pass

Ancestors

class ErrorRequiredPropertyMissing (value)

Global error type within this module.

Expand source code
class ErrorRequiredPropertyMissing(ResponseMessageError): pass

Ancestors

class ErrorResolveNamesInvalidFolderType (value)

Global error type within this module.

Expand source code
class ErrorResolveNamesInvalidFolderType(ResponseMessageError): pass

Ancestors

class ErrorResolveNamesOnlyOneContactsFolderAllowed (value)

Global error type within this module.

Expand source code
class ErrorResolveNamesOnlyOneContactsFolderAllowed(ResponseMessageError): pass

Ancestors

class ErrorResponseSchemaValidation (value)

Global error type within this module.

Expand source code
class ErrorResponseSchemaValidation(ResponseMessageError): pass

Ancestors

class ErrorRestrictionTooComplex (value)

Global error type within this module.

Expand source code
class ErrorRestrictionTooComplex(ResponseMessageError): pass

Ancestors

class ErrorRestrictionTooLong (value)

Global error type within this module.

Expand source code
class ErrorRestrictionTooLong(ResponseMessageError): pass

Ancestors

class ErrorResultSetTooBig (value)

Global error type within this module.

Expand source code
class ErrorResultSetTooBig(ResponseMessageError): pass

Ancestors

class ErrorRulesOverQuota (value)

Global error type within this module.

Expand source code
class ErrorRulesOverQuota(ResponseMessageError): pass

Ancestors

class ErrorSavedItemFolderNotFound (value)

Global error type within this module.

Expand source code
class ErrorSavedItemFolderNotFound(ResponseMessageError): pass

Ancestors

class ErrorSchemaValidation (value)

Global error type within this module.

Expand source code
class ErrorSchemaValidation(ResponseMessageError): pass

Ancestors

class ErrorSearchFolderNotInitialized (value)

Global error type within this module.

Expand source code
class ErrorSearchFolderNotInitialized(ResponseMessageError): pass

Ancestors

class ErrorSendAsDenied (value)

Global error type within this module.

Expand source code
class ErrorSendAsDenied(ResponseMessageError): pass

Ancestors

class ErrorSendMeetingCancellationsRequired (value)

Global error type within this module.

Expand source code
class ErrorSendMeetingCancellationsRequired(ResponseMessageError): pass

Ancestors

class ErrorSendMeetingInvitationsOrCancellationsRequired (value)

Global error type within this module.

Expand source code
class ErrorSendMeetingInvitationsOrCancellationsRequired(ResponseMessageError): pass

Ancestors

class ErrorSendMeetingInvitationsRequired (value)

Global error type within this module.

Expand source code
class ErrorSendMeetingInvitationsRequired(ResponseMessageError): pass

Ancestors

class ErrorSentMeetingRequestUpdate (value)

Global error type within this module.

Expand source code
class ErrorSentMeetingRequestUpdate(ResponseMessageError): pass

Ancestors

class ErrorSentTaskRequestUpdate (value)

Global error type within this module.

Expand source code
class ErrorSentTaskRequestUpdate(ResponseMessageError): pass

Ancestors

class ErrorServerBusy (*args, **kwargs)

Global error type within this module.

Expand source code
class ErrorServerBusy(ResponseMessageError):
    def __init__(self, *args, **kwargs):
        self.back_off = kwargs.pop('back_off', None)  # Requested back off value in seconds
        super().__init__(*args, **kwargs)

Ancestors

class ErrorServiceDiscoveryFailed (value)

Global error type within this module.

Expand source code
class ErrorServiceDiscoveryFailed(ResponseMessageError): pass

Ancestors

class ErrorSharingNoExternalEwsAvailable (value)

Global error type within this module.

Expand source code
class ErrorSharingNoExternalEwsAvailable(ResponseMessageError): pass

Ancestors

class ErrorSharingSynchronizationFailed (value)

Global error type within this module.

Expand source code
class ErrorSharingSynchronizationFailed(ResponseMessageError): pass

Ancestors

class ErrorStaleObject (value)

Global error type within this module.

Expand source code
class ErrorStaleObject(ResponseMessageError): pass

Ancestors

class ErrorSubmissionQuotaExceeded (value)

Global error type within this module.

Expand source code
class ErrorSubmissionQuotaExceeded(ResponseMessageError): pass

Ancestors

class ErrorSubscriptionAccessDenied (value)

Global error type within this module.

Expand source code
class ErrorSubscriptionAccessDenied(ResponseMessageError): pass

Ancestors

class ErrorSubscriptionDelegateAccessNotSupported (value)

Global error type within this module.

Expand source code
class ErrorSubscriptionDelegateAccessNotSupported(ResponseMessageError): pass

Ancestors

class ErrorSubscriptionNotFound (value)

Global error type within this module.

Expand source code
class ErrorSubscriptionNotFound(ResponseMessageError): pass

Ancestors

class ErrorSubscriptionUnsubsribed (value)

Global error type within this module.

Expand source code
class ErrorSubscriptionUnsubsribed(ResponseMessageError): pass

Ancestors

class ErrorSyncFolderNotFound (value)

Global error type within this module.

Expand source code
class ErrorSyncFolderNotFound(ResponseMessageError): pass

Ancestors

class ErrorTimeIntervalTooBig (value)

Global error type within this module.

Expand source code
class ErrorTimeIntervalTooBig(ResponseMessageError): pass

Ancestors

class ErrorTimeZone (value)

Global error type within this module.

Expand source code
class ErrorTimeZone(ResponseMessageError): pass

Ancestors

class ErrorTimeoutExpired (value)

Global error type within this module.

Expand source code
class ErrorTimeoutExpired(ResponseMessageError): pass

Ancestors

class ErrorToFolderNotFound (value)

Global error type within this module.

Expand source code
class ErrorToFolderNotFound(ResponseMessageError): pass

Ancestors

class ErrorTokenSerializationDenied (value)

Global error type within this module.

Expand source code
class ErrorTokenSerializationDenied(ResponseMessageError): pass

Ancestors

class ErrorTooManyObjectsOpened (value)

Global error type within this module.

Expand source code
class ErrorTooManyObjectsOpened(ResponseMessageError): pass

Ancestors

class ErrorUnableToGetUserOofSettings (value)

Global error type within this module.

Expand source code
class ErrorUnableToGetUserOofSettings(ResponseMessageError): pass

Ancestors

class ErrorUnifiedMessagingDialPlanNotFound (value)

Global error type within this module.

Expand source code
class ErrorUnifiedMessagingDialPlanNotFound(ResponseMessageError): pass

Ancestors

class ErrorUnifiedMessagingRequestFailed (value)

Global error type within this module.

Expand source code
class ErrorUnifiedMessagingRequestFailed(ResponseMessageError): pass

Ancestors

class ErrorUnifiedMessagingServerNotFound (value)

Global error type within this module.

Expand source code
class ErrorUnifiedMessagingServerNotFound(ResponseMessageError): pass

Ancestors

class ErrorUnsupportedCulture (value)

Global error type within this module.

Expand source code
class ErrorUnsupportedCulture(ResponseMessageError): pass

Ancestors

class ErrorUnsupportedMapiPropertyType (value)

Global error type within this module.

Expand source code
class ErrorUnsupportedMapiPropertyType(ResponseMessageError): pass

Ancestors

class ErrorUnsupportedMimeConversion (value)

Global error type within this module.

Expand source code
class ErrorUnsupportedMimeConversion(ResponseMessageError): pass

Ancestors

class ErrorUnsupportedPathForQuery (value)

Global error type within this module.

Expand source code
class ErrorUnsupportedPathForQuery(ResponseMessageError): pass

Ancestors

class ErrorUnsupportedPathForSortGroup (value)

Global error type within this module.

Expand source code
class ErrorUnsupportedPathForSortGroup(ResponseMessageError): pass

Ancestors

class ErrorUnsupportedPropertyDefinition (value)

Global error type within this module.

Expand source code
class ErrorUnsupportedPropertyDefinition(ResponseMessageError): pass

Ancestors

class ErrorUnsupportedQueryFilter (value)

Global error type within this module.

Expand source code
class ErrorUnsupportedQueryFilter(ResponseMessageError): pass

Ancestors

class ErrorUnsupportedRecurrence (value)

Global error type within this module.

Expand source code
class ErrorUnsupportedRecurrence(ResponseMessageError): pass

Ancestors

class ErrorUnsupportedSubFilter (value)

Global error type within this module.

Expand source code
class ErrorUnsupportedSubFilter(ResponseMessageError): pass

Ancestors

class ErrorUnsupportedTypeForConversion (value)

Global error type within this module.

Expand source code
class ErrorUnsupportedTypeForConversion(ResponseMessageError): pass

Ancestors

class ErrorUpdateDelegatesFailed (value)

Global error type within this module.

Expand source code
class ErrorUpdateDelegatesFailed(ResponseMessageError): pass

Ancestors

class ErrorUpdatePropertyMismatch (value)

Global error type within this module.

Expand source code
class ErrorUpdatePropertyMismatch(ResponseMessageError): pass

Ancestors

class ErrorUserNotAllowedByPolicy (value)

Global error type within this module.

Expand source code
class ErrorUserNotAllowedByPolicy(ResponseMessageError): pass

Ancestors

class ErrorUserNotUnifiedMessagingEnabled (value)

Global error type within this module.

Expand source code
class ErrorUserNotUnifiedMessagingEnabled(ResponseMessageError): pass

Ancestors

class ErrorUserWithoutFederatedProxyAddress (value)

Global error type within this module.

Expand source code
class ErrorUserWithoutFederatedProxyAddress(ResponseMessageError): pass

Ancestors

class ErrorValueOutOfRange (value)

Global error type within this module.

Expand source code
class ErrorValueOutOfRange(ResponseMessageError): pass

Ancestors

class ErrorVirusDetected (value)

Global error type within this module.

Expand source code
class ErrorVirusDetected(ResponseMessageError): pass

Ancestors

class ErrorVirusMessageDeleted (value)

Global error type within this module.

Expand source code
class ErrorVirusMessageDeleted(ResponseMessageError): pass

Ancestors

class ErrorVoiceMailNotImplemented (value)

Global error type within this module.

Expand source code
class ErrorVoiceMailNotImplemented(ResponseMessageError): pass

Ancestors

class ErrorWebRequestInInvalidState (value)

Global error type within this module.

Expand source code
class ErrorWebRequestInInvalidState(ResponseMessageError): pass

Ancestors

class ErrorWin32InteropError (value)

Global error type within this module.

Expand source code
class ErrorWin32InteropError(ResponseMessageError): pass

Ancestors

class ErrorWorkingHoursSaveFailed (value)

Global error type within this module.

Expand source code
class ErrorWorkingHoursSaveFailed(ResponseMessageError): pass

Ancestors

class ErrorWorkingHoursXmlMalformed (value)

Global error type within this module.

Expand source code
class ErrorWorkingHoursXmlMalformed(ResponseMessageError): pass

Ancestors

class ErrorWrongServerVersion (value)

Global error type within this module.

Expand source code
class ErrorWrongServerVersion(ResponseMessageError): pass

Ancestors

class ErrorWrongServerVersionDelegate (value)

Global error type within this module.

Expand source code
class ErrorWrongServerVersionDelegate(ResponseMessageError): pass

Ancestors

class MalformedResponseError (value)

Global error type within this module.

Expand source code
class MalformedResponseError(TransportError):
    pass

Ancestors

class MultipleObjectsReturned (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code
class MultipleObjectsReturned(Exception):
    pass

Ancestors

  • builtins.Exception
  • builtins.BaseException
class NaiveDateTimeNotAllowed (local_dt)

Inappropriate argument value (of correct type).

Expand source code
class NaiveDateTimeNotAllowed(ValueError):
    def __init__(self, local_dt):
        super().__init__()
        from .ewsdatetime import EWSDateTime
        if not isinstance(local_dt, EWSDateTime):
            raise ValueError("'local_dt' value %r must be an EWSDateTime" % local_dt)
        self.local_dt = local_dt

Ancestors

  • builtins.ValueError
  • builtins.Exception
  • builtins.BaseException
class RateLimitError (value, url, status_code, total_wait)

Global error type within this module.

Expand source code
class RateLimitError(TransportError):
    def __init__(self, value, url, status_code, total_wait):
        super().__init__(value)
        self.url = url
        self.status_code = status_code
        self.total_wait = total_wait

    def __str__(self):
        return str(
            '{value} (gave up after {total_wait:.3f} seconds. URL {url} returned status code {status_code})'.format(
                value=self.value, url=self.url, status_code=self.status_code, total_wait=self.total_wait)
        )

Ancestors

class RedirectError (url)

Global error type within this module.

Expand source code
class RedirectError(TransportError):
    def __init__(self, url):
        parsed_url = urlparse(url)
        self.url = url
        self.server = parsed_url.hostname.lower()
        self.has_ssl = parsed_url.scheme == 'https'
        super().__init__(str(self))

    def __str__(self):
        return 'We were redirected to %s' % self.url

Ancestors

class RelativeRedirect (value)

Global error type within this module.

Expand source code
class RelativeRedirect(TransportError):
    pass

Ancestors

class ResponseMessageError (value)

Global error type within this module.

Expand source code
class ResponseMessageError(TransportError):
    pass

Ancestors

Subclasses

class SOAPError (value)

Global error type within this module.

Expand source code
class SOAPError(TransportError):
    pass

Ancestors

class SessionPoolMaxSizeReached (value)

Global error type within this module.

Expand source code
class SessionPoolMaxSizeReached(EWSError):
    pass

Ancestors

  • EWSError
  • builtins.Exception
  • builtins.BaseException
class SessionPoolMinSizeReached (value)

Global error type within this module.

Expand source code
class SessionPoolMinSizeReached(EWSError):
    pass

Ancestors

  • EWSError
  • builtins.Exception
  • builtins.BaseException
class TimezoneDefinitionInvalidForYear (value)

Global error type within this module.

Expand source code
class TimezoneDefinitionInvalidForYear(EWSError):
    pass

Ancestors

  • EWSError
  • builtins.Exception
  • builtins.BaseException
class TransportError (value)

Global error type within this module.

Expand source code
class TransportError(EWSError):
    pass

Ancestors

  • EWSError
  • builtins.Exception
  • builtins.BaseException

Subclasses

class UnauthorizedError (value)

Global error type within this module.

Expand source code
class UnauthorizedError(EWSError):
    pass

Ancestors

  • EWSError
  • builtins.Exception
  • builtins.BaseException
class UnknownTimeZone (value)

Global error type within this module.

Expand source code
class UnknownTimeZone(EWSError):
    pass

Ancestors

  • EWSError
  • builtins.Exception
  • builtins.BaseException
exchangelib-4.6.1/docs/exchangelib/ewsdatetime.html000066400000000000000000001557501414601472700224200ustar00rootroot00000000000000 exchangelib.ewsdatetime API documentation

Module exchangelib.ewsdatetime

Expand source code
import datetime
import logging
import warnings

try:
    import zoneinfo
except ImportError:
    from backports import zoneinfo
import tzlocal

from .errors import NaiveDateTimeNotAllowed, UnknownTimeZone
from .winzone import IANA_TO_MS_TIMEZONE_MAP, MS_TIMEZONE_TO_IANA_MAP

log = logging.getLogger(__name__)


class EWSDate(datetime.date):
    """Extends the normal date implementation to satisfy EWS."""

    __slots__ = '_year', '_month', '_day', '_hashcode'

    def ewsformat(self):
        """ISO 8601 format to satisfy xs:date as interpreted by EWS. Example: 2009-01-15."""
        return self.isoformat()

    def __add__(self, other):
        dt = super().__add__(other)
        if isinstance(dt, self.__class__):
            return dt
        return self.from_date(dt)  # We want to return EWSDate objects

    def __iadd__(self, other):
        return self + other

    def __sub__(self, other):
        dt = super().__sub__(other)
        if isinstance(dt, datetime.timedelta):
            return dt
        if isinstance(dt, self.__class__):
            return dt
        return self.from_date(dt)  # We want to return EWSDate objects

    def __isub__(self, other):
        return self - other

    @classmethod
    def fromordinal(cls, n):
        dt = super().fromordinal(n)
        if isinstance(dt, cls):
            return dt
        return cls.from_date(dt)  # We want to return EWSDate objects

    @classmethod
    def from_date(cls, d):
        if type(d) is not datetime.date:
            raise ValueError("%r must be a date instance" % d)
        return cls(d.year, d.month, d.day)

    @classmethod
    def from_string(cls, date_string):
        # Sometimes, we'll receive a date string with timezone information. Not very useful.
        if date_string.endswith('Z'):
            date_fmt = '%Y-%m-%dZ'
        elif ':' in date_string:
            if '+' in date_string:
                date_fmt = '%Y-%m-%d+%H:%M'
            else:
                date_fmt = '%Y-%m-%d-%H:%M'
        else:
            date_fmt = '%Y-%m-%d'
        d = datetime.datetime.strptime(date_string, date_fmt).date()
        if isinstance(d, cls):
            return d
        return cls.from_date(d)  # We want to return EWSDate objects


class EWSDateTime(datetime.datetime):
    """Extends the normal datetime implementation to satisfy EWS."""

    __slots__ = '_year', '_month', '_day', '_hour', '_minute', '_second', '_microsecond', '_tzinfo', '_hashcode'

    def __new__(cls, *args, **kwargs):
        # pylint: disable=arguments-differ

        if len(args) == 8:
            tzinfo = args[7]
        else:
            tzinfo = kwargs.get('tzinfo')
        if isinstance(tzinfo, zoneinfo.ZoneInfo):
            # Don't allow pytz or dateutil timezones here. They are not safe to use as direct input for datetime()
            tzinfo = EWSTimeZone.from_timezone(tzinfo)
        if not isinstance(tzinfo, (EWSTimeZone, type(None))):
            raise ValueError('tzinfo %r must be an EWSTimeZone instance' % tzinfo)
        if len(args) == 8:
            args = args[:7] + (tzinfo,)
        else:
            kwargs['tzinfo'] = tzinfo
        return super().__new__(cls, *args, **kwargs)

    def ewsformat(self):
        """ISO 8601 format to satisfy xs:datetime as interpreted by EWS. Examples:
        * 2009-01-15T13:45:56Z
        * 2009-01-15T13:45:56+01:00
        """
        if not self.tzinfo:
            raise ValueError('%r must be timezone-aware' % self)
        if self.tzinfo.key == 'UTC':
            if self.microsecond:
                return self.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
            return self.strftime('%Y-%m-%dT%H:%M:%SZ')
        return self.isoformat()

    @classmethod
    def from_datetime(cls, d):
        if type(d) is not datetime.datetime:
            raise ValueError("%r must be a datetime instance" % d)
        if d.tzinfo is None:
            tz = None
        elif isinstance(d.tzinfo, EWSTimeZone):
            tz = d.tzinfo
        else:
            tz = EWSTimeZone.from_timezone(d.tzinfo)
        return cls(d.year, d.month, d.day, d.hour, d.minute, d.second, d.microsecond, tzinfo=tz)

    def astimezone(self, tz=None):
        if tz is None:
            tz = EWSTimeZone.localzone()
        t = super().astimezone(tz=tz).replace(tzinfo=tz)
        if isinstance(t, self.__class__):
            return t
        return self.from_datetime(t)  # We want to return EWSDateTime objects

    def __add__(self, other):
        t = super().__add__(other)
        if isinstance(t, self.__class__):
            return t
        return self.from_datetime(t)  # We want to return EWSDateTime objects

    def __iadd__(self, other):
        return self + other

    def __sub__(self, other):
        t = super().__sub__(other)
        if isinstance(t, datetime.timedelta):
            return t
        if isinstance(t, self.__class__):
            return t
        return self.from_datetime(t)  # We want to return EWSDateTime objects

    def __isub__(self, other):
        return self - other

    @classmethod
    def from_string(cls, date_string):
        # Parses several common datetime formats and returns timezone-aware EWSDateTime objects
        if date_string.endswith('Z'):
            # UTC datetime
            return super().strptime(date_string, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC)
        if len(date_string) == 19:
            # This is probably a naive datetime. Don't allow this, but signal caller with an appropriate error
            local_dt = super().strptime(date_string, '%Y-%m-%dT%H:%M:%S')
            raise NaiveDateTimeNotAllowed(local_dt)
        # This is probably a datetime value with timezone information. This comes in the form '+/-HH:MM'.
        aware_dt = datetime.datetime.fromisoformat(date_string).astimezone(UTC).replace(tzinfo=UTC)
        if isinstance(aware_dt, cls):
            return aware_dt
        return cls.from_datetime(aware_dt)

    @classmethod
    def fromtimestamp(cls, t, tz=None):
        dt = super().fromtimestamp(t, tz=tz)
        if isinstance(dt, cls):
            return dt
        return cls.from_datetime(dt)  # We want to return EWSDateTime objects

    @classmethod
    def utcfromtimestamp(cls, t):
        dt = super().utcfromtimestamp(t)
        if isinstance(dt, cls):
            return dt
        return cls.from_datetime(dt)  # We want to return EWSDateTime objects

    @classmethod
    def now(cls, tz=None):
        t = super().now(tz=tz)
        if isinstance(t, cls):
            return t
        return cls.from_datetime(t)  # We want to return EWSDateTime objects

    @classmethod
    def utcnow(cls):
        t = super().utcnow()
        if isinstance(t, cls):
            return t
        return cls.from_datetime(t)  # We want to return EWSDateTime objects

    def date(self):
        d = super().date()
        if isinstance(d, EWSDate):
            return d
        return EWSDate.from_date(d)  # We want to return EWSDate objects


class EWSTimeZone(zoneinfo.ZoneInfo):
    """Represents a timezone as expected by the EWS TimezoneContext / TimezoneDefinition XML element, and returned by
    services.GetServerTimeZones.
    """

    IANA_TO_MS_MAP = IANA_TO_MS_TIMEZONE_MAP
    MS_TO_IANA_MAP = MS_TIMEZONE_TO_IANA_MAP

    def __new__(cls, *args, **kwargs):
        try:
            instance = super().__new__(cls, *args, **kwargs)
        except zoneinfo.ZoneInfoNotFoundError as e:
            raise UnknownTimeZone(e.args[0])
        try:
            instance.ms_id = cls.IANA_TO_MS_MAP[instance.key][0]
        except KeyError:
            raise UnknownTimeZone('No Windows timezone name found for timezone "%s"' % instance.key)

        # We don't need the Windows long-format timezone name in long format. It's used in timezone XML elements, but
        # EWS happily accepts empty strings. For a full list of timezones supported by the target server, including
        # long-format names, see output of services.GetServerTimeZones(account.protocol).call()
        instance.ms_name = ''
        return instance

    def __eq__(self, other):
        # Microsoft timezones are less granular than IANA, so an EWSTimeZone created from 'Europe/Copenhagen' may return
        # from the server as 'Europe/Copenhagen'. We're catering for Microsoft here, so base equality on the Microsoft
        # timezone ID.
        if not isinstance(other, self.__class__):
            return NotImplemented
        return self.ms_id == other.ms_id

    @classmethod
    def from_ms_id(cls, ms_id):
        # Create a timezone instance from a Microsoft timezone ID. This is lossy because there is not a 1:1 translation
        # from MS timezone ID to IANA timezone.
        try:
            return cls(cls.MS_TO_IANA_MAP[ms_id])
        except KeyError:
            if '/' in ms_id:
                # EWS sometimes returns an ID that has a region/location format, e.g. 'Europe/Copenhagen'. Try the
                # string unaltered.
                return cls(ms_id)
            raise UnknownTimeZone("Windows timezone ID '%s' is unknown by CLDR" % ms_id)

    @classmethod
    def from_pytz(cls, tz):
        return cls(tz.zone)

    @classmethod
    def from_datetime(cls, tz):
        """Convert from a standard library `datetime.timezone` instance."""
        return cls(tz.tzname(None))

    @classmethod
    def from_dateutil(cls, tz):
        # Objects returned by dateutil.tz.tzlocal() and dateutil.tz.gettz() are not supported. They
        # don't contain enough information to reliably match them with a CLDR timezone.
        if hasattr(tz, '_filename'):
            key = '/'.join(tz._filename.split('/')[-2:])
            return cls(key)
        return cls(tz.tzname(datetime.datetime.now()))

    @classmethod
    def from_zoneinfo(cls, tz):
        return cls(tz.key)

    @classmethod
    def from_timezone(cls, tz):
        # Support multiple tzinfo implementations. We could use isinstance(), but then we'd have to have pytz
        # and dateutil as dependencies for this package.
        tz_module = tz.__class__.__module__.split('.')[0]
        try:
            return {
                cls.__module__.split('.')[0]: lambda z: z,
                'backports': cls.from_zoneinfo,
                'datetime': cls.from_datetime,
                'dateutil': cls.from_dateutil,
                'pytz': cls.from_pytz,
                'zoneinfo': cls.from_zoneinfo,
                'pytz_deprecation_shim': lambda z: cls.from_timezone(z.unwrap_shim())
            }[tz_module](tz)
        except KeyError:
            raise TypeError('Unsupported tzinfo type: %r' % tz)

    @classmethod
    def localzone(cls):
        try:
            tz = tzlocal.get_localzone()
        except zoneinfo.ZoneInfoNotFoundError:
            # Older versions of tzlocal will raise a pytz exception. Let's not depend on pytz just for that.
            raise UnknownTimeZone("Failed to guess local timezone")
        # Handle both old and new versions of tzlocal that may return pytz or zoneinfo objects, respectively
        return cls.from_timezone(tz)

    @classmethod
    def timezone(cls, location):
        warnings.warn('replace EWSTimeZone.timezone() with just EWSTimeZone()', DeprecationWarning, stacklevel=2)
        return cls(location)

    def normalize(self, dt, is_dst=False):
        warnings.warn('normalization is now handled gracefully', DeprecationWarning, stacklevel=2)
        return dt

    def localize(self, dt, is_dst=False):
        warnings.warn('replace tz.localize() with dt.replace(tzinfo=tz)', DeprecationWarning, stacklevel=2)
        if dt.tzinfo is not None:
            raise ValueError('%r must be timezone-unaware' % dt)
        dt = dt.replace(tzinfo=self)
        if is_dst is not None:
            # DST dates are assumed to always be after non-DST dates
            dt_before = dt.replace(fold=0)
            dt_after = dt.replace(fold=1)
            dst_before = dt_before.dst()
            dst_after = dt_after.dst()
            if dst_before > dst_after:
                dt = dt_before if is_dst else dt_after
            elif dst_before < dst_after:
                dt = dt_after if is_dst else dt_before
        return dt

    def fromutc(self, dt):
        t = super().fromutc(dt)
        if isinstance(t, EWSDateTime):
            return t
        return EWSDateTime.from_datetime(t)  # We want to return EWSDateTime objects


UTC = EWSTimeZone('UTC')
UTC_NOW = lambda: EWSDateTime.now(tz=UTC)  # noqa: E731

Functions

def UTC_NOW()
Expand source code
UTC_NOW = lambda: EWSDateTime.now(tz=UTC)  # noqa: E731

Classes

class EWSDate (...)

Extends the normal date implementation to satisfy EWS.

Expand source code
class EWSDate(datetime.date):
    """Extends the normal date implementation to satisfy EWS."""

    __slots__ = '_year', '_month', '_day', '_hashcode'

    def ewsformat(self):
        """ISO 8601 format to satisfy xs:date as interpreted by EWS. Example: 2009-01-15."""
        return self.isoformat()

    def __add__(self, other):
        dt = super().__add__(other)
        if isinstance(dt, self.__class__):
            return dt
        return self.from_date(dt)  # We want to return EWSDate objects

    def __iadd__(self, other):
        return self + other

    def __sub__(self, other):
        dt = super().__sub__(other)
        if isinstance(dt, datetime.timedelta):
            return dt
        if isinstance(dt, self.__class__):
            return dt
        return self.from_date(dt)  # We want to return EWSDate objects

    def __isub__(self, other):
        return self - other

    @classmethod
    def fromordinal(cls, n):
        dt = super().fromordinal(n)
        if isinstance(dt, cls):
            return dt
        return cls.from_date(dt)  # We want to return EWSDate objects

    @classmethod
    def from_date(cls, d):
        if type(d) is not datetime.date:
            raise ValueError("%r must be a date instance" % d)
        return cls(d.year, d.month, d.day)

    @classmethod
    def from_string(cls, date_string):
        # Sometimes, we'll receive a date string with timezone information. Not very useful.
        if date_string.endswith('Z'):
            date_fmt = '%Y-%m-%dZ'
        elif ':' in date_string:
            if '+' in date_string:
                date_fmt = '%Y-%m-%d+%H:%M'
            else:
                date_fmt = '%Y-%m-%d-%H:%M'
        else:
            date_fmt = '%Y-%m-%d'
        d = datetime.datetime.strptime(date_string, date_fmt).date()
        if isinstance(d, cls):
            return d
        return cls.from_date(d)  # We want to return EWSDate objects

Ancestors

  • datetime.date

Static methods

def from_date(d)
Expand source code
@classmethod
def from_date(cls, d):
    if type(d) is not datetime.date:
        raise ValueError("%r must be a date instance" % d)
    return cls(d.year, d.month, d.day)
def from_string(date_string)
Expand source code
@classmethod
def from_string(cls, date_string):
    # Sometimes, we'll receive a date string with timezone information. Not very useful.
    if date_string.endswith('Z'):
        date_fmt = '%Y-%m-%dZ'
    elif ':' in date_string:
        if '+' in date_string:
            date_fmt = '%Y-%m-%d+%H:%M'
        else:
            date_fmt = '%Y-%m-%d-%H:%M'
    else:
        date_fmt = '%Y-%m-%d'
    d = datetime.datetime.strptime(date_string, date_fmt).date()
    if isinstance(d, cls):
        return d
    return cls.from_date(d)  # We want to return EWSDate objects
def fromordinal(n)

int -> date corresponding to a proleptic Gregorian ordinal.

Expand source code
@classmethod
def fromordinal(cls, n):
    dt = super().fromordinal(n)
    if isinstance(dt, cls):
        return dt
    return cls.from_date(dt)  # We want to return EWSDate objects

Methods

def ewsformat(self)

ISO 8601 format to satisfy xs:date as interpreted by EWS. Example: 2009-01-15.

Expand source code
def ewsformat(self):
    """ISO 8601 format to satisfy xs:date as interpreted by EWS. Example: 2009-01-15."""
    return self.isoformat()
class EWSDateTime (*args, **kwargs)

Extends the normal datetime implementation to satisfy EWS.

Expand source code
class EWSDateTime(datetime.datetime):
    """Extends the normal datetime implementation to satisfy EWS."""

    __slots__ = '_year', '_month', '_day', '_hour', '_minute', '_second', '_microsecond', '_tzinfo', '_hashcode'

    def __new__(cls, *args, **kwargs):
        # pylint: disable=arguments-differ

        if len(args) == 8:
            tzinfo = args[7]
        else:
            tzinfo = kwargs.get('tzinfo')
        if isinstance(tzinfo, zoneinfo.ZoneInfo):
            # Don't allow pytz or dateutil timezones here. They are not safe to use as direct input for datetime()
            tzinfo = EWSTimeZone.from_timezone(tzinfo)
        if not isinstance(tzinfo, (EWSTimeZone, type(None))):
            raise ValueError('tzinfo %r must be an EWSTimeZone instance' % tzinfo)
        if len(args) == 8:
            args = args[:7] + (tzinfo,)
        else:
            kwargs['tzinfo'] = tzinfo
        return super().__new__(cls, *args, **kwargs)

    def ewsformat(self):
        """ISO 8601 format to satisfy xs:datetime as interpreted by EWS. Examples:
        * 2009-01-15T13:45:56Z
        * 2009-01-15T13:45:56+01:00
        """
        if not self.tzinfo:
            raise ValueError('%r must be timezone-aware' % self)
        if self.tzinfo.key == 'UTC':
            if self.microsecond:
                return self.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
            return self.strftime('%Y-%m-%dT%H:%M:%SZ')
        return self.isoformat()

    @classmethod
    def from_datetime(cls, d):
        if type(d) is not datetime.datetime:
            raise ValueError("%r must be a datetime instance" % d)
        if d.tzinfo is None:
            tz = None
        elif isinstance(d.tzinfo, EWSTimeZone):
            tz = d.tzinfo
        else:
            tz = EWSTimeZone.from_timezone(d.tzinfo)
        return cls(d.year, d.month, d.day, d.hour, d.minute, d.second, d.microsecond, tzinfo=tz)

    def astimezone(self, tz=None):
        if tz is None:
            tz = EWSTimeZone.localzone()
        t = super().astimezone(tz=tz).replace(tzinfo=tz)
        if isinstance(t, self.__class__):
            return t
        return self.from_datetime(t)  # We want to return EWSDateTime objects

    def __add__(self, other):
        t = super().__add__(other)
        if isinstance(t, self.__class__):
            return t
        return self.from_datetime(t)  # We want to return EWSDateTime objects

    def __iadd__(self, other):
        return self + other

    def __sub__(self, other):
        t = super().__sub__(other)
        if isinstance(t, datetime.timedelta):
            return t
        if isinstance(t, self.__class__):
            return t
        return self.from_datetime(t)  # We want to return EWSDateTime objects

    def __isub__(self, other):
        return self - other

    @classmethod
    def from_string(cls, date_string):
        # Parses several common datetime formats and returns timezone-aware EWSDateTime objects
        if date_string.endswith('Z'):
            # UTC datetime
            return super().strptime(date_string, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC)
        if len(date_string) == 19:
            # This is probably a naive datetime. Don't allow this, but signal caller with an appropriate error
            local_dt = super().strptime(date_string, '%Y-%m-%dT%H:%M:%S')
            raise NaiveDateTimeNotAllowed(local_dt)
        # This is probably a datetime value with timezone information. This comes in the form '+/-HH:MM'.
        aware_dt = datetime.datetime.fromisoformat(date_string).astimezone(UTC).replace(tzinfo=UTC)
        if isinstance(aware_dt, cls):
            return aware_dt
        return cls.from_datetime(aware_dt)

    @classmethod
    def fromtimestamp(cls, t, tz=None):
        dt = super().fromtimestamp(t, tz=tz)
        if isinstance(dt, cls):
            return dt
        return cls.from_datetime(dt)  # We want to return EWSDateTime objects

    @classmethod
    def utcfromtimestamp(cls, t):
        dt = super().utcfromtimestamp(t)
        if isinstance(dt, cls):
            return dt
        return cls.from_datetime(dt)  # We want to return EWSDateTime objects

    @classmethod
    def now(cls, tz=None):
        t = super().now(tz=tz)
        if isinstance(t, cls):
            return t
        return cls.from_datetime(t)  # We want to return EWSDateTime objects

    @classmethod
    def utcnow(cls):
        t = super().utcnow()
        if isinstance(t, cls):
            return t
        return cls.from_datetime(t)  # We want to return EWSDateTime objects

    def date(self):
        d = super().date()
        if isinstance(d, EWSDate):
            return d
        return EWSDate.from_date(d)  # We want to return EWSDate objects

Ancestors

  • datetime.datetime
  • datetime.date

Static methods

def from_datetime(d)
Expand source code
@classmethod
def from_datetime(cls, d):
    if type(d) is not datetime.datetime:
        raise ValueError("%r must be a datetime instance" % d)
    if d.tzinfo is None:
        tz = None
    elif isinstance(d.tzinfo, EWSTimeZone):
        tz = d.tzinfo
    else:
        tz = EWSTimeZone.from_timezone(d.tzinfo)
    return cls(d.year, d.month, d.day, d.hour, d.minute, d.second, d.microsecond, tzinfo=tz)
def from_string(date_string)
Expand source code
@classmethod
def from_string(cls, date_string):
    # Parses several common datetime formats and returns timezone-aware EWSDateTime objects
    if date_string.endswith('Z'):
        # UTC datetime
        return super().strptime(date_string, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC)
    if len(date_string) == 19:
        # This is probably a naive datetime. Don't allow this, but signal caller with an appropriate error
        local_dt = super().strptime(date_string, '%Y-%m-%dT%H:%M:%S')
        raise NaiveDateTimeNotAllowed(local_dt)
    # This is probably a datetime value with timezone information. This comes in the form '+/-HH:MM'.
    aware_dt = datetime.datetime.fromisoformat(date_string).astimezone(UTC).replace(tzinfo=UTC)
    if isinstance(aware_dt, cls):
        return aware_dt
    return cls.from_datetime(aware_dt)
def fromtimestamp(t, tz=None)

timestamp[, tz] -> tz's local time from POSIX timestamp.

Expand source code
@classmethod
def fromtimestamp(cls, t, tz=None):
    dt = super().fromtimestamp(t, tz=tz)
    if isinstance(dt, cls):
        return dt
    return cls.from_datetime(dt)  # We want to return EWSDateTime objects
def now(tz=None)

Returns new datetime object representing current time local to tz.

tz Timezone object.

If no tz is specified, uses local timezone.

Expand source code
@classmethod
def now(cls, tz=None):
    t = super().now(tz=tz)
    if isinstance(t, cls):
        return t
    return cls.from_datetime(t)  # We want to return EWSDateTime objects
def utcfromtimestamp(t)

Construct a naive UTC datetime from a POSIX timestamp.

Expand source code
@classmethod
def utcfromtimestamp(cls, t):
    dt = super().utcfromtimestamp(t)
    if isinstance(dt, cls):
        return dt
    return cls.from_datetime(dt)  # We want to return EWSDateTime objects
def utcnow()

Return a new datetime representing UTC day and time.

Expand source code
@classmethod
def utcnow(cls):
    t = super().utcnow()
    if isinstance(t, cls):
        return t
    return cls.from_datetime(t)  # We want to return EWSDateTime objects

Methods

def astimezone(self, tz=None)

tz -> convert to local time in new timezone tz

Expand source code
def astimezone(self, tz=None):
    if tz is None:
        tz = EWSTimeZone.localzone()
    t = super().astimezone(tz=tz).replace(tzinfo=tz)
    if isinstance(t, self.__class__):
        return t
    return self.from_datetime(t)  # We want to return EWSDateTime objects
def date(self)

Return date object with same year, month and day.

Expand source code
def date(self):
    d = super().date()
    if isinstance(d, EWSDate):
        return d
    return EWSDate.from_date(d)  # We want to return EWSDate objects
def ewsformat(self)

ISO 8601 format to satisfy xs:datetime as interpreted by EWS. Examples: * 2009-01-15T13:45:56Z * 2009-01-15T13:45:56+01:00

Expand source code
def ewsformat(self):
    """ISO 8601 format to satisfy xs:datetime as interpreted by EWS. Examples:
    * 2009-01-15T13:45:56Z
    * 2009-01-15T13:45:56+01:00
    """
    if not self.tzinfo:
        raise ValueError('%r must be timezone-aware' % self)
    if self.tzinfo.key == 'UTC':
        if self.microsecond:
            return self.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
        return self.strftime('%Y-%m-%dT%H:%M:%SZ')
    return self.isoformat()
class EWSTimeZone (*args, **kwargs)

Represents a timezone as expected by the EWS TimezoneContext / TimezoneDefinition XML element, and returned by services.GetServerTimeZones.

Expand source code
class EWSTimeZone(zoneinfo.ZoneInfo):
    """Represents a timezone as expected by the EWS TimezoneContext / TimezoneDefinition XML element, and returned by
    services.GetServerTimeZones.
    """

    IANA_TO_MS_MAP = IANA_TO_MS_TIMEZONE_MAP
    MS_TO_IANA_MAP = MS_TIMEZONE_TO_IANA_MAP

    def __new__(cls, *args, **kwargs):
        try:
            instance = super().__new__(cls, *args, **kwargs)
        except zoneinfo.ZoneInfoNotFoundError as e:
            raise UnknownTimeZone(e.args[0])
        try:
            instance.ms_id = cls.IANA_TO_MS_MAP[instance.key][0]
        except KeyError:
            raise UnknownTimeZone('No Windows timezone name found for timezone "%s"' % instance.key)

        # We don't need the Windows long-format timezone name in long format. It's used in timezone XML elements, but
        # EWS happily accepts empty strings. For a full list of timezones supported by the target server, including
        # long-format names, see output of services.GetServerTimeZones(account.protocol).call()
        instance.ms_name = ''
        return instance

    def __eq__(self, other):
        # Microsoft timezones are less granular than IANA, so an EWSTimeZone created from 'Europe/Copenhagen' may return
        # from the server as 'Europe/Copenhagen'. We're catering for Microsoft here, so base equality on the Microsoft
        # timezone ID.
        if not isinstance(other, self.__class__):
            return NotImplemented
        return self.ms_id == other.ms_id

    @classmethod
    def from_ms_id(cls, ms_id):
        # Create a timezone instance from a Microsoft timezone ID. This is lossy because there is not a 1:1 translation
        # from MS timezone ID to IANA timezone.
        try:
            return cls(cls.MS_TO_IANA_MAP[ms_id])
        except KeyError:
            if '/' in ms_id:
                # EWS sometimes returns an ID that has a region/location format, e.g. 'Europe/Copenhagen'. Try the
                # string unaltered.
                return cls(ms_id)
            raise UnknownTimeZone("Windows timezone ID '%s' is unknown by CLDR" % ms_id)

    @classmethod
    def from_pytz(cls, tz):
        return cls(tz.zone)

    @classmethod
    def from_datetime(cls, tz):
        """Convert from a standard library `datetime.timezone` instance."""
        return cls(tz.tzname(None))

    @classmethod
    def from_dateutil(cls, tz):
        # Objects returned by dateutil.tz.tzlocal() and dateutil.tz.gettz() are not supported. They
        # don't contain enough information to reliably match them with a CLDR timezone.
        if hasattr(tz, '_filename'):
            key = '/'.join(tz._filename.split('/')[-2:])
            return cls(key)
        return cls(tz.tzname(datetime.datetime.now()))

    @classmethod
    def from_zoneinfo(cls, tz):
        return cls(tz.key)

    @classmethod
    def from_timezone(cls, tz):
        # Support multiple tzinfo implementations. We could use isinstance(), but then we'd have to have pytz
        # and dateutil as dependencies for this package.
        tz_module = tz.__class__.__module__.split('.')[0]
        try:
            return {
                cls.__module__.split('.')[0]: lambda z: z,
                'backports': cls.from_zoneinfo,
                'datetime': cls.from_datetime,
                'dateutil': cls.from_dateutil,
                'pytz': cls.from_pytz,
                'zoneinfo': cls.from_zoneinfo,
                'pytz_deprecation_shim': lambda z: cls.from_timezone(z.unwrap_shim())
            }[tz_module](tz)
        except KeyError:
            raise TypeError('Unsupported tzinfo type: %r' % tz)

    @classmethod
    def localzone(cls):
        try:
            tz = tzlocal.get_localzone()
        except zoneinfo.ZoneInfoNotFoundError:
            # Older versions of tzlocal will raise a pytz exception. Let's not depend on pytz just for that.
            raise UnknownTimeZone("Failed to guess local timezone")
        # Handle both old and new versions of tzlocal that may return pytz or zoneinfo objects, respectively
        return cls.from_timezone(tz)

    @classmethod
    def timezone(cls, location):
        warnings.warn('replace EWSTimeZone.timezone() with just EWSTimeZone()', DeprecationWarning, stacklevel=2)
        return cls(location)

    def normalize(self, dt, is_dst=False):
        warnings.warn('normalization is now handled gracefully', DeprecationWarning, stacklevel=2)
        return dt

    def localize(self, dt, is_dst=False):
        warnings.warn('replace tz.localize() with dt.replace(tzinfo=tz)', DeprecationWarning, stacklevel=2)
        if dt.tzinfo is not None:
            raise ValueError('%r must be timezone-unaware' % dt)
        dt = dt.replace(tzinfo=self)
        if is_dst is not None:
            # DST dates are assumed to always be after non-DST dates
            dt_before = dt.replace(fold=0)
            dt_after = dt.replace(fold=1)
            dst_before = dt_before.dst()
            dst_after = dt_after.dst()
            if dst_before > dst_after:
                dt = dt_before if is_dst else dt_after
            elif dst_before < dst_after:
                dt = dt_after if is_dst else dt_before
        return dt

    def fromutc(self, dt):
        t = super().fromutc(dt)
        if isinstance(t, EWSDateTime):
            return t
        return EWSDateTime.from_datetime(t)  # We want to return EWSDateTime objects

Ancestors

  • backports.zoneinfo.ZoneInfo
  • datetime.tzinfo

Class variables

var IANA_TO_MS_MAP
var MS_TO_IANA_MAP

Static methods

def from_datetime(tz)

Convert from a standard library datetime.timezone instance.

Expand source code
@classmethod
def from_datetime(cls, tz):
    """Convert from a standard library `datetime.timezone` instance."""
    return cls(tz.tzname(None))
def from_dateutil(tz)
Expand source code
@classmethod
def from_dateutil(cls, tz):
    # Objects returned by dateutil.tz.tzlocal() and dateutil.tz.gettz() are not supported. They
    # don't contain enough information to reliably match them with a CLDR timezone.
    if hasattr(tz, '_filename'):
        key = '/'.join(tz._filename.split('/')[-2:])
        return cls(key)
    return cls(tz.tzname(datetime.datetime.now()))
def from_ms_id(ms_id)
Expand source code
@classmethod
def from_ms_id(cls, ms_id):
    # Create a timezone instance from a Microsoft timezone ID. This is lossy because there is not a 1:1 translation
    # from MS timezone ID to IANA timezone.
    try:
        return cls(cls.MS_TO_IANA_MAP[ms_id])
    except KeyError:
        if '/' in ms_id:
            # EWS sometimes returns an ID that has a region/location format, e.g. 'Europe/Copenhagen'. Try the
            # string unaltered.
            return cls(ms_id)
        raise UnknownTimeZone("Windows timezone ID '%s' is unknown by CLDR" % ms_id)
def from_pytz(tz)
Expand source code
@classmethod
def from_pytz(cls, tz):
    return cls(tz.zone)
def from_timezone(tz)
Expand source code
@classmethod
def from_timezone(cls, tz):
    # Support multiple tzinfo implementations. We could use isinstance(), but then we'd have to have pytz
    # and dateutil as dependencies for this package.
    tz_module = tz.__class__.__module__.split('.')[0]
    try:
        return {
            cls.__module__.split('.')[0]: lambda z: z,
            'backports': cls.from_zoneinfo,
            'datetime': cls.from_datetime,
            'dateutil': cls.from_dateutil,
            'pytz': cls.from_pytz,
            'zoneinfo': cls.from_zoneinfo,
            'pytz_deprecation_shim': lambda z: cls.from_timezone(z.unwrap_shim())
        }[tz_module](tz)
    except KeyError:
        raise TypeError('Unsupported tzinfo type: %r' % tz)
def from_zoneinfo(tz)
Expand source code
@classmethod
def from_zoneinfo(cls, tz):
    return cls(tz.key)
def localzone()
Expand source code
@classmethod
def localzone(cls):
    try:
        tz = tzlocal.get_localzone()
    except zoneinfo.ZoneInfoNotFoundError:
        # Older versions of tzlocal will raise a pytz exception. Let's not depend on pytz just for that.
        raise UnknownTimeZone("Failed to guess local timezone")
    # Handle both old and new versions of tzlocal that may return pytz or zoneinfo objects, respectively
    return cls.from_timezone(tz)
def timezone(location)
Expand source code
@classmethod
def timezone(cls, location):
    warnings.warn('replace EWSTimeZone.timezone() with just EWSTimeZone()', DeprecationWarning, stacklevel=2)
    return cls(location)

Methods

def fromutc(self, dt)

Given a datetime with local time in UTC, retrieve an adjusted datetime in local time.

Expand source code
def fromutc(self, dt):
    t = super().fromutc(dt)
    if isinstance(t, EWSDateTime):
        return t
    return EWSDateTime.from_datetime(t)  # We want to return EWSDateTime objects
def localize(self, dt, is_dst=False)
Expand source code
def localize(self, dt, is_dst=False):
    warnings.warn('replace tz.localize() with dt.replace(tzinfo=tz)', DeprecationWarning, stacklevel=2)
    if dt.tzinfo is not None:
        raise ValueError('%r must be timezone-unaware' % dt)
    dt = dt.replace(tzinfo=self)
    if is_dst is not None:
        # DST dates are assumed to always be after non-DST dates
        dt_before = dt.replace(fold=0)
        dt_after = dt.replace(fold=1)
        dst_before = dt_before.dst()
        dst_after = dt_after.dst()
        if dst_before > dst_after:
            dt = dt_before if is_dst else dt_after
        elif dst_before < dst_after:
            dt = dt_after if is_dst else dt_before
    return dt
def normalize(self, dt, is_dst=False)
Expand source code
def normalize(self, dt, is_dst=False):
    warnings.warn('normalization is now handled gracefully', DeprecationWarning, stacklevel=2)
    return dt
exchangelib-4.6.1/docs/exchangelib/extended_properties.html000066400000000000000000001624321414601472700241540ustar00rootroot00000000000000 exchangelib.extended_properties API documentation

Module exchangelib.extended_properties

Expand source code
import logging
from decimal import Decimal

from .ewsdatetime import EWSDateTime
from .properties import EWSElement, ExtendedFieldURI
from .util import create_element, add_xml_child, get_xml_attrs, get_xml_attr, set_xml_value, value_to_xml_text, \
    xml_text_to_value, is_iterable, TNS

log = logging.getLogger(__name__)


class ExtendedProperty(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedproperty"""

    ELEMENT_NAME = 'ExtendedProperty'

    # Enum values: https://docs.microsoft.com/en-us/dotnet/api/exchangewebservices.distinguishedpropertysettype
    DISTINGUISHED_SETS = {
        'Address',
        'Appointment',
        'CalendarAssistant',
        'Common',
        'InternetHeaders',
        'Meeting',
        'PublicStrings',
        'Sharing',
        'Task',
        'UnifiedMessaging',
    }
    # Enum values: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedfielduri
    PROPERTY_TYPES = {
        'ApplicationTime',
        'Binary',
        'BinaryArray',
        'Boolean',
        'CLSID',
        'CLSIDArray',
        'Currency',
        'CurrencyArray',
        'Double',
        'DoubleArray',
        # 'Error',
        'Float',
        'FloatArray',
        'Integer',
        'IntegerArray',
        'Long',
        'LongArray',
        # 'Null',
        # 'Object',
        # 'ObjectArray',
        'Short',
        'ShortArray',
        'SystemTime',
        'SystemTimeArray',
        'String',
        'StringArray',
    }  # The commented-out types cannot be used for setting or getting (see docs) and are thus not very useful here

    # Translation table between common distinguished_property_set_id and property_set_id values. See
    # https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/commonly-used-property-sets
    # ID values must be lowercase.
    DISTINGUISHED_SET_NAME_TO_ID_MAP = {
        'Address': '00062004-0000-0000-c000-000000000046',
        'AirSync': '71035549-0739-4dcb-9163-00f0580dbbdf',
        'Appointment': '00062002-0000-0000-c000-000000000046',
        'Common': '00062008-0000-0000-c000-000000000046',
        'InternetHeaders': '00020386-0000-0000-c000-000000000046',
        'Log': '0006200a-0000-0000-c000-000000000046',
        'Mapi': '00020328-0000-0000-c000-000000000046',
        'Meeting': '6ed8da90-450b-101b-98da-00aa003f1305',
        'Messaging': '41f28f13-83f4-4114-a584-eedb5a6b0bff',
        'Note': '0006200e-0000-0000-c000-000000000046',
        'PostRss': '00062041-0000-0000-c000-000000000046',
        'PublicStrings': '00020329-0000-0000-c000-000000000046',
        'Remote': '00062014-0000-0000-c000-000000000046',
        'Report': '00062013-0000-0000-c000-000000000046',
        'Sharing': '00062040-0000-0000-c000-000000000046',
        'Task': '00062003-0000-0000-c000-000000000046',
        'UnifiedMessaging': '4442858e-a9e3-4e80-b900-317a210cc15b',
    }
    DISTINGUISHED_SET_ID_TO_NAME_MAP = {v: k for k, v in DISTINGUISHED_SET_NAME_TO_ID_MAP.items()}

    distinguished_property_set_id = None
    property_set_id = None
    property_tag = None  # hex integer (e.g. 0x8000) or string ('0x8000')
    property_name = None
    property_id = None  # integer as hex-formatted int (e.g. 0x8000) or normal int (32768)
    property_type = ''

    __slots__ = 'value',

    def __init__(self, *args, **kwargs):
        if not kwargs:
            # Allow to set attributes without keyword
            kwargs = dict(zip(self._slots_keys, args))
        self.value = kwargs.pop('value')
        super().__init__(**kwargs)

    @classmethod
    def validate_cls(cls):
        # Validate values of class attributes and their inter-dependencies
        cls._validate_distinguished_property_set_id()
        cls._validate_property_set_id()
        cls._validate_property_tag()
        cls._validate_property_name()
        cls._validate_property_id()
        cls._validate_property_type()

    @classmethod
    def _validate_distinguished_property_set_id(cls):
        if cls.distinguished_property_set_id:
            if any([cls.property_set_id, cls.property_tag]):
                raise ValueError(
                    "When 'distinguished_property_set_id' is set, 'property_set_id' and 'property_tag' must be None"
                )
            if not any([cls.property_id, cls.property_name]):
                raise ValueError(
                    "When 'distinguished_property_set_id' is set, 'property_id' or 'property_name' must also be set"
                )
            if cls.distinguished_property_set_id not in cls.DISTINGUISHED_SETS:
                raise ValueError(
                    "'distinguished_property_set_id' %r must be one of %s"
                    % (cls.distinguished_property_set_id, sorted(cls.DISTINGUISHED_SETS))
                )

    @classmethod
    def _validate_property_set_id(cls):
        if cls.property_set_id:
            if any([cls.distinguished_property_set_id, cls.property_tag]):
                raise ValueError(
                    "When 'property_set_id' is set, 'distinguished_property_set_id' and 'property_tag' must be None"
                )
            if not any([cls.property_id, cls.property_name]):
                raise ValueError(
                    "When 'property_set_id' is set, 'property_id' or 'property_name' must also be set"
                )

    @classmethod
    def _validate_property_tag(cls):
        if cls.property_tag:
            if any([
                cls.distinguished_property_set_id, cls.property_set_id, cls.property_name, cls.property_id
            ]):
                raise ValueError("When 'property_tag' is set, only 'property_type' must be set")
            if 0x8000 <= cls.property_tag_as_int() <= 0xFFFE:
                raise ValueError(
                    "'property_tag' value '%s' is reserved for custom properties" % cls.property_tag_as_hex()
                )

    @classmethod
    def _validate_property_name(cls):
        if cls.property_name:
            if any([cls.property_id, cls.property_tag]):
                raise ValueError("When 'property_name' is set, 'property_id' and 'property_tag' must be None")
            if not any([cls.distinguished_property_set_id, cls.property_set_id]):
                raise ValueError(
                    "When 'property_name' is set, 'distinguished_property_set_id' or 'property_set_id' must also be set"
                )

    @classmethod
    def _validate_property_id(cls):
        if cls.property_id:
            if any([cls.property_name, cls.property_tag]):
                raise ValueError("When 'property_id' is set, 'property_name' and 'property_tag' must be None")
            if not any([cls.distinguished_property_set_id, cls.property_set_id]):
                raise ValueError(
                    "When 'property_id' is set, 'distinguished_property_set_id' or 'property_set_id' must also be set"
                )

    @classmethod
    def _validate_property_type(cls):
        if cls.property_type not in cls.PROPERTY_TYPES:
            raise ValueError(
                "'property_type' %r must be one of %s" % (cls.property_type, sorted(cls.PROPERTY_TYPES))
            )

    def clean(self, version=None):
        self.validate_cls()
        python_type = self.python_type()
        if self.is_array_type():
            if not is_iterable(self.value):
                raise ValueError("'%s' value %r must be a list" % (self.__class__.__name__, self.value))
            for v in self.value:
                if not isinstance(v, python_type):
                    raise TypeError(
                        "'%s' value element %r must be an instance of %s" % (self.__class__.__name__, v, python_type))
        else:
            if not isinstance(self.value, python_type):
                raise TypeError(
                    "'%s' value %r must be an instance of %s" % (self.__class__.__name__, self.value, python_type))

    @classmethod
    def _normalize_obj(cls, obj):
        # Sometimes, EWS will helpfully translate a 'distinguished_property_set_id' value to a 'property_set_id' value
        # and vice versa. Align these values on an ExtendedFieldURI instance.
        try:
            obj.property_set_id = cls.DISTINGUISHED_SET_NAME_TO_ID_MAP[obj.distinguished_property_set_id]
        except KeyError:
            try:
                obj.distinguished_property_set_id = cls.DISTINGUISHED_SET_ID_TO_NAME_MAP[obj.property_set_id]
            except KeyError:
                pass
        return obj

    @classmethod
    def is_property_instance(cls, elem):
        """Return whether an 'ExtendedProperty' element matches the definition for this class. Extended property fields
        do not have a name, so we must match on the cls.property_* attributes to match a field in the request with a
        field in the response.
        """
        # We can't use ExtendedFieldURI.from_xml(). It clears the XML element but we may not want to consume it here.
        kwargs = {
            f.name: f.from_xml(elem=elem.find(ExtendedFieldURI.response_tag()), account=None)
            for f in ExtendedFieldURI.FIELDS
        }
        xml_obj = ExtendedFieldURI(**kwargs)
        cls_obj = cls.as_object()
        return cls._normalize_obj(cls_obj) == cls._normalize_obj(xml_obj)

    @classmethod
    def from_xml(cls, elem, account):
        # Gets value of this specific ExtendedProperty from a list of 'ExtendedProperty' XML elements
        python_type = cls.python_type()
        if cls.is_array_type():
            values = elem.find('{%s}Values' % TNS)
            return [
                xml_text_to_value(value=val, value_type=python_type)
                for val in get_xml_attrs(values, '{%s}Value' % TNS)
            ]
        extended_field_value = xml_text_to_value(value=get_xml_attr(elem, '{%s}Value' % TNS), value_type=python_type)
        if python_type == str and not extended_field_value:
            # For string types, we want to return the empty string instead of None if the element was
            # actually found, but there was no XML value. For other types, it would be more problematic
            # to make that distinction, e.g. return False for bool, 0 for int, etc.
            return ''
        return extended_field_value

    def to_xml(self, version):
        if self.is_array_type():
            values = create_element('t:Values')
            for v in self.value:
                add_xml_child(values, 't:Value', v)
            return values
        return set_xml_value(create_element('t:Value'), self.value, version=version)

    @classmethod
    def is_array_type(cls):
        return cls.property_type.endswith('Array')

    @classmethod
    def property_tag_as_int(cls):
        if isinstance(cls.property_tag, str):
            return int(cls.property_tag, base=16)
        return cls.property_tag

    @classmethod
    def property_tag_as_hex(cls):
        return hex(cls.property_tag) if isinstance(cls.property_tag, int) else cls.property_tag

    @classmethod
    def python_type(cls):
        # Return the best equivalent for a Python type for the property type of this class
        base_type = cls.property_type[:-5] if cls.is_array_type() else cls.property_type
        return {
            'ApplicationTime': Decimal,
            'Binary': bytes,
            'Boolean': bool,
            'CLSID': str,
            'Currency': int,
            'Double': Decimal,
            'Float': Decimal,
            'Integer': int,
            'Long': int,
            'Short': int,
            'SystemTime': EWSDateTime,
            'String': str,
        }[base_type]

    @classmethod
    def as_object(cls):
        # Return an object we can use to match with the incoming object from XML
        return ExtendedFieldURI(
            distinguished_property_set_id=cls.distinguished_property_set_id,
            property_set_id=cls.property_set_id.lower() if cls.property_set_id else None,
            property_tag=cls.property_tag_as_hex(),
            property_name=cls.property_name,
            property_id=value_to_xml_text(cls.property_id) if cls.property_id else None,
            property_type=cls.property_type,
        )


class ExternId(ExtendedProperty):
    """This is a custom extended property defined by us. It's useful for synchronization purposes, to attach a unique ID
    from an external system.
    """

    property_set_id = 'c11ff724-aa03-4555-9952-8fa248a11c3e'  # This is arbitrary. We just want a unique UUID.
    property_name = 'External ID'
    property_type = 'String'


class Flag(ExtendedProperty):
    """This property returns None for Not Flagged messages, 1 for Completed messages and 2 for Flagged messages.

    For a description of each status, see:
    https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxoflag/eda9fd25-6407-4cec-9e62-26e4f9d6a098
    """

    property_tag = 0x1090
    property_type = 'Integer'

Classes

class ExtendedProperty (*args, **kwargs)
Expand source code
class ExtendedProperty(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedproperty"""

    ELEMENT_NAME = 'ExtendedProperty'

    # Enum values: https://docs.microsoft.com/en-us/dotnet/api/exchangewebservices.distinguishedpropertysettype
    DISTINGUISHED_SETS = {
        'Address',
        'Appointment',
        'CalendarAssistant',
        'Common',
        'InternetHeaders',
        'Meeting',
        'PublicStrings',
        'Sharing',
        'Task',
        'UnifiedMessaging',
    }
    # Enum values: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedfielduri
    PROPERTY_TYPES = {
        'ApplicationTime',
        'Binary',
        'BinaryArray',
        'Boolean',
        'CLSID',
        'CLSIDArray',
        'Currency',
        'CurrencyArray',
        'Double',
        'DoubleArray',
        # 'Error',
        'Float',
        'FloatArray',
        'Integer',
        'IntegerArray',
        'Long',
        'LongArray',
        # 'Null',
        # 'Object',
        # 'ObjectArray',
        'Short',
        'ShortArray',
        'SystemTime',
        'SystemTimeArray',
        'String',
        'StringArray',
    }  # The commented-out types cannot be used for setting or getting (see docs) and are thus not very useful here

    # Translation table between common distinguished_property_set_id and property_set_id values. See
    # https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/commonly-used-property-sets
    # ID values must be lowercase.
    DISTINGUISHED_SET_NAME_TO_ID_MAP = {
        'Address': '00062004-0000-0000-c000-000000000046',
        'AirSync': '71035549-0739-4dcb-9163-00f0580dbbdf',
        'Appointment': '00062002-0000-0000-c000-000000000046',
        'Common': '00062008-0000-0000-c000-000000000046',
        'InternetHeaders': '00020386-0000-0000-c000-000000000046',
        'Log': '0006200a-0000-0000-c000-000000000046',
        'Mapi': '00020328-0000-0000-c000-000000000046',
        'Meeting': '6ed8da90-450b-101b-98da-00aa003f1305',
        'Messaging': '41f28f13-83f4-4114-a584-eedb5a6b0bff',
        'Note': '0006200e-0000-0000-c000-000000000046',
        'PostRss': '00062041-0000-0000-c000-000000000046',
        'PublicStrings': '00020329-0000-0000-c000-000000000046',
        'Remote': '00062014-0000-0000-c000-000000000046',
        'Report': '00062013-0000-0000-c000-000000000046',
        'Sharing': '00062040-0000-0000-c000-000000000046',
        'Task': '00062003-0000-0000-c000-000000000046',
        'UnifiedMessaging': '4442858e-a9e3-4e80-b900-317a210cc15b',
    }
    DISTINGUISHED_SET_ID_TO_NAME_MAP = {v: k for k, v in DISTINGUISHED_SET_NAME_TO_ID_MAP.items()}

    distinguished_property_set_id = None
    property_set_id = None
    property_tag = None  # hex integer (e.g. 0x8000) or string ('0x8000')
    property_name = None
    property_id = None  # integer as hex-formatted int (e.g. 0x8000) or normal int (32768)
    property_type = ''

    __slots__ = 'value',

    def __init__(self, *args, **kwargs):
        if not kwargs:
            # Allow to set attributes without keyword
            kwargs = dict(zip(self._slots_keys, args))
        self.value = kwargs.pop('value')
        super().__init__(**kwargs)

    @classmethod
    def validate_cls(cls):
        # Validate values of class attributes and their inter-dependencies
        cls._validate_distinguished_property_set_id()
        cls._validate_property_set_id()
        cls._validate_property_tag()
        cls._validate_property_name()
        cls._validate_property_id()
        cls._validate_property_type()

    @classmethod
    def _validate_distinguished_property_set_id(cls):
        if cls.distinguished_property_set_id:
            if any([cls.property_set_id, cls.property_tag]):
                raise ValueError(
                    "When 'distinguished_property_set_id' is set, 'property_set_id' and 'property_tag' must be None"
                )
            if not any([cls.property_id, cls.property_name]):
                raise ValueError(
                    "When 'distinguished_property_set_id' is set, 'property_id' or 'property_name' must also be set"
                )
            if cls.distinguished_property_set_id not in cls.DISTINGUISHED_SETS:
                raise ValueError(
                    "'distinguished_property_set_id' %r must be one of %s"
                    % (cls.distinguished_property_set_id, sorted(cls.DISTINGUISHED_SETS))
                )

    @classmethod
    def _validate_property_set_id(cls):
        if cls.property_set_id:
            if any([cls.distinguished_property_set_id, cls.property_tag]):
                raise ValueError(
                    "When 'property_set_id' is set, 'distinguished_property_set_id' and 'property_tag' must be None"
                )
            if not any([cls.property_id, cls.property_name]):
                raise ValueError(
                    "When 'property_set_id' is set, 'property_id' or 'property_name' must also be set"
                )

    @classmethod
    def _validate_property_tag(cls):
        if cls.property_tag:
            if any([
                cls.distinguished_property_set_id, cls.property_set_id, cls.property_name, cls.property_id
            ]):
                raise ValueError("When 'property_tag' is set, only 'property_type' must be set")
            if 0x8000 <= cls.property_tag_as_int() <= 0xFFFE:
                raise ValueError(
                    "'property_tag' value '%s' is reserved for custom properties" % cls.property_tag_as_hex()
                )

    @classmethod
    def _validate_property_name(cls):
        if cls.property_name:
            if any([cls.property_id, cls.property_tag]):
                raise ValueError("When 'property_name' is set, 'property_id' and 'property_tag' must be None")
            if not any([cls.distinguished_property_set_id, cls.property_set_id]):
                raise ValueError(
                    "When 'property_name' is set, 'distinguished_property_set_id' or 'property_set_id' must also be set"
                )

    @classmethod
    def _validate_property_id(cls):
        if cls.property_id:
            if any([cls.property_name, cls.property_tag]):
                raise ValueError("When 'property_id' is set, 'property_name' and 'property_tag' must be None")
            if not any([cls.distinguished_property_set_id, cls.property_set_id]):
                raise ValueError(
                    "When 'property_id' is set, 'distinguished_property_set_id' or 'property_set_id' must also be set"
                )

    @classmethod
    def _validate_property_type(cls):
        if cls.property_type not in cls.PROPERTY_TYPES:
            raise ValueError(
                "'property_type' %r must be one of %s" % (cls.property_type, sorted(cls.PROPERTY_TYPES))
            )

    def clean(self, version=None):
        self.validate_cls()
        python_type = self.python_type()
        if self.is_array_type():
            if not is_iterable(self.value):
                raise ValueError("'%s' value %r must be a list" % (self.__class__.__name__, self.value))
            for v in self.value:
                if not isinstance(v, python_type):
                    raise TypeError(
                        "'%s' value element %r must be an instance of %s" % (self.__class__.__name__, v, python_type))
        else:
            if not isinstance(self.value, python_type):
                raise TypeError(
                    "'%s' value %r must be an instance of %s" % (self.__class__.__name__, self.value, python_type))

    @classmethod
    def _normalize_obj(cls, obj):
        # Sometimes, EWS will helpfully translate a 'distinguished_property_set_id' value to a 'property_set_id' value
        # and vice versa. Align these values on an ExtendedFieldURI instance.
        try:
            obj.property_set_id = cls.DISTINGUISHED_SET_NAME_TO_ID_MAP[obj.distinguished_property_set_id]
        except KeyError:
            try:
                obj.distinguished_property_set_id = cls.DISTINGUISHED_SET_ID_TO_NAME_MAP[obj.property_set_id]
            except KeyError:
                pass
        return obj

    @classmethod
    def is_property_instance(cls, elem):
        """Return whether an 'ExtendedProperty' element matches the definition for this class. Extended property fields
        do not have a name, so we must match on the cls.property_* attributes to match a field in the request with a
        field in the response.
        """
        # We can't use ExtendedFieldURI.from_xml(). It clears the XML element but we may not want to consume it here.
        kwargs = {
            f.name: f.from_xml(elem=elem.find(ExtendedFieldURI.response_tag()), account=None)
            for f in ExtendedFieldURI.FIELDS
        }
        xml_obj = ExtendedFieldURI(**kwargs)
        cls_obj = cls.as_object()
        return cls._normalize_obj(cls_obj) == cls._normalize_obj(xml_obj)

    @classmethod
    def from_xml(cls, elem, account):
        # Gets value of this specific ExtendedProperty from a list of 'ExtendedProperty' XML elements
        python_type = cls.python_type()
        if cls.is_array_type():
            values = elem.find('{%s}Values' % TNS)
            return [
                xml_text_to_value(value=val, value_type=python_type)
                for val in get_xml_attrs(values, '{%s}Value' % TNS)
            ]
        extended_field_value = xml_text_to_value(value=get_xml_attr(elem, '{%s}Value' % TNS), value_type=python_type)
        if python_type == str and not extended_field_value:
            # For string types, we want to return the empty string instead of None if the element was
            # actually found, but there was no XML value. For other types, it would be more problematic
            # to make that distinction, e.g. return False for bool, 0 for int, etc.
            return ''
        return extended_field_value

    def to_xml(self, version):
        if self.is_array_type():
            values = create_element('t:Values')
            for v in self.value:
                add_xml_child(values, 't:Value', v)
            return values
        return set_xml_value(create_element('t:Value'), self.value, version=version)

    @classmethod
    def is_array_type(cls):
        return cls.property_type.endswith('Array')

    @classmethod
    def property_tag_as_int(cls):
        if isinstance(cls.property_tag, str):
            return int(cls.property_tag, base=16)
        return cls.property_tag

    @classmethod
    def property_tag_as_hex(cls):
        return hex(cls.property_tag) if isinstance(cls.property_tag, int) else cls.property_tag

    @classmethod
    def python_type(cls):
        # Return the best equivalent for a Python type for the property type of this class
        base_type = cls.property_type[:-5] if cls.is_array_type() else cls.property_type
        return {
            'ApplicationTime': Decimal,
            'Binary': bytes,
            'Boolean': bool,
            'CLSID': str,
            'Currency': int,
            'Double': Decimal,
            'Float': Decimal,
            'Integer': int,
            'Long': int,
            'Short': int,
            'SystemTime': EWSDateTime,
            'String': str,
        }[base_type]

    @classmethod
    def as_object(cls):
        # Return an object we can use to match with the incoming object from XML
        return ExtendedFieldURI(
            distinguished_property_set_id=cls.distinguished_property_set_id,
            property_set_id=cls.property_set_id.lower() if cls.property_set_id else None,
            property_tag=cls.property_tag_as_hex(),
            property_name=cls.property_name,
            property_id=value_to_xml_text(cls.property_id) if cls.property_id else None,
            property_type=cls.property_type,
        )

Ancestors

Subclasses

Class variables

var DISTINGUISHED_SETS
var DISTINGUISHED_SET_ID_TO_NAME_MAP
var DISTINGUISHED_SET_NAME_TO_ID_MAP
var ELEMENT_NAME
var PROPERTY_TYPES
var distinguished_property_set_id
var property_id
var property_name
var property_set_id
var property_tag
var property_type

Static methods

def as_object()
Expand source code
@classmethod
def as_object(cls):
    # Return an object we can use to match with the incoming object from XML
    return ExtendedFieldURI(
        distinguished_property_set_id=cls.distinguished_property_set_id,
        property_set_id=cls.property_set_id.lower() if cls.property_set_id else None,
        property_tag=cls.property_tag_as_hex(),
        property_name=cls.property_name,
        property_id=value_to_xml_text(cls.property_id) if cls.property_id else None,
        property_type=cls.property_type,
    )
def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    # Gets value of this specific ExtendedProperty from a list of 'ExtendedProperty' XML elements
    python_type = cls.python_type()
    if cls.is_array_type():
        values = elem.find('{%s}Values' % TNS)
        return [
            xml_text_to_value(value=val, value_type=python_type)
            for val in get_xml_attrs(values, '{%s}Value' % TNS)
        ]
    extended_field_value = xml_text_to_value(value=get_xml_attr(elem, '{%s}Value' % TNS), value_type=python_type)
    if python_type == str and not extended_field_value:
        # For string types, we want to return the empty string instead of None if the element was
        # actually found, but there was no XML value. For other types, it would be more problematic
        # to make that distinction, e.g. return False for bool, 0 for int, etc.
        return ''
    return extended_field_value
def is_array_type()
Expand source code
@classmethod
def is_array_type(cls):
    return cls.property_type.endswith('Array')
def is_property_instance(elem)

Return whether an 'ExtendedProperty' element matches the definition for this class. Extended property fields do not have a name, so we must match on the cls.property_* attributes to match a field in the request with a field in the response.

Expand source code
@classmethod
def is_property_instance(cls, elem):
    """Return whether an 'ExtendedProperty' element matches the definition for this class. Extended property fields
    do not have a name, so we must match on the cls.property_* attributes to match a field in the request with a
    field in the response.
    """
    # We can't use ExtendedFieldURI.from_xml(). It clears the XML element but we may not want to consume it here.
    kwargs = {
        f.name: f.from_xml(elem=elem.find(ExtendedFieldURI.response_tag()), account=None)
        for f in ExtendedFieldURI.FIELDS
    }
    xml_obj = ExtendedFieldURI(**kwargs)
    cls_obj = cls.as_object()
    return cls._normalize_obj(cls_obj) == cls._normalize_obj(xml_obj)
def property_tag_as_hex()
Expand source code
@classmethod
def property_tag_as_hex(cls):
    return hex(cls.property_tag) if isinstance(cls.property_tag, int) else cls.property_tag
def property_tag_as_int()
Expand source code
@classmethod
def property_tag_as_int(cls):
    if isinstance(cls.property_tag, str):
        return int(cls.property_tag, base=16)
    return cls.property_tag
def python_type()
Expand source code
@classmethod
def python_type(cls):
    # Return the best equivalent for a Python type for the property type of this class
    base_type = cls.property_type[:-5] if cls.is_array_type() else cls.property_type
    return {
        'ApplicationTime': Decimal,
        'Binary': bytes,
        'Boolean': bool,
        'CLSID': str,
        'Currency': int,
        'Double': Decimal,
        'Float': Decimal,
        'Integer': int,
        'Long': int,
        'Short': int,
        'SystemTime': EWSDateTime,
        'String': str,
    }[base_type]
def validate_cls()
Expand source code
@classmethod
def validate_cls(cls):
    # Validate values of class attributes and their inter-dependencies
    cls._validate_distinguished_property_set_id()
    cls._validate_property_set_id()
    cls._validate_property_tag()
    cls._validate_property_name()
    cls._validate_property_id()
    cls._validate_property_type()

Instance variables

var value

Return an attribute of instance, which is of type owner.

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    self.validate_cls()
    python_type = self.python_type()
    if self.is_array_type():
        if not is_iterable(self.value):
            raise ValueError("'%s' value %r must be a list" % (self.__class__.__name__, self.value))
        for v in self.value:
            if not isinstance(v, python_type):
                raise TypeError(
                    "'%s' value element %r must be an instance of %s" % (self.__class__.__name__, v, python_type))
    else:
        if not isinstance(self.value, python_type):
            raise TypeError(
                "'%s' value %r must be an instance of %s" % (self.__class__.__name__, self.value, python_type))
def to_xml(self, version)
Expand source code
def to_xml(self, version):
    if self.is_array_type():
        values = create_element('t:Values')
        for v in self.value:
            add_xml_child(values, 't:Value', v)
        return values
    return set_xml_value(create_element('t:Value'), self.value, version=version)

Inherited members

class ExternId (*args, **kwargs)

This is a custom extended property defined by us. It's useful for synchronization purposes, to attach a unique ID from an external system.

Expand source code
class ExternId(ExtendedProperty):
    """This is a custom extended property defined by us. It's useful for synchronization purposes, to attach a unique ID
    from an external system.
    """

    property_set_id = 'c11ff724-aa03-4555-9952-8fa248a11c3e'  # This is arbitrary. We just want a unique UUID.
    property_name = 'External ID'
    property_type = 'String'

Ancestors

Class variables

var property_name
var property_set_id
var property_type

Inherited members

class Flag (*args, **kwargs)

This property returns None for Not Flagged messages, 1 for Completed messages and 2 for Flagged messages.

For a description of each status, see: https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxoflag/eda9fd25-6407-4cec-9e62-26e4f9d6a098

Expand source code
class Flag(ExtendedProperty):
    """This property returns None for Not Flagged messages, 1 for Completed messages and 2 for Flagged messages.

    For a description of each status, see:
    https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxoflag/eda9fd25-6407-4cec-9e62-26e4f9d6a098
    """

    property_tag = 0x1090
    property_type = 'Integer'

Ancestors

Class variables

var property_tag
var property_type

Inherited members

exchangelib-4.6.1/docs/exchangelib/fields.html000066400000000000000000012022361414601472700213440ustar00rootroot00000000000000 exchangelib.fields API documentation

Module exchangelib.fields

Expand source code
import abc
import datetime
import logging
from collections import OrderedDict
from decimal import Decimal, InvalidOperation
from importlib import import_module

from .ewsdatetime import EWSDateTime, EWSDate, EWSTimeZone, NaiveDateTimeNotAllowed, UnknownTimeZone, UTC
from .util import create_element, get_xml_attr, get_xml_attrs, set_xml_value, value_to_xml_text, is_iterable, \
    xml_text_to_value, TNS
from .version import Build, Version, EXCHANGE_2013

log = logging.getLogger(__name__)


# DayOfWeekIndex enum. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/dayofweekindex
FIRST = 'First'
SECOND = 'Second'
THIRD = 'Third'
FOURTH = 'Fourth'
LAST = 'Last'
WEEK_NUMBERS = (FIRST, SECOND, THIRD, FOURTH, LAST)

# Month enum
JANUARY = 'January'
FEBRUARY = 'February'
MARCH = 'March'
APRIL = 'April'
MAY = 'May'
JUNE = 'June'
JULY = 'July'
AUGUST = 'August'
SEPTEMBER = 'September'
OCTOBER = 'October'
NOVEMBER = 'November'
DECEMBER = 'December'
MONTHS = (JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER)

# Weekday enum
MONDAY = 'Monday'
TUESDAY = 'Tuesday'
WEDNESDAY = 'Wednesday'
THURSDAY = 'Thursday'
FRIDAY = 'Friday'
SATURDAY = 'Saturday'
SUNDAY = 'Sunday'
WEEKDAY_NAMES = (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY)

# Used for weekday recurrences except weekly recurrences. E.g. for "First WeekendDay in March"
DAY = 'Day'
WEEK_DAY = 'Weekday'  # Non-weekend day
WEEKEND_DAY = 'WeekendDay'
EXTRA_WEEKDAY_OPTIONS = (DAY, WEEK_DAY, WEEKEND_DAY)

# DaysOfWeek enum: See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/daysofweek-daysofweektype
WEEKDAYS = WEEKDAY_NAMES + EXTRA_WEEKDAY_OPTIONS


class InvalidField(ValueError):
    """Used when a field name does not match any defined fields."""


class InvalidFieldForVersion(ValueError):
    """Used when a field is not supported on the given Exchnage version."""


class InvalidChoiceForVersion(ValueError):
    """Used when a value is not valid for an enum-type field."""


def split_field_path(field_path):
    """Split a string path into its field, label and subfield parts.

    :param field_path:

    :return Examples:
      'start' -> ('start', None, None)
      'phone_numbers__PrimaryPhone' -> ('phone_numbers', 'PrimaryPhone', None)
      'physical_addresses__Home__street' -> ('physical_addresses', 'Home', 'street')
    """
    if not isinstance(field_path, str):
        raise ValueError("Field path %r must be a string" % field_path)
    search_parts = field_path.split('__')
    field = search_parts[0]
    try:
        label = search_parts[1]
    except IndexError:
        label = None
    try:
        subfield = search_parts[2]
    except IndexError:
        subfield = None
    return field, label, subfield


def resolve_field_path(field_path, folder, strict=True):
    """Take the name of a field, or '__'-delimited path to a subfield, and return the corresponding Field object,
    label and SubField object.
    """
    from .indexed_properties import SingleFieldIndexedElement, MultiFieldIndexedElement
    fieldname, label, subfieldname = split_field_path(field_path)
    field = folder.get_item_field_by_fieldname(fieldname)
    subfield = None
    if isinstance(field, IndexedField):
        if strict and not label:
            raise ValueError(
                "IndexedField path '%s' must specify label, e.g. '%s__%s'"
                % (field_path, fieldname, field.value_cls.get_field_by_fieldname('label').default)
            )
        valid_labels = field.value_cls.get_field_by_fieldname('label').supported_choices(
            version=folder.account.version
        )
        if label and label not in valid_labels:
            raise ValueError(
                "Label '%s' on IndexedField path '%s' must be one of %s"
                % (label, field_path, ', '.join(valid_labels))
            )
        if issubclass(field.value_cls, MultiFieldIndexedElement):
            if strict and not subfieldname:
                raise ValueError(
                    "IndexedField path '%s' must specify subfield, e.g. '%s__%s__%s'"
                    % (field_path, fieldname, label, field.value_cls.FIELDS[1].name)
                )

            if subfieldname:
                try:
                    subfield = field.value_cls.get_field_by_fieldname(subfieldname)
                except ValueError:
                    fnames = ', '.join(f.name for f in field.value_cls.supported_fields(
                        version=folder.account.version
                    ))
                    raise ValueError(
                        "Subfield '%s' on IndexedField path '%s' must be one of %s"
                        % (subfieldname, field_path, fnames)
                    )
        else:
            if not issubclass(field.value_cls, SingleFieldIndexedElement):
                raise ValueError("'field.value_cls' %r must be an SingleFieldIndexedElement instance" % field.value_cls)
            if subfieldname:
                raise ValueError(
                    "IndexedField path '%s' must not specify subfield, e.g. just '%s__%s'"
                    % (field_path, fieldname, label)
                )
            subfield = field.value_cls.value_field(version=folder.account.version)
    else:
        if label or subfieldname:
            raise ValueError(
                "Field path '%s' must not specify label or subfield, e.g. just '%s'"
                % (field_path, fieldname)
            )
    return field, label, subfield


class FieldPath:
    """Holds values needed to point to a single field. For indexed properties, we allow setting either field,
    field and label, or field, label and subfield. This allows pointing to either the full indexed property set, a
    property with a specific label, or a particular subfield field on that property.
    """

    def __init__(self, field, label=None, subfield=None):
        # 'label' and 'subfield' are only used for IndexedField fields
        if not isinstance(field, (FieldURIField, ExtendedPropertyField)):
            raise ValueError("'field' %r must be an FieldURIField, of ExtendedPropertyField instance" % field)
        if label and not isinstance(label, str):
            raise ValueError("'label' %r must be a %s instance" % (label, str))
        if subfield and not isinstance(subfield, SubField):
            raise ValueError("'subfield' %r must be a SubField instance" % subfield)
        self.field = field
        self.label = label
        self.subfield = subfield

    @classmethod
    def from_string(cls, field_path, folder, strict=False):
        field, label, subfield = resolve_field_path(field_path, folder=folder, strict=strict)
        return cls(field=field, label=label, subfield=subfield)

    def get_value(self, item):
        # For indexed properties, get either the full property set, the property with matching label, or a particular
        # subfield.
        if self.label:
            for subitem in getattr(item, self.field.name):
                if subitem.label == self.label:
                    if self.subfield:
                        return getattr(subitem, self.subfield.name)
                    return subitem
            return None  # No item with this label
        return getattr(item, self.field.name)

    def get_sort_value(self, item):
        # For fields that allow values of different types, we need to return a value that is
        val = self.get_value(item)
        if isinstance(self.field, DateOrDateTimeField) and isinstance(val, EWSDate):
            return item.date_to_datetime(field_name=self.field.name)
        return val

    def to_xml(self):
        if isinstance(self.field, IndexedField):
            if not self.label or not self.subfield:
                raise ValueError("Field path for indexed field '%s' is missing label and/or subfield" % self.field.name)
            return self.subfield.field_uri_xml(field_uri=self.field.field_uri, label=self.label)
        return self.field.field_uri_xml()

    def expand(self, version):
        # If this path does not point to a specific subfield on an indexed property, return all the possible path
        # combinations for this field path.
        if isinstance(self.field, IndexedField):
            labels = [self.label] if self.label \
                else self.field.value_cls.get_field_by_fieldname('label').supported_choices(version=version)
            subfields = [self.subfield] if self.subfield else self.field.value_cls.supported_fields(version=version)
            for label in labels:
                for subfield in subfields:
                    yield FieldPath(field=self.field, label=label, subfield=subfield)
        else:
            yield self

    @property
    def path(self):
        if self.label:
            from .indexed_properties import SingleFieldIndexedElement
            if issubclass(self.field.value_cls, SingleFieldIndexedElement) or not self.subfield:
                return '%s__%s' % (self.field.name, self.label)
            return '%s__%s__%s' % (self.field.name, self.label, self.subfield.name)
        return self.field.name

    def __eq__(self, other):
        return hash(self) == hash(other)

    def __str__(self):
        return self.path

    def __repr__(self):
        return self.__class__.__name__ + repr((self.field, self.label, self.subfield))

    def __hash__(self):
        return hash((self.field, self.label, self.subfield))


class FieldOrder:
    """Holds values needed to call server-side sorting on a single field path."""

    def __init__(self, field_path, reverse=False):
        if not isinstance(field_path, FieldPath):
            raise ValueError("'field_path' %r must be a FieldPath instance" % field_path)
        if not isinstance(reverse, bool):
            raise ValueError("'reverse' %r must be a boolean" % reverse)
        self.field_path = field_path
        self.reverse = reverse

    @classmethod
    def from_string(cls, field_path, folder):
        return cls(
            field_path=FieldPath.from_string(field_path=field_path.lstrip('-'), folder=folder, strict=True),
            reverse=field_path.startswith('-')
        )

    def to_xml(self):
        field_order = create_element('t:FieldOrder', attrs=dict(Order='Descending' if self.reverse else 'Ascending'))
        field_order.append(self.field_path.to_xml())
        return field_order


class Field(metaclass=abc.ABCMeta):
    """Holds information related to an item field."""

    value_cls = None
    is_list = False
    # Is the field a complex EWS type? Quoting the EWS FindItem docs:
    #
    #   The FindItem operation returns only the first 512 bytes of any streamable property. For Unicode, it returns
    #   the first 255 characters by using a null-terminated Unicode string. It does not return any of the message
    #   body formats or the recipient lists.
    #
    is_complex = False

    def __init__(self, name=None, is_required=False, is_required_after_save=False, is_read_only=False,
                 is_read_only_after_send=False, is_searchable=True, is_attribute=False, default=None,
                 supported_from=None, deprecated_from=None):
        self.name = name
        self.default = default  # Default value if none is given
        self.is_required = is_required
        # Some fields cannot be deleted on update. Default to True if 'is_required' is set
        self.is_required_after_save = is_required or is_required_after_save
        self.is_read_only = is_read_only
        # Set this for fields that raise ErrorInvalidPropertyUpdateSentMessage on update after send. Default to True
        # if 'is_read_only' is set
        self.is_read_only_after_send = is_read_only or is_read_only_after_send
        # Define whether the field can be used in a QuerySet. For some reason, EWS disallows searching on some fields,
        # instead throwing ErrorInvalidValueForProperty
        self.is_searchable = is_searchable
        # When true, this field is treated as an XML attribute instead of an element
        self.is_attribute = is_attribute
        # The Exchange build when this field was introduced. When talking with versions prior to this version,
        # we will ignore this field.
        if supported_from is not None and not isinstance(supported_from, Build):
            raise ValueError("'supported_from' %r must be a Build instance" % supported_from)
        self.supported_from = supported_from
        # The Exchange build when this field was deprecated. When talking with versions at or later than this version,
        # we will ignore this field.
        if deprecated_from is not None and not isinstance(deprecated_from, Build):
            raise ValueError("'deprecated_from' %r must be a Build instance" % deprecated_from)
        self.deprecated_from = deprecated_from

    def clean(self, value, version=None):
        if version and not self.supports_version(version):
            raise InvalidFieldForVersion("Field '%s' does not support EWS builds prior to %s (server has %s)" % (
                self.name, self.supported_from, version))
        if value is None:
            if self.is_required and self.default is None:
                raise ValueError("'%s' is a required field with no default" % self.name)
            return self.default
        if self.is_list:
            if not is_iterable(value):
                raise ValueError("Field '%s' value %r must be a list" % (self.name, value))
            for v in value:
                if not isinstance(v, self.value_cls):
                    raise TypeError("Field '%s' value %r must be of type %s" % (self.name, v, self.value_cls))
                if hasattr(v, 'clean'):
                    v.clean(version=version)
        else:
            if not isinstance(value, self.value_cls):
                raise TypeError("Field '%s' value %r must be of type %s" % (self.name, value, self.value_cls))
            if hasattr(value, 'clean'):
                value.clean(version=version)
        return value

    @abc.abstractmethod
    def from_xml(self, elem, account):
        pass

    @abc.abstractmethod
    def to_xml(self, value, version):
        pass

    def supports_version(self, version):
        # 'version' is a Version instance, for convenience by callers
        if not isinstance(version, Version):
            raise ValueError("'version' %r must be a Version instance" % version)
        if self.supported_from and version.build < self.supported_from:
            return False
        if self.deprecated_from and version.build >= self.deprecated_from:
            return False
        return True

    def __eq__(self, other):
        return hash(self) == hash(other)

    @abc.abstractmethod
    def __hash__(self):
        pass

    def __repr__(self):
        return self.__class__.__name__ + '(%s)' % ', '.join('%s=%r' % (f, getattr(self, f)) for f in (
            'name', 'value_cls', 'is_list', 'is_complex', 'default'))


class FieldURIField(Field):
    """A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It
    may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be
    'itemtype:FieldName'
    """

    def __init__(self, *args, **kwargs):
        self.field_uri = kwargs.pop('field_uri', None)
        self.namespace = kwargs.pop('namespace', TNS)
        super().__init__(*args, **kwargs)
        # See all valid FieldURI values at
        # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/fielduri
        # The field_uri has a prefix when the FieldURI points to an Item field.
        if self.field_uri is None:
            self.field_uri_postfix = None
        elif ':' in self.field_uri:
            self.field_uri_postfix = self.field_uri.split(':')[1]
        else:
            self.field_uri_postfix = self.field_uri

    def _get_val_from_elem(self, elem):
        if self.is_attribute:
            return elem.get(self.field_uri) or None
        return get_xml_attr(elem, self.response_tag())

    def from_xml(self, elem, account):
        val = self._get_val_from_elem(elem)
        if val is not None:
            try:
                return xml_text_to_value(val, self.value_cls)
            except (ValueError, InvalidOperation):
                log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
                return None
        return self.default

    def to_xml(self, value, version):
        field_elem = create_element(self.request_tag())
        return set_xml_value(field_elem, value, version=version)

    def field_uri_xml(self):
        from .properties import FieldURI
        if not self.field_uri:
            raise ValueError("'field_uri' value is missing")
        return FieldURI(field_uri=self.field_uri).to_xml(version=None)

    def request_tag(self):
        if not self.field_uri_postfix:
            raise ValueError("'field_uri_postfix' value is missing")
        return 't:%s' % self.field_uri_postfix

    def response_tag(self):
        if not self.field_uri_postfix:
            raise ValueError("'field_uri_postfix' value is missing")
        return '{%s}%s' % (self.namespace, self.field_uri_postfix)

    def __hash__(self):
        return hash(self.field_uri)


class BooleanField(FieldURIField):
    """A field that handles boolean values."""

    value_cls = bool


class OnOffField(BooleanField):
    """A field that handles boolean values that are On/Off instead of True/False."""


class IntegerField(FieldURIField):
    """A field that handles integer values."""

    value_cls = int

    def __init__(self, *args, **kwargs):
        self.min = kwargs.pop('min', None)
        self.max = kwargs.pop('max', None)
        super().__init__(*args, **kwargs)

    def _clean_single_value(self, v):
        if self.min is not None and v < self.min:
            raise ValueError(
                "Value %r on field '%s' must be greater than %s" % (v, self.name, self.min))
        if self.max is not None and v > self.max:
            raise ValueError("Value %r on field '%s' must be less than %s" % (v, self.name, self.max))

    def clean(self, value, version=None):
        value = super().clean(value, version=version)
        if value is not None:
            if self.is_list:
                for v in value:
                    self._clean_single_value(v)
            else:
                self._clean_single_value(value)
        return value


class DecimalField(IntegerField):
    """A field that handles decimal values."""

    value_cls = Decimal


class EnumField(IntegerField):
    """A field type where you can enter either the 1-based index in an enum (tuple), or the enum value. Values will be
    stored internally as integers but output in XML as strings.
    """

    def __init__(self, *args, **kwargs):
        self.enum = kwargs.pop('enum')
        # Set different min/max defaults than IntegerField
        if 'max' in kwargs:
            raise AttributeError("EnumField does not support the 'max' attribute")
        kwargs['min'] = kwargs.pop('min', 1)
        kwargs['max'] = kwargs['min'] + len(self.enum) - 1
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        if self.is_list:
            value = list(value)  # Convert to something we can index
            for i, v in enumerate(value):
                if isinstance(v, str):
                    if v not in self.enum:
                        raise ValueError(
                            "List value '%s' on field '%s' must be one of %s" % (v, self.name, self.enum))
                    value[i] = self.enum.index(v) + 1
            if not value:
                raise ValueError("Value '%s' on field '%s' must not be empty" % (value, self.name))
            if len(value) > len(set(value)):
                raise ValueError("List entries '%s' on field '%s' must be unique" % (value, self.name))
        else:
            if isinstance(value, str):
                if value not in self.enum:
                    raise ValueError(
                        "Value '%s' on field '%s' must be one of %s" % (value, self.name, self.enum))
                value = self.enum.index(value) + 1
        return super().clean(value, version=version)

    def as_string(self, value):
        # Converts an integer in the enum to its equivalent string
        if isinstance(value, str):
            return value
        if self.is_list:
            return [self.enum[v - 1] for v in sorted(value)]
        return self.enum[value - 1]

    def from_xml(self, elem, account):
        val = self._get_val_from_elem(elem)
        if val is not None:
            try:
                if self.is_list:
                    return [self.enum.index(v) + 1 for v in val.split(' ')]
                return self.enum.index(val) + 1
            except ValueError:
                log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
                return None
        return self.default

    def to_xml(self, value, version):
        field_elem = create_element(self.request_tag())
        if self.is_list:
            return set_xml_value(field_elem, ' '.join(self.as_string(value)), version=version)
        return set_xml_value(field_elem, self.as_string(value), version=version)


class EnumListField(EnumField):
    """Like EnumField, but for lists of enum values."""

    is_list = True


class EnumAsIntField(EnumField):
    """Like EnumField, but communicates values with EWS in integers."""

    def from_xml(self, elem, account):
        return super(EnumField, self).from_xml(elem=elem, account=account)

    def to_xml(self, value, version):
        field_elem = create_element(self.request_tag())
        return set_xml_value(field_elem, value, version=version)


class AppointmentStateField(IntegerField):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/appointmentstate"""

    NONE = 'None'
    MEETING = 'Meeting'
    RECEIVED = 'Received'
    CANCELLED = 'Cancelled'
    STATES = {
        NONE: 0x0000,
        MEETING: 0x0001,
        RECEIVED: 0x0002,
        CANCELLED: 0x0004,
    }

    def from_xml(self, elem, account):
        val = super().from_xml(elem=elem, account=account)
        if val is None:
            return val
        return tuple(name for name, mask in self.STATES.items() if bool(val & mask))


class Base64Field(FieldURIField):
    """A field that handles binary data and automatically Base64 encodes and decodes the data."""

    value_cls = bytes
    is_complex = True

    def __init__(self, *args, **kwargs):
        if 'is_searchable' not in kwargs:
            kwargs['is_searchable'] = False
        super().__init__(*args, **kwargs)


class MimeContentField(Base64Field):
    """Like Base64Field. This element has an optional 'CharacterSet' attribute, but it specifies the encoding of the
    base64-encoded string (which doesn't make sense since base64-encoded strings are always ASCII). We ignore it here
    because the decoded data could be in some other encoding, specified in the "Content-Type" HTTP header.
    """


class DateField(FieldURIField):
    """A field that handles date values."""

    value_cls = EWSDate

    def clean(self, value, version=None):
        # Allow plain datetime.date values as input
        if type(value) is datetime.date:
            value = self.value_cls.from_date(value)
        return super().clean(value=value, version=version)


class DateTimeBackedDateField(DateField):
    """A field that acts like a date, but where values are sent to EWS as EWSDateTime."""

    def __init__(self, *args, **kwargs):
        # Not all fields assume a default time of 00:00, so make this configurable
        self._default_time = kwargs.pop('default_time', datetime.time(0, 0))
        super().__init__(*args, **kwargs)
        # Create internal field to handle datetime-only logic
        self._datetime_field = DateTimeField(*args, **kwargs)

    def date_to_datetime(self, value):
        return self._datetime_field.value_cls.combine(value, self._default_time).replace(tzinfo=UTC)

    def from_xml(self, elem, account):
        val = self._get_val_from_elem(elem)
        if val is not None and len(val) == 25:
            # This is a datetime string with timezone info, e.g. '2021-03-01T21:55:54+00:00'. We don't want to have
            # datetime values converted to UTC before converting to date. EWSDateTime.from_string() insists on
            # converting to UTC, but we don't have an EWSTimeZone we can convert the timezone info to. Instead, parse
            # the string with .fromisoformat().
            return datetime.datetime.fromisoformat(val).date()
        # Revert to default parsing of datetime strings
        res = self._datetime_field.from_xml(elem=elem, account=account)
        if res is None:
            return res
        return res.date()

    def to_xml(self, value, version):
        # Convert date to datetime
        value = self.date_to_datetime(value)
        return self._datetime_field.to_xml(value=value, version=version)


class TimeField(FieldURIField):
    """A field that handles time values."""

    value_cls = datetime.time

    def from_xml(self, elem, account):
        val = self._get_val_from_elem(elem)
        if val is not None:
            try:
                if ':' in val:
                    # Assume a string of the form HH:MM:SS
                    return datetime.datetime.strptime(val, '%H:%M:%S').time()
                # Assume an integer in minutes since midnight
                return (datetime.datetime(2000, 1, 1) + datetime.timedelta(minutes=int(val))).time()
            except ValueError:
                pass
        return self.default


class DateTimeField(FieldURIField):
    """A field that handles datetime values."""

    value_cls = EWSDateTime

    def clean(self, value, version=None):
        if isinstance(value, datetime.datetime):
            if not value.tzinfo:
                raise ValueError("Value '%s' on field '%s' must be timezone aware" % (value, self.name))
            if type(value) is datetime.datetime:
                value = self.value_cls.from_datetime(value)
        return super().clean(value, version=version)

    def from_xml(self, elem, account):
        val = self._get_val_from_elem(elem)
        if val is not None:
            try:
                return self.value_cls.from_string(val)
            except ValueError as e:
                if isinstance(e, NaiveDateTimeNotAllowed):
                    # We encountered a naive datetime
                    if account:
                        # Convert to timezone-aware datetime using the default timezone of the account
                        tz = account.default_timezone
                        log.info('Found naive datetime %s on field %s. Assuming timezone %s', e.local_dt, self.name, tz)
                        return e.local_dt.replace(tzinfo=tz)
                    # There's nothing we can do but return the naive date. It's better than assuming e.g. UTC.
                    log.warning('Returning naive datetime %s on field %s', e.local_dt, self.name)
                    return e.local_dt
                log.info("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
                return None
        return self.default


class DateOrDateTimeField(DateTimeField):
    """This field can handle both EWSDate and EWSDateTime. Used for calendar items where 'start' and 'end'
    values are conceptually dates when the calendar item is an all-day event, but datetimes in all other cases, and
    for recurrences where the returned 'start' and 'end' values may be either dates or datetimes depending on whether
    the recurring item is a task or a calendar item.

    For all-day calendar items, we assume both start and end dates are inclusive.

    For filtering kwarg validation and other places where we must decide on a specific class, we settle on datetime.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Create internal field to handle date-only logic
        self._date_field = DateField(*args, **kwargs)

    def clean(self, value, version=None):
        # Most calendar items will contain datetime values. We can't access the is_all_day value here, so CalendarItem
        # must handle that sanity check.
        if type(value) in (EWSDate, datetime.date):
            return self._date_field.clean(value=value, version=version)
        return super().clean(value=value, version=version)

    def from_xml(self, elem, account):
        val = self._get_val_from_elem(elem)
        if val is not None and len(val) == 16:
            # This is a date format with timezone info, as sent by task recurrences. Eg: '2006-01-09+01:00'
            return self._date_field.from_xml(elem=elem, account=account)
        return super().from_xml(elem=elem, account=account)


class TimeZoneField(FieldURIField):
    """A field that handles timezone values."""

    value_cls = EWSTimeZone

    def clean(self, value, version=None):
        # Allow other timezone implementations as input
        if value is not None:
            value = self.value_cls.from_timezone(value)
        return super().clean(value=value, version=version)

    def from_xml(self, elem, account):
        field_elem = elem.find(self.response_tag())
        if field_elem is not None:
            ms_id = field_elem.get('Id')
            ms_name = field_elem.get('Name')
            try:
                return self.value_cls.from_ms_id(ms_id or ms_name)
            except UnknownTimeZone:
                log.warning(
                    "Cannot convert value '%s' on field '%s' to type %s (unknown timezone ID)",
                    (ms_id or ms_name), self.name, self.value_cls
                )
                return None
        return self.default

    def to_xml(self, value, version):
        attrs = OrderedDict([('Id', value.ms_id)])
        if value.ms_name:
            attrs['Name'] = value.ms_name
        return create_element(self.request_tag(), attrs=attrs)


class TextField(FieldURIField):
    """A field that stores a string value with no length limit."""

    value_cls = str
    is_complex = True


class TextListField(TextField):
    """Like TextField, but for lists of text."""

    is_list = True

    def from_xml(self, elem, account):
        iter_elem = elem.find(self.response_tag())
        if iter_elem is not None:
            return get_xml_attrs(iter_elem, '{%s}String' % TNS)
        return self.default


class MessageField(TextField):
    """A field that handles the Message element."""

    INNER_ELEMENT_NAME = 'Message'

    def from_xml(self, elem, account):
        reply = elem.find(self.response_tag())
        if reply is None:
            return None
        message = reply.find('{%s}%s' % (TNS, self.INNER_ELEMENT_NAME))
        if message is None:
            return None
        return message.text

    def to_xml(self, value, version):
        field_elem = create_element(self.request_tag())
        message = create_element('t:%s' % self.INNER_ELEMENT_NAME)
        message.text = value
        return set_xml_value(field_elem, message, version=version)


class CharField(TextField):
    """A field that stores a string value with a limited length."""

    is_complex = False

    def __init__(self, *args, **kwargs):
        self.max_length = kwargs.pop('max_length', 255)
        if not 1 <= self.max_length <= 255:
            # A field supporting messages longer than 255 chars should be TextField
            raise ValueError("'max_length' must be in the range 1-255")
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        value = super().clean(value, version=version)
        if value is not None:
            if self.is_list:
                for v in value:
                    if len(v) > self.max_length:
                        raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, v, self.max_length))
            else:
                if len(value) > self.max_length:
                    raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, value, self.max_length))
        return value


class IdField(CharField):
    """A field to hold the 'Id' and 'Changekey' attributes on 'ItemId' type items. There is no guaranteed max length,
    but we can assume 512 bytes in practice. See
    https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/ews-identifiers-in-exchange
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.max_length = 512  # This is above the normal 255 limit, but this is actually an attribute, not a field
        self.is_searchable = False
        self.is_attribute = True


class CharListField(CharField):
    """Like CharField, but for lists of strings."""

    is_list = True

    def __init__(self, *args, **kwargs):
        self.list_elem_name = kwargs.pop('list_elem_name', 'String')
        super().__init__(*args, **kwargs)

    def list_elem_tag(self):
        return '{%s}%s' % (self.namespace, self.list_elem_name)

    def from_xml(self, elem, account):
        iter_elem = elem.find(self.response_tag())
        if iter_elem is not None:
            return get_xml_attrs(iter_elem, self.list_elem_tag())
        return self.default


class URIField(TextField):
    """Helper to mark strings that must conform to xsd:anyURI.
    If we want an URI validator, see http://stackoverflow.com/questions/14466585/is-this-regex-correct-for-xsdanyuri
    """


class EmailAddressField(CharField):
    """A helper class used for email address string that we can use for email validation."""


class CultureField(CharField):
    """Helper to mark strings that are # RFC 1766 culture values."""


class Choice:
    """Implement versioned choices for the ChoiceField field."""

    def __init__(self, value, supported_from=None):
        self.value = value
        self.supported_from = supported_from

    def supports_version(self, version):
        # 'version' is a Version instance, for convenience by callers
        if not isinstance(version, Version):
            raise ValueError("'version' %r must be a Version instance" % version)
        if not self.supported_from:
            return True
        return version.build >= self.supported_from


class ChoiceField(CharField):
    """Like CharField, but restricts the value to a limited set of strings."""

    def __init__(self, *args, **kwargs):
        self.choices = kwargs.pop('choices')
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        value = super().clean(value, version=version)
        if value is None:
            return None
        valid_choices = [c.value for c in self.choices]
        if version:
            valid_choices_for_version = self.supported_choices(version=version)
            if value in valid_choices_for_version:
                return value
            if value in valid_choices:
                raise InvalidChoiceForVersion("Choice '%s' only supports EWS builds from %s to %s (server has %s)" % (
                    self.name, self.supported_from or '*', self.deprecated_from or '*', version))
        else:
            if value in valid_choices:
                return value
        raise ValueError("Invalid choice '%s' for field '%s'. Valid choices are: %s" % (
            value, self.name, ', '.join(valid_choices)
        ))

    def supported_choices(self, version):
        return [c.value for c in self.choices if c.supports_version(version)]


FREE_BUSY_CHOICES = [Choice('Free'), Choice('Tentative'), Choice('Busy'), Choice('OOF'), Choice('NoData'),
                     Choice('WorkingElsewhere', supported_from=EXCHANGE_2013)]


class FreeBusyStatusField(ChoiceField):
    """Like ChoiceField, but specifically for Free/Busy values."""

    def __init__(self, *args, **kwargs):
        kwargs['choices'] = set(FREE_BUSY_CHOICES)
        super().__init__(*args, **kwargs)


class BodyField(TextField):
    """A TextField with specific requirements for the Item body."""

    def __init__(self, *args, **kwargs):
        from .properties import Body
        self.value_cls = Body
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        if value is not None and not isinstance(value, self.value_cls):
            value = self.value_cls(value)
        return super().clean(value, version=version)

    def from_xml(self, elem, account):
        from .properties import Body, HTMLBody
        field_elem = elem.find(self.response_tag())
        val = None if field_elem is None else field_elem.text or None
        if val is not None:
            body_type = field_elem.get('BodyType')
            return {
                Body.body_type: Body,
                HTMLBody.body_type: HTMLBody,
            }[body_type](val)
        return self.default

    def to_xml(self, value, version):
        from .properties import Body, HTMLBody
        field_elem = create_element(self.request_tag())
        body_type = {
            Body: Body.body_type,
            HTMLBody: HTMLBody.body_type,
        }[type(value)]
        field_elem.set('BodyType', body_type)
        return set_xml_value(field_elem, value, version=version)


class EWSElementField(FieldURIField):
    """A generic field for any EWSElement object."""

    def __init__(self, *args, **kwargs):
        self._value_cls = kwargs.pop('value_cls')
        if 'namespace' not in kwargs:
            kwargs['namespace'] = self.value_cls.NAMESPACE
        super().__init__(*args, **kwargs)

    @property
    def value_cls(self):
        if isinstance(self._value_cls, str):
            # Support 'value_cls' as string to allow self-referencing classes. The class must be importable from the
            # top-level module.
            self._value_cls = getattr(import_module(self.__module__.split('.')[0]), self._value_cls)
        return self._value_cls

    def from_xml(self, elem, account):
        if self.is_list:
            iter_elem = elem.find(self.response_tag())
            if iter_elem is not None:
                return [self.value_cls.from_xml(elem=e, account=account)
                        for e in iter_elem.findall(self.value_cls.response_tag())]
        else:
            if self.field_uri is None:
                sub_elem = elem.find(self.value_cls.response_tag())
            else:
                sub_elem = elem.find(self.response_tag())
            if sub_elem is not None:
                return self.value_cls.from_xml(elem=sub_elem, account=account)
        return self.default

    def to_xml(self, value, version):
        if self.field_uri is None:
            return value.to_xml(version=version)
        field_elem = create_element(self.request_tag())
        return set_xml_value(field_elem, value, version=version)


class EWSElementListField(EWSElementField):
    """Like EWSElementField, but for lists of EWSElement objects."""

    is_list = True
    is_complex = True


class AssociatedCalendarItemIdField(EWSElementField):
    is_complex = True

    def __init__(self, *args, **kwargs):
        from .properties import AssociatedCalendarItemId
        kwargs['value_cls'] = AssociatedCalendarItemId
        super().__init__(*args, **kwargs)

    def to_xml(self, value, version):
        return value.to_xml(version=version)


class RecurrenceField(EWSElementField):
    is_complex = True

    def __init__(self, *args, **kwargs):
        from .recurrence import Recurrence
        kwargs['value_cls'] = Recurrence
        super().__init__(*args, **kwargs)

    def to_xml(self, value, version):
        return value.to_xml(version=version)


class TaskRecurrenceField(EWSElementField):
    is_complex = True

    def __init__(self, *args, **kwargs):
        from .recurrence import TaskRecurrence
        kwargs['value_cls'] = TaskRecurrence
        super().__init__(*args, **kwargs)

    def to_xml(self, value, version):
        return value.to_xml(version=version)


class ReferenceItemIdField(EWSElementField):
    is_complex = True

    def __init__(self, *args, **kwargs):
        from .properties import ReferenceItemId
        kwargs['value_cls'] = ReferenceItemId
        super().__init__(*args, **kwargs)

    def to_xml(self, value, version):
        return value.to_xml(version=version)


class OccurrenceField(EWSElementField):
    is_complex = True


class OccurrenceListField(OccurrenceField):
    is_list = True


class MessageHeaderField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import MessageHeader
        kwargs['value_cls'] = MessageHeader
        super().__init__(*args, **kwargs)


class BaseEmailField(EWSElementField):
    """A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for."""

    is_complex = True  # FindItem only returns the name, not the email address

    def clean(self, value, version=None):
        if isinstance(value, str):
            value = self.value_cls(email_address=value)
        return super().clean(value, version=version)

    def from_xml(self, elem, account):
        if self.field_uri is None:
            sub_elem = elem.find(self.value_cls.response_tag())
        else:
            sub_elem = elem.find(self.response_tag())
        if sub_elem is not None:
            if self.field_uri is not None:
                # We want the nested Mailbox, not the wrapper element
                nested_elem = sub_elem.find(self.value_cls.response_tag())
                if nested_elem is None:
                    raise ValueError(
                        'Expected XML element %r missing on field %r' % (self.value_cls.response_tag(), self.name)
                    )
                return self.value_cls.from_xml(elem=nested_elem, account=account)
            return self.value_cls.from_xml(elem=sub_elem, account=account)
        return self.default


class EmailField(BaseEmailField):
    def __init__(self, *args, **kwargs):
        from .properties import Email
        kwargs['value_cls'] = Email
        super().__init__(*args, **kwargs)


class RecipientAddressField(BaseEmailField):
    def __init__(self, *args, **kwargs):
        from .properties import RecipientAddress
        kwargs['value_cls'] = RecipientAddress
        super().__init__(*args, **kwargs)


class MailboxField(BaseEmailField):
    def __init__(self, *args, **kwargs):
        from .properties import Mailbox
        kwargs['value_cls'] = Mailbox
        super().__init__(*args, **kwargs)


class MailboxListField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import Mailbox
        kwargs['value_cls'] = Mailbox
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        if value is not None:
            value = [self.value_cls(email_address=s) if isinstance(s, str) else s for s in value]
        return super().clean(value, version=version)


class MemberListField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import Member
        kwargs['value_cls'] = Member
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        from .properties import Mailbox
        if value is not None:
            value = [
                self.value_cls(mailbox=Mailbox(email_address=s)) if isinstance(s, str) else s for s in value
            ]
        return super().clean(value, version=version)


class AttendeesField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import Attendee
        kwargs['value_cls'] = Attendee
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        from .properties import Mailbox
        if value is not None:
            value = [self.value_cls(mailbox=Mailbox(email_address=s), response_type='Accept')
                     if isinstance(s, str) else s for s in value]
        return super().clean(value, version=version)


class AttachmentField(EWSElementListField):
    """A field for item attachments."""

    def __init__(self, *args, **kwargs):
        from .attachments import Attachment
        kwargs['value_cls'] = Attachment
        super().__init__(*args, **kwargs)

    def from_xml(self, elem, account):
        from .attachments import FileAttachment, ItemAttachment
        iter_elem = elem.find(self.response_tag())
        # Look for both FileAttachment and ItemAttachment
        if iter_elem is not None:
            attachments = []
            for att_type in (ItemAttachment, FileAttachment):
                attachments.extend(
                    [att_type.from_xml(elem=e, account=account) for e in iter_elem.findall(att_type.response_tag())]
                )
            return attachments
        return self.default


class LabelField(ChoiceField):
    """A field to hold the label on an IndexedElement."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.is_attribute = True

    def from_xml(self, elem, account):
        return elem.get(self.field_uri)


class SubField(Field):
    """A field to hold the value on an IndexedElement."""

    namespace = TNS
    value_cls = str

    def from_xml(self, elem, account):
        return elem.text

    def to_xml(self, value, version):
        return value

    @staticmethod
    def field_uri_xml(field_uri, label):
        from .properties import IndexedFieldURI
        return IndexedFieldURI(field_uri=field_uri, field_index=label).to_xml(version=None)

    def clean(self, value, version=None):
        value = super().clean(value, version=version)
        if self.is_required and not value:
            raise ValueError('Value for subfield %r must be non-empty' % self.name)
        return value

    def __hash__(self):
        return hash(self.name)


class EmailSubField(SubField):
    """A field to hold the value on an SingleFieldIndexedElement."""

    value_cls = str

    def from_xml(self, elem, account):
        return elem.text or elem.get('Name')  # Sometimes elem.text is empty. Exchange saves the same in 'Name' attr


class NamedSubField(SubField):
    """A field to hold the value on an MultiFieldIndexedElement."""

    value_cls = str

    def __init__(self, *args, **kwargs):
        self.field_uri = kwargs.pop('field_uri')
        if ':' in self.field_uri:
            raise ValueError("'field_uri' value must not contain a colon")
        super().__init__(*args, **kwargs)

    def from_xml(self, elem, account):
        field_elem = elem.find(self.response_tag())
        val = None if field_elem is None else field_elem.text or None
        if val is not None:
            return val
        return self.default

    def to_xml(self, value, version):
        field_elem = create_element(self.request_tag())
        return set_xml_value(field_elem, value, version=version)

    def field_uri_xml(self, field_uri, label):
        from .properties import IndexedFieldURI
        return IndexedFieldURI(field_uri='%s:%s' % (field_uri, self.field_uri), field_index=label).to_xml(version=None)

    def request_tag(self):
        return 't:%s' % self.field_uri

    def response_tag(self):
        return '{%s}%s' % (self.namespace, self.field_uri)


class IndexedField(EWSElementField):
    """A base class for all indexed fields."""

    PARENT_ELEMENT_NAME = None

    def __init__(self, *args, **kwargs):
        from .indexed_properties import IndexedElement
        value_cls = kwargs['value_cls']
        if not issubclass(value_cls, IndexedElement):
            raise ValueError("'value_cls' %r must be a subclass of IndexedElement" % value_cls)
        super().__init__(*args, **kwargs)

    def to_xml(self, value, version):
        return set_xml_value(create_element('t:%s' % self.PARENT_ELEMENT_NAME), value, version)

    def response_tag(self):
        return '{%s}%s' % (self.namespace, self.PARENT_ELEMENT_NAME)

    def __hash__(self):
        return hash(self.field_uri)


class EmailAddressesField(IndexedField):
    is_list = True

    PARENT_ELEMENT_NAME = 'EmailAddresses'

    def __init__(self, *args, **kwargs):
        from .indexed_properties import EmailAddress
        kwargs['value_cls'] = EmailAddress
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        if value is not None:
            default_labels = self.value_cls.LABEL_CHOICES
            if len(value) > len(default_labels):
                raise ValueError('This field can handle at most %s values (value: %r)' % (len(default_labels), value))
            tmp = []
            for s, default_label in zip(value, default_labels):
                if not isinstance(s, str):
                    tmp.append(s)
                    continue
                tmp.append(self.value_cls(email=s, label=default_label))
            value = tmp
        return super().clean(value, version=version)


class PhoneNumberField(IndexedField):
    is_list = True

    PARENT_ELEMENT_NAME = 'PhoneNumbers'

    def __init__(self, *args, **kwargs):
        from .indexed_properties import PhoneNumber
        kwargs['value_cls'] = PhoneNumber
        super().__init__(*args, **kwargs)


class PhysicalAddressField(IndexedField):
    is_list = True

    PARENT_ELEMENT_NAME = 'PhysicalAddresses'

    def __init__(self, *args, **kwargs):
        from .indexed_properties import PhysicalAddress
        kwargs['value_cls'] = PhysicalAddress
        super().__init__(*args, **kwargs)


class ExtendedPropertyField(Field):
    def __init__(self, *args, **kwargs):
        self.value_cls = kwargs.pop('value_cls')
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        if value is None:
            if self.is_required:
                raise ValueError("'%s' is a required field" % self.name)
            return self.default
        if not isinstance(value, self.value_cls):
            # Allow keeping ExtendedProperty field values as their simple Python type, but run clean() anyway
            tmp = self.value_cls(value)
            tmp.clean(version=version)
            return value
        value.clean(version=version)
        return value

    def field_uri_xml(self):
        from .properties import ExtendedFieldURI
        cls = self.value_cls
        return ExtendedFieldURI(
            distinguished_property_set_id=cls.distinguished_property_set_id,
            property_set_id=cls.property_set_id.lower() if cls.property_set_id else None,
            property_tag=cls.property_tag_as_hex(),
            property_name=cls.property_name,
            property_id=value_to_xml_text(cls.property_id) if cls.property_id else None,
            property_type=cls.property_type,
        ).to_xml(version=None)

    def from_xml(self, elem, account):
        extended_properties = elem.findall(self.value_cls.response_tag())
        for extended_property in extended_properties:
            if self.value_cls.is_property_instance(extended_property):
                return self.value_cls.from_xml(elem=extended_property, account=account)
        return self.default

    def to_xml(self, value, version):
        extended_property = create_element(self.value_cls.request_tag())
        set_xml_value(extended_property, self.field_uri_xml(), version=version)
        if isinstance(value, self.value_cls):
            set_xml_value(extended_property, value, version=version)
        else:
            # Allow keeping ExtendedProperty field values as their simple Python type
            set_xml_value(extended_property, self.value_cls(value), version=version)
        return extended_property

    def __hash__(self):
        return hash(self.name)


class ItemField(FieldURIField):
    @property
    def value_cls(self):
        from .items import Item
        return Item

    def from_xml(self, elem, account):
        from .items import ITEM_CLASSES
        for item_cls in ITEM_CLASSES:
            item_elem = elem.find(item_cls.response_tag())
            if item_elem is not None:
                return item_cls.from_xml(elem=item_elem, account=account)
        return None

    def to_xml(self, value, version):
        # We don't want to wrap in an Item element
        return value.to_xml(version=version)


class UnknownEntriesField(CharListField):
    def list_elem_tag(self):
        return '{%s}UnknownEntry' % self.namespace


class PermissionSetField(EWSElementField):
    is_complex = True

    def __init__(self, *args, **kwargs):
        from .properties import PermissionSet
        kwargs['value_cls'] = PermissionSet
        super().__init__(*args, **kwargs)


class EffectiveRightsField(EWSElementField):
    def __init__(self, *args, **kwargs):
        from .properties import EffectiveRights
        kwargs['value_cls'] = EffectiveRights
        super().__init__(*args, **kwargs)


class BuildField(CharField):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.value_cls = Build

    def from_xml(self, elem, account):
        val = self._get_val_from_elem(elem)
        if val:
            try:
                return self.value_cls.from_hex_string(val)
            except (TypeError, ValueError):
                log.warning('Invalid server version string: %r', val)
        return val


class ProtocolListField(EWSElementListField):
    # There is not containing element for this field. Just multiple 'Protocol' elements on the 'Account' element.
    def __init__(self, *args, **kwargs):
        from .autodiscover.properties import Protocol
        kwargs['value_cls'] = Protocol
        super().__init__(*args, **kwargs)

    def from_xml(self, elem, account):
        return [self.value_cls.from_xml(elem=e, account=account) for e in elem.findall(self.value_cls.response_tag())]


class RoutingTypeField(ChoiceField):
    def __init__(self, *args, **kwargs):
        kwargs['choices'] = {Choice('SMTP'), Choice('EX')}
        kwargs['default'] = 'SMTP'
        super().__init__(*args, **kwargs)


class IdElementField(EWSElementField):
    def __init__(self, *args, **kwargs):
        kwargs['is_searchable'] = False
        kwargs['is_read_only'] = True
        super().__init__(*args, **kwargs)


class TypeValueField(FieldURIField):
    """This field type has no value_cls because values may have many different types."""

    TYPES_MAP = {
        'Boolean': bool,
        'Integer32': int,
        'UnsignedInteger32': int,
        'Integer64': int,
        'UnsignedInteger64': int,
        # Python doesn't have a single-byte type to represent 'Byte'
        'ByteArray': bytes,
        'String': str,
        'StringArray': str,  # A list of strings
        'DateTime': EWSDateTime,
    }
    TYPES_MAP_REVERSED = {
        bool: 'Boolean',
        int: 'Integer64',
        # Python doesn't have a single-byte type to represent 'Byte'
        bytes: 'ByteArray',
        str: 'String',
        datetime.datetime: 'DateTime',
        EWSDateTime: 'DateTime',
    }

    @classmethod
    def get_type(cls, value):
        if isinstance(value, bytes) and len(value) == 1:
            # This is a single byte. Translate it to the 'Byte' type
            return 'Byte'
        if is_iterable(value):
            # We don't allow generators as values, so keep the logic simple
            try:
                first = next(iter(value))
            except StopIteration:
                first = None
            value_type = '%sArray' % cls.TYPES_MAP_REVERSED[type(first)]
            if value_type not in cls.TYPES_MAP:
                raise ValueError('%r is not a supported type' % value)
            return value_type
        return cls.TYPES_MAP_REVERSED[type(value)]

    @classmethod
    def is_array_type(cls, value_type):
        return value_type == 'StringArray'

    def clean(self, value, version=None):
        if value is None:
            if self.is_required and self.default is None:
                raise ValueError("'%s' is a required field with no default" % self.name)
            return self.default
        return value

    def from_xml(self, elem, account):
        field_elem = elem.find(self.response_tag())
        if field_elem is None:
            return self.default
        value_type_str = get_xml_attr(field_elem, '{%s}Type' % TNS)
        value = get_xml_attr(field_elem, '{%s}Value' % TNS)
        if value_type_str == 'Byte':
            try:
                # The value is an unsigned integer in the range 0 -> 255. Convert it to a single byte
                return xml_text_to_value(value, int).to_bytes(1, 'little', signed=False)
            except OverflowError as e:
                log.warning('Invalid byte value %r (%e)', value, e)
                return None
        value_type = self.TYPES_MAP[value_type_str]
        if self. is_array_type(value_type_str):
            return tuple(xml_text_to_value(value=v, value_type=value_type) for v in value.split(' '))
        return xml_text_to_value(value=value, value_type=value_type)

    def to_xml(self, value, version):
        value_type_str = self.get_type(value)
        if value_type_str == 'Byte':
            # A single byte is encoded to an unsigned integer in the range 0 -> 255
            value = int.from_bytes(value, byteorder='little', signed=False)
        elif is_iterable(value):
            value = ' '.join(value_to_xml_text(v) for v in value)
        field_elem = create_element(self.request_tag())
        field_elem.append(set_xml_value(create_element('t:Type'), value_type_str, version=version))
        field_elem.append(set_xml_value(create_element('t:Value'), value, version=version))
        return field_elem


class DictionaryField(FieldURIField):
    value_cls = dict

    def from_xml(self, elem, account):
        from .properties import DictionaryEntry
        iter_elem = elem.find(self.response_tag())
        if iter_elem is not None:
            entries = [
                DictionaryEntry.from_xml(elem=e, account=account)
                for e in iter_elem.findall(DictionaryEntry.response_tag())
            ]
            return {e.key: e.value for e in entries}
        return self.default

    def clean(self, value, version=None):
        if isinstance(value, dict):
            cleaned = {}
            for k, v in value.items():
                if type(k) is datetime.datetime:
                    k = EWSDateTime.from_datetime(k)
                if type(v) is datetime.datetime:
                    v = EWSDateTime.from_datetime(v)
                cleaned[k] = v
            value = cleaned
        return super().clean(value=value, version=version)

    def to_xml(self, value, version):
        from .properties import DictionaryEntry
        field_elem = create_element(self.request_tag())
        entries = [DictionaryEntry(key=k, value=v) for k, v in value.items()]
        return set_xml_value(field_elem, entries, version=version)


class PersonaPhoneNumberField(EWSElementField):
    is_complex = True

    def __init__(self, *args, **kwargs):
        from .properties import PhoneNumber
        kwargs['value_cls'] = PhoneNumber
        super().__init__(*args, **kwargs)


class BodyContentAttributedValueField(EWSElementField):
    is_complex = True

    def __init__(self, *args, **kwargs):
        from .properties import BodyContentAttributedValue
        kwargs['value_cls'] = BodyContentAttributedValue
        super().__init__(*args, **kwargs)


class StringAttributedValueField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import StringAttributedValue
        kwargs['value_cls'] = StringAttributedValue
        super().__init__(*args, **kwargs)


class PhoneNumberAttributedValueField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import PhoneNumberAttributedValue
        kwargs['value_cls'] = PhoneNumberAttributedValue
        super().__init__(*args, **kwargs)


class EmailAddressAttributedValueField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import EmailAddressAttributedValue
        kwargs['value_cls'] = EmailAddressAttributedValue
        super().__init__(*args, **kwargs)


class PostalAddressAttributedValueField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import PostalAddressAttributedValue
        kwargs['value_cls'] = PostalAddressAttributedValue
        super().__init__(*args, **kwargs)


class GenericEventListField(EWSElementField):
    """A list field that can contain all subclasses of Event."""

    is_list = True

    @property
    def _event_types_map(self):
        return {v.response_tag(): v for v in self.value_classes}

    def __init__(self, *args, **kwargs):
        from .properties import CopiedEvent, CreatedEvent, DeletedEvent, ModifiedEvent, MovedEvent, \
            NewMailEvent, StatusEvent, FreeBusyChangedEvent
        kwargs['value_cls'] = None  # Parent class requires this kwarg
        kwargs['namespace'] = None  # Parent class requires this kwarg
        super().__init__(*args, **kwargs)
        self.value_classes = (
            CopiedEvent, CreatedEvent, DeletedEvent, ModifiedEvent, MovedEvent, NewMailEvent, StatusEvent,
            FreeBusyChangedEvent,
        )

    def from_xml(self, elem, account):
        events = []
        for event in elem:
            # This may or may not be an event element. Could also be other child elements of Notification
            try:
                value_cls = self._event_types_map[event.tag]
            except KeyError:
                continue
            events.append(value_cls.from_xml(elem=event, account=account))
        return events or self.default

Functions

def resolve_field_path(field_path, folder, strict=True)

Take the name of a field, or '__'-delimited path to a subfield, and return the corresponding Field object, label and SubField object.

Expand source code
def resolve_field_path(field_path, folder, strict=True):
    """Take the name of a field, or '__'-delimited path to a subfield, and return the corresponding Field object,
    label and SubField object.
    """
    from .indexed_properties import SingleFieldIndexedElement, MultiFieldIndexedElement
    fieldname, label, subfieldname = split_field_path(field_path)
    field = folder.get_item_field_by_fieldname(fieldname)
    subfield = None
    if isinstance(field, IndexedField):
        if strict and not label:
            raise ValueError(
                "IndexedField path '%s' must specify label, e.g. '%s__%s'"
                % (field_path, fieldname, field.value_cls.get_field_by_fieldname('label').default)
            )
        valid_labels = field.value_cls.get_field_by_fieldname('label').supported_choices(
            version=folder.account.version
        )
        if label and label not in valid_labels:
            raise ValueError(
                "Label '%s' on IndexedField path '%s' must be one of %s"
                % (label, field_path, ', '.join(valid_labels))
            )
        if issubclass(field.value_cls, MultiFieldIndexedElement):
            if strict and not subfieldname:
                raise ValueError(
                    "IndexedField path '%s' must specify subfield, e.g. '%s__%s__%s'"
                    % (field_path, fieldname, label, field.value_cls.FIELDS[1].name)
                )

            if subfieldname:
                try:
                    subfield = field.value_cls.get_field_by_fieldname(subfieldname)
                except ValueError:
                    fnames = ', '.join(f.name for f in field.value_cls.supported_fields(
                        version=folder.account.version
                    ))
                    raise ValueError(
                        "Subfield '%s' on IndexedField path '%s' must be one of %s"
                        % (subfieldname, field_path, fnames)
                    )
        else:
            if not issubclass(field.value_cls, SingleFieldIndexedElement):
                raise ValueError("'field.value_cls' %r must be an SingleFieldIndexedElement instance" % field.value_cls)
            if subfieldname:
                raise ValueError(
                    "IndexedField path '%s' must not specify subfield, e.g. just '%s__%s'"
                    % (field_path, fieldname, label)
                )
            subfield = field.value_cls.value_field(version=folder.account.version)
    else:
        if label or subfieldname:
            raise ValueError(
                "Field path '%s' must not specify label or subfield, e.g. just '%s'"
                % (field_path, fieldname)
            )
    return field, label, subfield
def split_field_path(field_path)

Split a string path into its field, label and subfield parts.

:param field_path:

:return Examples: 'start' -> ('start', None, None) 'phone_numbers__PrimaryPhone' -> ('phone_numbers', 'PrimaryPhone', None) 'physical_addresses__Home__street' -> ('physical_addresses', 'Home', 'street')

Expand source code
def split_field_path(field_path):
    """Split a string path into its field, label and subfield parts.

    :param field_path:

    :return Examples:
      'start' -> ('start', None, None)
      'phone_numbers__PrimaryPhone' -> ('phone_numbers', 'PrimaryPhone', None)
      'physical_addresses__Home__street' -> ('physical_addresses', 'Home', 'street')
    """
    if not isinstance(field_path, str):
        raise ValueError("Field path %r must be a string" % field_path)
    search_parts = field_path.split('__')
    field = search_parts[0]
    try:
        label = search_parts[1]
    except IndexError:
        label = None
    try:
        subfield = search_parts[2]
    except IndexError:
        subfield = None
    return field, label, subfield

Classes

class AppointmentStateField (*args, **kwargs)
Expand source code
class AppointmentStateField(IntegerField):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/appointmentstate"""

    NONE = 'None'
    MEETING = 'Meeting'
    RECEIVED = 'Received'
    CANCELLED = 'Cancelled'
    STATES = {
        NONE: 0x0000,
        MEETING: 0x0001,
        RECEIVED: 0x0002,
        CANCELLED: 0x0004,
    }

    def from_xml(self, elem, account):
        val = super().from_xml(elem=elem, account=account)
        if val is None:
            return val
        return tuple(name for name, mask in self.STATES.items() if bool(val & mask))

Ancestors

Class variables

var CANCELLED
var MEETING
var NONE
var RECEIVED
var STATES

Methods

def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    val = super().from_xml(elem=elem, account=account)
    if val is None:
        return val
    return tuple(name for name, mask in self.STATES.items() if bool(val & mask))

Inherited members

class AssociatedCalendarItemIdField (*args, **kwargs)

A generic field for any EWSElement object.

Expand source code
class AssociatedCalendarItemIdField(EWSElementField):
    is_complex = True

    def __init__(self, *args, **kwargs):
        from .properties import AssociatedCalendarItemId
        kwargs['value_cls'] = AssociatedCalendarItemId
        super().__init__(*args, **kwargs)

    def to_xml(self, value, version):
        return value.to_xml(version=version)

Ancestors

Class variables

var is_complex

Methods

def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    return value.to_xml(version=version)
class AttachmentField (*args, **kwargs)

A field for item attachments.

Expand source code
class AttachmentField(EWSElementListField):
    """A field for item attachments."""

    def __init__(self, *args, **kwargs):
        from .attachments import Attachment
        kwargs['value_cls'] = Attachment
        super().__init__(*args, **kwargs)

    def from_xml(self, elem, account):
        from .attachments import FileAttachment, ItemAttachment
        iter_elem = elem.find(self.response_tag())
        # Look for both FileAttachment and ItemAttachment
        if iter_elem is not None:
            attachments = []
            for att_type in (ItemAttachment, FileAttachment):
                attachments.extend(
                    [att_type.from_xml(elem=e, account=account) for e in iter_elem.findall(att_type.response_tag())]
                )
            return attachments
        return self.default

Ancestors

Methods

def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    from .attachments import FileAttachment, ItemAttachment
    iter_elem = elem.find(self.response_tag())
    # Look for both FileAttachment and ItemAttachment
    if iter_elem is not None:
        attachments = []
        for att_type in (ItemAttachment, FileAttachment):
            attachments.extend(
                [att_type.from_xml(elem=e, account=account) for e in iter_elem.findall(att_type.response_tag())]
            )
        return attachments
    return self.default
class AttendeesField (*args, **kwargs)

Like EWSElementField, but for lists of EWSElement objects.

Expand source code
class AttendeesField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import Attendee
        kwargs['value_cls'] = Attendee
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        from .properties import Mailbox
        if value is not None:
            value = [self.value_cls(mailbox=Mailbox(email_address=s), response_type='Accept')
                     if isinstance(s, str) else s for s in value]
        return super().clean(value, version=version)

Ancestors

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    from .properties import Mailbox
    if value is not None:
        value = [self.value_cls(mailbox=Mailbox(email_address=s), response_type='Accept')
                 if isinstance(s, str) else s for s in value]
    return super().clean(value, version=version)
class Base64Field (*args, **kwargs)

A field that handles binary data and automatically Base64 encodes and decodes the data.

Expand source code
class Base64Field(FieldURIField):
    """A field that handles binary data and automatically Base64 encodes and decodes the data."""

    value_cls = bytes
    is_complex = True

    def __init__(self, *args, **kwargs):
        if 'is_searchable' not in kwargs:
            kwargs['is_searchable'] = False
        super().__init__(*args, **kwargs)

Ancestors

Subclasses

Class variables

var is_complex
var value_cls

bytes(iterable_of_ints) -> bytes bytes(string, encoding[, errors]) -> bytes bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer bytes(int) -> bytes object of size given by the parameter initialized with null bytes bytes() -> empty bytes object

Construct an immutable array of bytes from: - an iterable yielding integers in range(256) - a text string encoded using the specified encoding - any object implementing the buffer API. - an integer

class BaseEmailField (*args, **kwargs)

A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for.

Expand source code
class BaseEmailField(EWSElementField):
    """A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for."""

    is_complex = True  # FindItem only returns the name, not the email address

    def clean(self, value, version=None):
        if isinstance(value, str):
            value = self.value_cls(email_address=value)
        return super().clean(value, version=version)

    def from_xml(self, elem, account):
        if self.field_uri is None:
            sub_elem = elem.find(self.value_cls.response_tag())
        else:
            sub_elem = elem.find(self.response_tag())
        if sub_elem is not None:
            if self.field_uri is not None:
                # We want the nested Mailbox, not the wrapper element
                nested_elem = sub_elem.find(self.value_cls.response_tag())
                if nested_elem is None:
                    raise ValueError(
                        'Expected XML element %r missing on field %r' % (self.value_cls.response_tag(), self.name)
                    )
                return self.value_cls.from_xml(elem=nested_elem, account=account)
            return self.value_cls.from_xml(elem=sub_elem, account=account)
        return self.default

Ancestors

Subclasses

Class variables

var is_complex

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    if isinstance(value, str):
        value = self.value_cls(email_address=value)
    return super().clean(value, version=version)
def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    if self.field_uri is None:
        sub_elem = elem.find(self.value_cls.response_tag())
    else:
        sub_elem = elem.find(self.response_tag())
    if sub_elem is not None:
        if self.field_uri is not None:
            # We want the nested Mailbox, not the wrapper element
            nested_elem = sub_elem.find(self.value_cls.response_tag())
            if nested_elem is None:
                raise ValueError(
                    'Expected XML element %r missing on field %r' % (self.value_cls.response_tag(), self.name)
                )
            return self.value_cls.from_xml(elem=nested_elem, account=account)
        return self.value_cls.from_xml(elem=sub_elem, account=account)
    return self.default
class BodyContentAttributedValueField (*args, **kwargs)

A generic field for any EWSElement object.

Expand source code
class BodyContentAttributedValueField(EWSElementField):
    is_complex = True

    def __init__(self, *args, **kwargs):
        from .properties import BodyContentAttributedValue
        kwargs['value_cls'] = BodyContentAttributedValue
        super().__init__(*args, **kwargs)

Ancestors

Class variables

var is_complex
class BodyField (*args, **kwargs)

A TextField with specific requirements for the Item body.

Expand source code
class BodyField(TextField):
    """A TextField with specific requirements for the Item body."""

    def __init__(self, *args, **kwargs):
        from .properties import Body
        self.value_cls = Body
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        if value is not None and not isinstance(value, self.value_cls):
            value = self.value_cls(value)
        return super().clean(value, version=version)

    def from_xml(self, elem, account):
        from .properties import Body, HTMLBody
        field_elem = elem.find(self.response_tag())
        val = None if field_elem is None else field_elem.text or None
        if val is not None:
            body_type = field_elem.get('BodyType')
            return {
                Body.body_type: Body,
                HTMLBody.body_type: HTMLBody,
            }[body_type](val)
        return self.default

    def to_xml(self, value, version):
        from .properties import Body, HTMLBody
        field_elem = create_element(self.request_tag())
        body_type = {
            Body: Body.body_type,
            HTMLBody: HTMLBody.body_type,
        }[type(value)]
        field_elem.set('BodyType', body_type)
        return set_xml_value(field_elem, value, version=version)

Ancestors

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    if value is not None and not isinstance(value, self.value_cls):
        value = self.value_cls(value)
    return super().clean(value, version=version)
def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    from .properties import Body, HTMLBody
    field_elem = elem.find(self.response_tag())
    val = None if field_elem is None else field_elem.text or None
    if val is not None:
        body_type = field_elem.get('BodyType')
        return {
            Body.body_type: Body,
            HTMLBody.body_type: HTMLBody,
        }[body_type](val)
    return self.default
def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    from .properties import Body, HTMLBody
    field_elem = create_element(self.request_tag())
    body_type = {
        Body: Body.body_type,
        HTMLBody: HTMLBody.body_type,
    }[type(value)]
    field_elem.set('BodyType', body_type)
    return set_xml_value(field_elem, value, version=version)

Inherited members

class BooleanField (*args, **kwargs)

A field that handles boolean values.

Expand source code
class BooleanField(FieldURIField):
    """A field that handles boolean values."""

    value_cls = bool

Ancestors

Subclasses

Class variables

var value_cls

bool(x) -> bool

Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

class BuildField (*args, **kwargs)

A field that stores a string value with a limited length.

Expand source code
class BuildField(CharField):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.value_cls = Build

    def from_xml(self, elem, account):
        val = self._get_val_from_elem(elem)
        if val:
            try:
                return self.value_cls.from_hex_string(val)
            except (TypeError, ValueError):
                log.warning('Invalid server version string: %r', val)
        return val

Ancestors

Methods

def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    val = self._get_val_from_elem(elem)
    if val:
        try:
            return self.value_cls.from_hex_string(val)
        except (TypeError, ValueError):
            log.warning('Invalid server version string: %r', val)
    return val

Inherited members

class CharField (*args, **kwargs)

A field that stores a string value with a limited length.

Expand source code
class CharField(TextField):
    """A field that stores a string value with a limited length."""

    is_complex = False

    def __init__(self, *args, **kwargs):
        self.max_length = kwargs.pop('max_length', 255)
        if not 1 <= self.max_length <= 255:
            # A field supporting messages longer than 255 chars should be TextField
            raise ValueError("'max_length' must be in the range 1-255")
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        value = super().clean(value, version=version)
        if value is not None:
            if self.is_list:
                for v in value:
                    if len(v) > self.max_length:
                        raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, v, self.max_length))
            else:
                if len(value) > self.max_length:
                    raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, value, self.max_length))
        return value

Ancestors

Subclasses

Class variables

var is_complex

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    value = super().clean(value, version=version)
    if value is not None:
        if self.is_list:
            for v in value:
                if len(v) > self.max_length:
                    raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, v, self.max_length))
        else:
            if len(value) > self.max_length:
                raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, value, self.max_length))
    return value

Inherited members

class CharListField (*args, **kwargs)

Like CharField, but for lists of strings.

Expand source code
class CharListField(CharField):
    """Like CharField, but for lists of strings."""

    is_list = True

    def __init__(self, *args, **kwargs):
        self.list_elem_name = kwargs.pop('list_elem_name', 'String')
        super().__init__(*args, **kwargs)

    def list_elem_tag(self):
        return '{%s}%s' % (self.namespace, self.list_elem_name)

    def from_xml(self, elem, account):
        iter_elem = elem.find(self.response_tag())
        if iter_elem is not None:
            return get_xml_attrs(iter_elem, self.list_elem_tag())
        return self.default

Ancestors

Subclasses

Class variables

var is_list

Methods

def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    iter_elem = elem.find(self.response_tag())
    if iter_elem is not None:
        return get_xml_attrs(iter_elem, self.list_elem_tag())
    return self.default
def list_elem_tag(self)
Expand source code
def list_elem_tag(self):
    return '{%s}%s' % (self.namespace, self.list_elem_name)

Inherited members

class Choice (value, supported_from=None)

Implement versioned choices for the ChoiceField field.

Expand source code
class Choice:
    """Implement versioned choices for the ChoiceField field."""

    def __init__(self, value, supported_from=None):
        self.value = value
        self.supported_from = supported_from

    def supports_version(self, version):
        # 'version' is a Version instance, for convenience by callers
        if not isinstance(version, Version):
            raise ValueError("'version' %r must be a Version instance" % version)
        if not self.supported_from:
            return True
        return version.build >= self.supported_from

Methods

def supports_version(self, version)
Expand source code
def supports_version(self, version):
    # 'version' is a Version instance, for convenience by callers
    if not isinstance(version, Version):
        raise ValueError("'version' %r must be a Version instance" % version)
    if not self.supported_from:
        return True
    return version.build >= self.supported_from
class ChoiceField (*args, **kwargs)

Like CharField, but restricts the value to a limited set of strings.

Expand source code
class ChoiceField(CharField):
    """Like CharField, but restricts the value to a limited set of strings."""

    def __init__(self, *args, **kwargs):
        self.choices = kwargs.pop('choices')
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        value = super().clean(value, version=version)
        if value is None:
            return None
        valid_choices = [c.value for c in self.choices]
        if version:
            valid_choices_for_version = self.supported_choices(version=version)
            if value in valid_choices_for_version:
                return value
            if value in valid_choices:
                raise InvalidChoiceForVersion("Choice '%s' only supports EWS builds from %s to %s (server has %s)" % (
                    self.name, self.supported_from or '*', self.deprecated_from or '*', version))
        else:
            if value in valid_choices:
                return value
        raise ValueError("Invalid choice '%s' for field '%s'. Valid choices are: %s" % (
            value, self.name, ', '.join(valid_choices)
        ))

    def supported_choices(self, version):
        return [c.value for c in self.choices if c.supports_version(version)]

Ancestors

Subclasses

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    value = super().clean(value, version=version)
    if value is None:
        return None
    valid_choices = [c.value for c in self.choices]
    if version:
        valid_choices_for_version = self.supported_choices(version=version)
        if value in valid_choices_for_version:
            return value
        if value in valid_choices:
            raise InvalidChoiceForVersion("Choice '%s' only supports EWS builds from %s to %s (server has %s)" % (
                self.name, self.supported_from or '*', self.deprecated_from or '*', version))
    else:
        if value in valid_choices:
            return value
    raise ValueError("Invalid choice '%s' for field '%s'. Valid choices are: %s" % (
        value, self.name, ', '.join(valid_choices)
    ))
def supported_choices(self, version)
Expand source code
def supported_choices(self, version):
    return [c.value for c in self.choices if c.supports_version(version)]

Inherited members

class CultureField (*args, **kwargs)

Helper to mark strings that are # RFC 1766 culture values.

Expand source code
class CultureField(CharField):
    """Helper to mark strings that are # RFC 1766 culture values."""

Ancestors

Inherited members

class DateField (*args, **kwargs)

A field that handles date values.

Expand source code
class DateField(FieldURIField):
    """A field that handles date values."""

    value_cls = EWSDate

    def clean(self, value, version=None):
        # Allow plain datetime.date values as input
        if type(value) is datetime.date:
            value = self.value_cls.from_date(value)
        return super().clean(value=value, version=version)

Ancestors

Subclasses

Class variables

var value_cls

Extends the normal date implementation to satisfy EWS.

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    # Allow plain datetime.date values as input
    if type(value) is datetime.date:
        value = self.value_cls.from_date(value)
    return super().clean(value=value, version=version)
class DateOrDateTimeField (*args, **kwargs)

This field can handle both EWSDate and EWSDateTime. Used for calendar items where 'start' and 'end' values are conceptually dates when the calendar item is an all-day event, but datetimes in all other cases, and for recurrences where the returned 'start' and 'end' values may be either dates or datetimes depending on whether the recurring item is a task or a calendar item.

For all-day calendar items, we assume both start and end dates are inclusive.

For filtering kwarg validation and other places where we must decide on a specific class, we settle on datetime.

Expand source code
class DateOrDateTimeField(DateTimeField):
    """This field can handle both EWSDate and EWSDateTime. Used for calendar items where 'start' and 'end'
    values are conceptually dates when the calendar item is an all-day event, but datetimes in all other cases, and
    for recurrences where the returned 'start' and 'end' values may be either dates or datetimes depending on whether
    the recurring item is a task or a calendar item.

    For all-day calendar items, we assume both start and end dates are inclusive.

    For filtering kwarg validation and other places where we must decide on a specific class, we settle on datetime.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Create internal field to handle date-only logic
        self._date_field = DateField(*args, **kwargs)

    def clean(self, value, version=None):
        # Most calendar items will contain datetime values. We can't access the is_all_day value here, so CalendarItem
        # must handle that sanity check.
        if type(value) in (EWSDate, datetime.date):
            return self._date_field.clean(value=value, version=version)
        return super().clean(value=value, version=version)

    def from_xml(self, elem, account):
        val = self._get_val_from_elem(elem)
        if val is not None and len(val) == 16:
            # This is a date format with timezone info, as sent by task recurrences. Eg: '2006-01-09+01:00'
            return self._date_field.from_xml(elem=elem, account=account)
        return super().from_xml(elem=elem, account=account)

Ancestors

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    # Most calendar items will contain datetime values. We can't access the is_all_day value here, so CalendarItem
    # must handle that sanity check.
    if type(value) in (EWSDate, datetime.date):
        return self._date_field.clean(value=value, version=version)
    return super().clean(value=value, version=version)
def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    val = self._get_val_from_elem(elem)
    if val is not None and len(val) == 16:
        # This is a date format with timezone info, as sent by task recurrences. Eg: '2006-01-09+01:00'
        return self._date_field.from_xml(elem=elem, account=account)
    return super().from_xml(elem=elem, account=account)

Inherited members

class DateTimeBackedDateField (*args, **kwargs)

A field that acts like a date, but where values are sent to EWS as EWSDateTime.

Expand source code
class DateTimeBackedDateField(DateField):
    """A field that acts like a date, but where values are sent to EWS as EWSDateTime."""

    def __init__(self, *args, **kwargs):
        # Not all fields assume a default time of 00:00, so make this configurable
        self._default_time = kwargs.pop('default_time', datetime.time(0, 0))
        super().__init__(*args, **kwargs)
        # Create internal field to handle datetime-only logic
        self._datetime_field = DateTimeField(*args, **kwargs)

    def date_to_datetime(self, value):
        return self._datetime_field.value_cls.combine(value, self._default_time).replace(tzinfo=UTC)

    def from_xml(self, elem, account):
        val = self._get_val_from_elem(elem)
        if val is not None and len(val) == 25:
            # This is a datetime string with timezone info, e.g. '2021-03-01T21:55:54+00:00'. We don't want to have
            # datetime values converted to UTC before converting to date. EWSDateTime.from_string() insists on
            # converting to UTC, but we don't have an EWSTimeZone we can convert the timezone info to. Instead, parse
            # the string with .fromisoformat().
            return datetime.datetime.fromisoformat(val).date()
        # Revert to default parsing of datetime strings
        res = self._datetime_field.from_xml(elem=elem, account=account)
        if res is None:
            return res
        return res.date()

    def to_xml(self, value, version):
        # Convert date to datetime
        value = self.date_to_datetime(value)
        return self._datetime_field.to_xml(value=value, version=version)

Ancestors

Methods

def date_to_datetime(self, value)
Expand source code
def date_to_datetime(self, value):
    return self._datetime_field.value_cls.combine(value, self._default_time).replace(tzinfo=UTC)
def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    val = self._get_val_from_elem(elem)
    if val is not None and len(val) == 25:
        # This is a datetime string with timezone info, e.g. '2021-03-01T21:55:54+00:00'. We don't want to have
        # datetime values converted to UTC before converting to date. EWSDateTime.from_string() insists on
        # converting to UTC, but we don't have an EWSTimeZone we can convert the timezone info to. Instead, parse
        # the string with .fromisoformat().
        return datetime.datetime.fromisoformat(val).date()
    # Revert to default parsing of datetime strings
    res = self._datetime_field.from_xml(elem=elem, account=account)
    if res is None:
        return res
    return res.date()
def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    # Convert date to datetime
    value = self.date_to_datetime(value)
    return self._datetime_field.to_xml(value=value, version=version)

Inherited members

class DateTimeField (*args, **kwargs)

A field that handles datetime values.

Expand source code
class DateTimeField(FieldURIField):
    """A field that handles datetime values."""

    value_cls = EWSDateTime

    def clean(self, value, version=None):
        if isinstance(value, datetime.datetime):
            if not value.tzinfo:
                raise ValueError("Value '%s' on field '%s' must be timezone aware" % (value, self.name))
            if type(value) is datetime.datetime:
                value = self.value_cls.from_datetime(value)
        return super().clean(value, version=version)

    def from_xml(self, elem, account):
        val = self._get_val_from_elem(elem)
        if val is not None:
            try:
                return self.value_cls.from_string(val)
            except ValueError as e:
                if isinstance(e, NaiveDateTimeNotAllowed):
                    # We encountered a naive datetime
                    if account:
                        # Convert to timezone-aware datetime using the default timezone of the account
                        tz = account.default_timezone
                        log.info('Found naive datetime %s on field %s. Assuming timezone %s', e.local_dt, self.name, tz)
                        return e.local_dt.replace(tzinfo=tz)
                    # There's nothing we can do but return the naive date. It's better than assuming e.g. UTC.
                    log.warning('Returning naive datetime %s on field %s', e.local_dt, self.name)
                    return e.local_dt
                log.info("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
                return None
        return self.default

Ancestors

Subclasses

Class variables

var value_cls

Extends the normal datetime implementation to satisfy EWS.

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    if isinstance(value, datetime.datetime):
        if not value.tzinfo:
            raise ValueError("Value '%s' on field '%s' must be timezone aware" % (value, self.name))
        if type(value) is datetime.datetime:
            value = self.value_cls.from_datetime(value)
    return super().clean(value, version=version)
def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    val = self._get_val_from_elem(elem)
    if val is not None:
        try:
            return self.value_cls.from_string(val)
        except ValueError as e:
            if isinstance(e, NaiveDateTimeNotAllowed):
                # We encountered a naive datetime
                if account:
                    # Convert to timezone-aware datetime using the default timezone of the account
                    tz = account.default_timezone
                    log.info('Found naive datetime %s on field %s. Assuming timezone %s', e.local_dt, self.name, tz)
                    return e.local_dt.replace(tzinfo=tz)
                # There's nothing we can do but return the naive date. It's better than assuming e.g. UTC.
                log.warning('Returning naive datetime %s on field %s', e.local_dt, self.name)
                return e.local_dt
            log.info("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
            return None
    return self.default
class DecimalField (*args, **kwargs)

A field that handles decimal values.

Expand source code
class DecimalField(IntegerField):
    """A field that handles decimal values."""

    value_cls = Decimal

Ancestors

Inherited members

class DictionaryField (*args, **kwargs)

A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be 'itemtype:FieldName'

Expand source code
class DictionaryField(FieldURIField):
    value_cls = dict

    def from_xml(self, elem, account):
        from .properties import DictionaryEntry
        iter_elem = elem.find(self.response_tag())
        if iter_elem is not None:
            entries = [
                DictionaryEntry.from_xml(elem=e, account=account)
                for e in iter_elem.findall(DictionaryEntry.response_tag())
            ]
            return {e.key: e.value for e in entries}
        return self.default

    def clean(self, value, version=None):
        if isinstance(value, dict):
            cleaned = {}
            for k, v in value.items():
                if type(k) is datetime.datetime:
                    k = EWSDateTime.from_datetime(k)
                if type(v) is datetime.datetime:
                    v = EWSDateTime.from_datetime(v)
                cleaned[k] = v
            value = cleaned
        return super().clean(value=value, version=version)

    def to_xml(self, value, version):
        from .properties import DictionaryEntry
        field_elem = create_element(self.request_tag())
        entries = [DictionaryEntry(key=k, value=v) for k, v in value.items()]
        return set_xml_value(field_elem, entries, version=version)

Ancestors

Class variables

var value_cls

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    if isinstance(value, dict):
        cleaned = {}
        for k, v in value.items():
            if type(k) is datetime.datetime:
                k = EWSDateTime.from_datetime(k)
            if type(v) is datetime.datetime:
                v = EWSDateTime.from_datetime(v)
            cleaned[k] = v
        value = cleaned
    return super().clean(value=value, version=version)
def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    from .properties import DictionaryEntry
    iter_elem = elem.find(self.response_tag())
    if iter_elem is not None:
        entries = [
            DictionaryEntry.from_xml(elem=e, account=account)
            for e in iter_elem.findall(DictionaryEntry.response_tag())
        ]
        return {e.key: e.value for e in entries}
    return self.default
def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    from .properties import DictionaryEntry
    field_elem = create_element(self.request_tag())
    entries = [DictionaryEntry(key=k, value=v) for k, v in value.items()]
    return set_xml_value(field_elem, entries, version=version)
class EWSElementField (*args, **kwargs)

A generic field for any EWSElement object.

Expand source code
class EWSElementField(FieldURIField):
    """A generic field for any EWSElement object."""

    def __init__(self, *args, **kwargs):
        self._value_cls = kwargs.pop('value_cls')
        if 'namespace' not in kwargs:
            kwargs['namespace'] = self.value_cls.NAMESPACE
        super().__init__(*args, **kwargs)

    @property
    def value_cls(self):
        if isinstance(self._value_cls, str):
            # Support 'value_cls' as string to allow self-referencing classes. The class must be importable from the
            # top-level module.
            self._value_cls = getattr(import_module(self.__module__.split('.')[0]), self._value_cls)
        return self._value_cls

    def from_xml(self, elem, account):
        if self.is_list:
            iter_elem = elem.find(self.response_tag())
            if iter_elem is not None:
                return [self.value_cls.from_xml(elem=e, account=account)
                        for e in iter_elem.findall(self.value_cls.response_tag())]
        else:
            if self.field_uri is None:
                sub_elem = elem.find(self.value_cls.response_tag())
            else:
                sub_elem = elem.find(self.response_tag())
            if sub_elem is not None:
                return self.value_cls.from_xml(elem=sub_elem, account=account)
        return self.default

    def to_xml(self, value, version):
        if self.field_uri is None:
            return value.to_xml(version=version)
        field_elem = create_element(self.request_tag())
        return set_xml_value(field_elem, value, version=version)

Ancestors

Subclasses

Instance variables

var value_cls
Expand source code
@property
def value_cls(self):
    if isinstance(self._value_cls, str):
        # Support 'value_cls' as string to allow self-referencing classes. The class must be importable from the
        # top-level module.
        self._value_cls = getattr(import_module(self.__module__.split('.')[0]), self._value_cls)
    return self._value_cls

Methods

def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    if self.is_list:
        iter_elem = elem.find(self.response_tag())
        if iter_elem is not None:
            return [self.value_cls.from_xml(elem=e, account=account)
                    for e in iter_elem.findall(self.value_cls.response_tag())]
    else:
        if self.field_uri is None:
            sub_elem = elem.find(self.value_cls.response_tag())
        else:
            sub_elem = elem.find(self.response_tag())
        if sub_elem is not None:
            return self.value_cls.from_xml(elem=sub_elem, account=account)
    return self.default
def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    if self.field_uri is None:
        return value.to_xml(version=version)
    field_elem = create_element(self.request_tag())
    return set_xml_value(field_elem, value, version=version)
class EWSElementListField (*args, **kwargs)

Like EWSElementField, but for lists of EWSElement objects.

Expand source code
class EWSElementListField(EWSElementField):
    """Like EWSElementField, but for lists of EWSElement objects."""

    is_list = True
    is_complex = True

Ancestors

Subclasses

Class variables

var is_complex
var is_list
class EffectiveRightsField (*args, **kwargs)

A generic field for any EWSElement object.

Expand source code
class EffectiveRightsField(EWSElementField):
    def __init__(self, *args, **kwargs):
        from .properties import EffectiveRights
        kwargs['value_cls'] = EffectiveRights
        super().__init__(*args, **kwargs)

Ancestors

class EmailAddressAttributedValueField (*args, **kwargs)

Like EWSElementField, but for lists of EWSElement objects.

Expand source code
class EmailAddressAttributedValueField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import EmailAddressAttributedValue
        kwargs['value_cls'] = EmailAddressAttributedValue
        super().__init__(*args, **kwargs)

Ancestors

class EmailAddressField (*args, **kwargs)

A helper class used for email address string that we can use for email validation.

Expand source code
class EmailAddressField(CharField):
    """A helper class used for email address string that we can use for email validation."""

Ancestors

Inherited members

class EmailAddressesField (*args, **kwargs)

A base class for all indexed fields.

Expand source code
class EmailAddressesField(IndexedField):
    is_list = True

    PARENT_ELEMENT_NAME = 'EmailAddresses'

    def __init__(self, *args, **kwargs):
        from .indexed_properties import EmailAddress
        kwargs['value_cls'] = EmailAddress
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        if value is not None:
            default_labels = self.value_cls.LABEL_CHOICES
            if len(value) > len(default_labels):
                raise ValueError('This field can handle at most %s values (value: %r)' % (len(default_labels), value))
            tmp = []
            for s, default_label in zip(value, default_labels):
                if not isinstance(s, str):
                    tmp.append(s)
                    continue
                tmp.append(self.value_cls(email=s, label=default_label))
            value = tmp
        return super().clean(value, version=version)

Ancestors

Class variables

var PARENT_ELEMENT_NAME
var is_list

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    if value is not None:
        default_labels = self.value_cls.LABEL_CHOICES
        if len(value) > len(default_labels):
            raise ValueError('This field can handle at most %s values (value: %r)' % (len(default_labels), value))
        tmp = []
        for s, default_label in zip(value, default_labels):
            if not isinstance(s, str):
                tmp.append(s)
                continue
            tmp.append(self.value_cls(email=s, label=default_label))
        value = tmp
    return super().clean(value, version=version)
class EmailField (*args, **kwargs)

A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for.

Expand source code
class EmailField(BaseEmailField):
    def __init__(self, *args, **kwargs):
        from .properties import Email
        kwargs['value_cls'] = Email
        super().__init__(*args, **kwargs)

Ancestors

class EmailSubField (name=None, is_required=False, is_required_after_save=False, is_read_only=False, is_read_only_after_send=False, is_searchable=True, is_attribute=False, default=None, supported_from=None, deprecated_from=None)

A field to hold the value on an SingleFieldIndexedElement.

Expand source code
class EmailSubField(SubField):
    """A field to hold the value on an SingleFieldIndexedElement."""

    value_cls = str

    def from_xml(self, elem, account):
        return elem.text or elem.get('Name')  # Sometimes elem.text is empty. Exchange saves the same in 'Name' attr

Ancestors

Methods

def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    return elem.text or elem.get('Name')  # Sometimes elem.text is empty. Exchange saves the same in 'Name' attr

Inherited members

class EnumAsIntField (*args, **kwargs)

Like EnumField, but communicates values with EWS in integers.

Expand source code
class EnumAsIntField(EnumField):
    """Like EnumField, but communicates values with EWS in integers."""

    def from_xml(self, elem, account):
        return super(EnumField, self).from_xml(elem=elem, account=account)

    def to_xml(self, value, version):
        field_elem = create_element(self.request_tag())
        return set_xml_value(field_elem, value, version=version)

Ancestors

Methods

def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    return super(EnumField, self).from_xml(elem=elem, account=account)
def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    field_elem = create_element(self.request_tag())
    return set_xml_value(field_elem, value, version=version)

Inherited members

class EnumField (*args, **kwargs)

A field type where you can enter either the 1-based index in an enum (tuple), or the enum value. Values will be stored internally as integers but output in XML as strings.

Expand source code
class EnumField(IntegerField):
    """A field type where you can enter either the 1-based index in an enum (tuple), or the enum value. Values will be
    stored internally as integers but output in XML as strings.
    """

    def __init__(self, *args, **kwargs):
        self.enum = kwargs.pop('enum')
        # Set different min/max defaults than IntegerField
        if 'max' in kwargs:
            raise AttributeError("EnumField does not support the 'max' attribute")
        kwargs['min'] = kwargs.pop('min', 1)
        kwargs['max'] = kwargs['min'] + len(self.enum) - 1
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        if self.is_list:
            value = list(value)  # Convert to something we can index
            for i, v in enumerate(value):
                if isinstance(v, str):
                    if v not in self.enum:
                        raise ValueError(
                            "List value '%s' on field '%s' must be one of %s" % (v, self.name, self.enum))
                    value[i] = self.enum.index(v) + 1
            if not value:
                raise ValueError("Value '%s' on field '%s' must not be empty" % (value, self.name))
            if len(value) > len(set(value)):
                raise ValueError("List entries '%s' on field '%s' must be unique" % (value, self.name))
        else:
            if isinstance(value, str):
                if value not in self.enum:
                    raise ValueError(
                        "Value '%s' on field '%s' must be one of %s" % (value, self.name, self.enum))
                value = self.enum.index(value) + 1
        return super().clean(value, version=version)

    def as_string(self, value):
        # Converts an integer in the enum to its equivalent string
        if isinstance(value, str):
            return value
        if self.is_list:
            return [self.enum[v - 1] for v in sorted(value)]
        return self.enum[value - 1]

    def from_xml(self, elem, account):
        val = self._get_val_from_elem(elem)
        if val is not None:
            try:
                if self.is_list:
                    return [self.enum.index(v) + 1 for v in val.split(' ')]
                return self.enum.index(val) + 1
            except ValueError:
                log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
                return None
        return self.default

    def to_xml(self, value, version):
        field_elem = create_element(self.request_tag())
        if self.is_list:
            return set_xml_value(field_elem, ' '.join(self.as_string(value)), version=version)
        return set_xml_value(field_elem, self.as_string(value), version=version)

Ancestors

Subclasses

Methods

def as_string(self, value)
Expand source code
def as_string(self, value):
    # Converts an integer in the enum to its equivalent string
    if isinstance(value, str):
        return value
    if self.is_list:
        return [self.enum[v - 1] for v in sorted(value)]
    return self.enum[value - 1]
def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    if self.is_list:
        value = list(value)  # Convert to something we can index
        for i, v in enumerate(value):
            if isinstance(v, str):
                if v not in self.enum:
                    raise ValueError(
                        "List value '%s' on field '%s' must be one of %s" % (v, self.name, self.enum))
                value[i] = self.enum.index(v) + 1
        if not value:
            raise ValueError("Value '%s' on field '%s' must not be empty" % (value, self.name))
        if len(value) > len(set(value)):
            raise ValueError("List entries '%s' on field '%s' must be unique" % (value, self.name))
    else:
        if isinstance(value, str):
            if value not in self.enum:
                raise ValueError(
                    "Value '%s' on field '%s' must be one of %s" % (value, self.name, self.enum))
            value = self.enum.index(value) + 1
    return super().clean(value, version=version)
def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    val = self._get_val_from_elem(elem)
    if val is not None:
        try:
            if self.is_list:
                return [self.enum.index(v) + 1 for v in val.split(' ')]
            return self.enum.index(val) + 1
        except ValueError:
            log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
            return None
    return self.default
def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    field_elem = create_element(self.request_tag())
    if self.is_list:
        return set_xml_value(field_elem, ' '.join(self.as_string(value)), version=version)
    return set_xml_value(field_elem, self.as_string(value), version=version)

Inherited members

class EnumListField (*args, **kwargs)

Like EnumField, but for lists of enum values.

Expand source code
class EnumListField(EnumField):
    """Like EnumField, but for lists of enum values."""

    is_list = True

Ancestors

Class variables

var is_list

Inherited members

class ExtendedPropertyField (*args, **kwargs)

Holds information related to an item field.

Expand source code
class ExtendedPropertyField(Field):
    def __init__(self, *args, **kwargs):
        self.value_cls = kwargs.pop('value_cls')
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        if value is None:
            if self.is_required:
                raise ValueError("'%s' is a required field" % self.name)
            return self.default
        if not isinstance(value, self.value_cls):
            # Allow keeping ExtendedProperty field values as their simple Python type, but run clean() anyway
            tmp = self.value_cls(value)
            tmp.clean(version=version)
            return value
        value.clean(version=version)
        return value

    def field_uri_xml(self):
        from .properties import ExtendedFieldURI
        cls = self.value_cls
        return ExtendedFieldURI(
            distinguished_property_set_id=cls.distinguished_property_set_id,
            property_set_id=cls.property_set_id.lower() if cls.property_set_id else None,
            property_tag=cls.property_tag_as_hex(),
            property_name=cls.property_name,
            property_id=value_to_xml_text(cls.property_id) if cls.property_id else None,
            property_type=cls.property_type,
        ).to_xml(version=None)

    def from_xml(self, elem, account):
        extended_properties = elem.findall(self.value_cls.response_tag())
        for extended_property in extended_properties:
            if self.value_cls.is_property_instance(extended_property):
                return self.value_cls.from_xml(elem=extended_property, account=account)
        return self.default

    def to_xml(self, value, version):
        extended_property = create_element(self.value_cls.request_tag())
        set_xml_value(extended_property, self.field_uri_xml(), version=version)
        if isinstance(value, self.value_cls):
            set_xml_value(extended_property, value, version=version)
        else:
            # Allow keeping ExtendedProperty field values as their simple Python type
            set_xml_value(extended_property, self.value_cls(value), version=version)
        return extended_property

    def __hash__(self):
        return hash(self.name)

Ancestors

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    if value is None:
        if self.is_required:
            raise ValueError("'%s' is a required field" % self.name)
        return self.default
    if not isinstance(value, self.value_cls):
        # Allow keeping ExtendedProperty field values as their simple Python type, but run clean() anyway
        tmp = self.value_cls(value)
        tmp.clean(version=version)
        return value
    value.clean(version=version)
    return value
def field_uri_xml(self)
Expand source code
def field_uri_xml(self):
    from .properties import ExtendedFieldURI
    cls = self.value_cls
    return ExtendedFieldURI(
        distinguished_property_set_id=cls.distinguished_property_set_id,
        property_set_id=cls.property_set_id.lower() if cls.property_set_id else None,
        property_tag=cls.property_tag_as_hex(),
        property_name=cls.property_name,
        property_id=value_to_xml_text(cls.property_id) if cls.property_id else None,
        property_type=cls.property_type,
    ).to_xml(version=None)
def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    extended_properties = elem.findall(self.value_cls.response_tag())
    for extended_property in extended_properties:
        if self.value_cls.is_property_instance(extended_property):
            return self.value_cls.from_xml(elem=extended_property, account=account)
    return self.default
def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    extended_property = create_element(self.value_cls.request_tag())
    set_xml_value(extended_property, self.field_uri_xml(), version=version)
    if isinstance(value, self.value_cls):
        set_xml_value(extended_property, value, version=version)
    else:
        # Allow keeping ExtendedProperty field values as their simple Python type
        set_xml_value(extended_property, self.value_cls(value), version=version)
    return extended_property
class Field (name=None, is_required=False, is_required_after_save=False, is_read_only=False, is_read_only_after_send=False, is_searchable=True, is_attribute=False, default=None, supported_from=None, deprecated_from=None)

Holds information related to an item field.

Expand source code
class Field(metaclass=abc.ABCMeta):
    """Holds information related to an item field."""

    value_cls = None
    is_list = False
    # Is the field a complex EWS type? Quoting the EWS FindItem docs:
    #
    #   The FindItem operation returns only the first 512 bytes of any streamable property. For Unicode, it returns
    #   the first 255 characters by using a null-terminated Unicode string. It does not return any of the message
    #   body formats or the recipient lists.
    #
    is_complex = False

    def __init__(self, name=None, is_required=False, is_required_after_save=False, is_read_only=False,
                 is_read_only_after_send=False, is_searchable=True, is_attribute=False, default=None,
                 supported_from=None, deprecated_from=None):
        self.name = name
        self.default = default  # Default value if none is given
        self.is_required = is_required
        # Some fields cannot be deleted on update. Default to True if 'is_required' is set
        self.is_required_after_save = is_required or is_required_after_save
        self.is_read_only = is_read_only
        # Set this for fields that raise ErrorInvalidPropertyUpdateSentMessage on update after send. Default to True
        # if 'is_read_only' is set
        self.is_read_only_after_send = is_read_only or is_read_only_after_send
        # Define whether the field can be used in a QuerySet. For some reason, EWS disallows searching on some fields,
        # instead throwing ErrorInvalidValueForProperty
        self.is_searchable = is_searchable
        # When true, this field is treated as an XML attribute instead of an element
        self.is_attribute = is_attribute
        # The Exchange build when this field was introduced. When talking with versions prior to this version,
        # we will ignore this field.
        if supported_from is not None and not isinstance(supported_from, Build):
            raise ValueError("'supported_from' %r must be a Build instance" % supported_from)
        self.supported_from = supported_from
        # The Exchange build when this field was deprecated. When talking with versions at or later than this version,
        # we will ignore this field.
        if deprecated_from is not None and not isinstance(deprecated_from, Build):
            raise ValueError("'deprecated_from' %r must be a Build instance" % deprecated_from)
        self.deprecated_from = deprecated_from

    def clean(self, value, version=None):
        if version and not self.supports_version(version):
            raise InvalidFieldForVersion("Field '%s' does not support EWS builds prior to %s (server has %s)" % (
                self.name, self.supported_from, version))
        if value is None:
            if self.is_required and self.default is None:
                raise ValueError("'%s' is a required field with no default" % self.name)
            return self.default
        if self.is_list:
            if not is_iterable(value):
                raise ValueError("Field '%s' value %r must be a list" % (self.name, value))
            for v in value:
                if not isinstance(v, self.value_cls):
                    raise TypeError("Field '%s' value %r must be of type %s" % (self.name, v, self.value_cls))
                if hasattr(v, 'clean'):
                    v.clean(version=version)
        else:
            if not isinstance(value, self.value_cls):
                raise TypeError("Field '%s' value %r must be of type %s" % (self.name, value, self.value_cls))
            if hasattr(value, 'clean'):
                value.clean(version=version)
        return value

    @abc.abstractmethod
    def from_xml(self, elem, account):
        pass

    @abc.abstractmethod
    def to_xml(self, value, version):
        pass

    def supports_version(self, version):
        # 'version' is a Version instance, for convenience by callers
        if not isinstance(version, Version):
            raise ValueError("'version' %r must be a Version instance" % version)
        if self.supported_from and version.build < self.supported_from:
            return False
        if self.deprecated_from and version.build >= self.deprecated_from:
            return False
        return True

    def __eq__(self, other):
        return hash(self) == hash(other)

    @abc.abstractmethod
    def __hash__(self):
        pass

    def __repr__(self):
        return self.__class__.__name__ + '(%s)' % ', '.join('%s=%r' % (f, getattr(self, f)) for f in (
            'name', 'value_cls', 'is_list', 'is_complex', 'default'))

Subclasses

Class variables

var is_complex
var is_list
var value_cls

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    if version and not self.supports_version(version):
        raise InvalidFieldForVersion("Field '%s' does not support EWS builds prior to %s (server has %s)" % (
            self.name, self.supported_from, version))
    if value is None:
        if self.is_required and self.default is None:
            raise ValueError("'%s' is a required field with no default" % self.name)
        return self.default
    if self.is_list:
        if not is_iterable(value):
            raise ValueError("Field '%s' value %r must be a list" % (self.name, value))
        for v in value:
            if not isinstance(v, self.value_cls):
                raise TypeError("Field '%s' value %r must be of type %s" % (self.name, v, self.value_cls))
            if hasattr(v, 'clean'):
                v.clean(version=version)
    else:
        if not isinstance(value, self.value_cls):
            raise TypeError("Field '%s' value %r must be of type %s" % (self.name, value, self.value_cls))
        if hasattr(value, 'clean'):
            value.clean(version=version)
    return value
def from_xml(self, elem, account)
Expand source code
@abc.abstractmethod
def from_xml(self, elem, account):
    pass
def supports_version(self, version)
Expand source code
def supports_version(self, version):
    # 'version' is a Version instance, for convenience by callers
    if not isinstance(version, Version):
        raise ValueError("'version' %r must be a Version instance" % version)
    if self.supported_from and version.build < self.supported_from:
        return False
    if self.deprecated_from and version.build >= self.deprecated_from:
        return False
    return True
def to_xml(self, value, version)
Expand source code
@abc.abstractmethod
def to_xml(self, value, version):
    pass
class FieldOrder (field_path, reverse=False)

Holds values needed to call server-side sorting on a single field path.

Expand source code
class FieldOrder:
    """Holds values needed to call server-side sorting on a single field path."""

    def __init__(self, field_path, reverse=False):
        if not isinstance(field_path, FieldPath):
            raise ValueError("'field_path' %r must be a FieldPath instance" % field_path)
        if not isinstance(reverse, bool):
            raise ValueError("'reverse' %r must be a boolean" % reverse)
        self.field_path = field_path
        self.reverse = reverse

    @classmethod
    def from_string(cls, field_path, folder):
        return cls(
            field_path=FieldPath.from_string(field_path=field_path.lstrip('-'), folder=folder, strict=True),
            reverse=field_path.startswith('-')
        )

    def to_xml(self):
        field_order = create_element('t:FieldOrder', attrs=dict(Order='Descending' if self.reverse else 'Ascending'))
        field_order.append(self.field_path.to_xml())
        return field_order

Static methods

def from_string(field_path, folder)
Expand source code
@classmethod
def from_string(cls, field_path, folder):
    return cls(
        field_path=FieldPath.from_string(field_path=field_path.lstrip('-'), folder=folder, strict=True),
        reverse=field_path.startswith('-')
    )

Methods

def to_xml(self)
Expand source code
def to_xml(self):
    field_order = create_element('t:FieldOrder', attrs=dict(Order='Descending' if self.reverse else 'Ascending'))
    field_order.append(self.field_path.to_xml())
    return field_order
class FieldPath (field, label=None, subfield=None)

Holds values needed to point to a single field. For indexed properties, we allow setting either field, field and label, or field, label and subfield. This allows pointing to either the full indexed property set, a property with a specific label, or a particular subfield field on that property.

Expand source code
class FieldPath:
    """Holds values needed to point to a single field. For indexed properties, we allow setting either field,
    field and label, or field, label and subfield. This allows pointing to either the full indexed property set, a
    property with a specific label, or a particular subfield field on that property.
    """

    def __init__(self, field, label=None, subfield=None):
        # 'label' and 'subfield' are only used for IndexedField fields
        if not isinstance(field, (FieldURIField, ExtendedPropertyField)):
            raise ValueError("'field' %r must be an FieldURIField, of ExtendedPropertyField instance" % field)
        if label and not isinstance(label, str):
            raise ValueError("'label' %r must be a %s instance" % (label, str))
        if subfield and not isinstance(subfield, SubField):
            raise ValueError("'subfield' %r must be a SubField instance" % subfield)
        self.field = field
        self.label = label
        self.subfield = subfield

    @classmethod
    def from_string(cls, field_path, folder, strict=False):
        field, label, subfield = resolve_field_path(field_path, folder=folder, strict=strict)
        return cls(field=field, label=label, subfield=subfield)

    def get_value(self, item):
        # For indexed properties, get either the full property set, the property with matching label, or a particular
        # subfield.
        if self.label:
            for subitem in getattr(item, self.field.name):
                if subitem.label == self.label:
                    if self.subfield:
                        return getattr(subitem, self.subfield.name)
                    return subitem
            return None  # No item with this label
        return getattr(item, self.field.name)

    def get_sort_value(self, item):
        # For fields that allow values of different types, we need to return a value that is
        val = self.get_value(item)
        if isinstance(self.field, DateOrDateTimeField) and isinstance(val, EWSDate):
            return item.date_to_datetime(field_name=self.field.name)
        return val

    def to_xml(self):
        if isinstance(self.field, IndexedField):
            if not self.label or not self.subfield:
                raise ValueError("Field path for indexed field '%s' is missing label and/or subfield" % self.field.name)
            return self.subfield.field_uri_xml(field_uri=self.field.field_uri, label=self.label)
        return self.field.field_uri_xml()

    def expand(self, version):
        # If this path does not point to a specific subfield on an indexed property, return all the possible path
        # combinations for this field path.
        if isinstance(self.field, IndexedField):
            labels = [self.label] if self.label \
                else self.field.value_cls.get_field_by_fieldname('label').supported_choices(version=version)
            subfields = [self.subfield] if self.subfield else self.field.value_cls.supported_fields(version=version)
            for label in labels:
                for subfield in subfields:
                    yield FieldPath(field=self.field, label=label, subfield=subfield)
        else:
            yield self

    @property
    def path(self):
        if self.label:
            from .indexed_properties import SingleFieldIndexedElement
            if issubclass(self.field.value_cls, SingleFieldIndexedElement) or not self.subfield:
                return '%s__%s' % (self.field.name, self.label)
            return '%s__%s__%s' % (self.field.name, self.label, self.subfield.name)
        return self.field.name

    def __eq__(self, other):
        return hash(self) == hash(other)

    def __str__(self):
        return self.path

    def __repr__(self):
        return self.__class__.__name__ + repr((self.field, self.label, self.subfield))

    def __hash__(self):
        return hash((self.field, self.label, self.subfield))

Static methods

def from_string(field_path, folder, strict=False)
Expand source code
@classmethod
def from_string(cls, field_path, folder, strict=False):
    field, label, subfield = resolve_field_path(field_path, folder=folder, strict=strict)
    return cls(field=field, label=label, subfield=subfield)

Instance variables

var path
Expand source code
@property
def path(self):
    if self.label:
        from .indexed_properties import SingleFieldIndexedElement
        if issubclass(self.field.value_cls, SingleFieldIndexedElement) or not self.subfield:
            return '%s__%s' % (self.field.name, self.label)
        return '%s__%s__%s' % (self.field.name, self.label, self.subfield.name)
    return self.field.name

Methods

def expand(self, version)
Expand source code
def expand(self, version):
    # If this path does not point to a specific subfield on an indexed property, return all the possible path
    # combinations for this field path.
    if isinstance(self.field, IndexedField):
        labels = [self.label] if self.label \
            else self.field.value_cls.get_field_by_fieldname('label').supported_choices(version=version)
        subfields = [self.subfield] if self.subfield else self.field.value_cls.supported_fields(version=version)
        for label in labels:
            for subfield in subfields:
                yield FieldPath(field=self.field, label=label, subfield=subfield)
    else:
        yield self
def get_sort_value(self, item)
Expand source code
def get_sort_value(self, item):
    # For fields that allow values of different types, we need to return a value that is
    val = self.get_value(item)
    if isinstance(self.field, DateOrDateTimeField) and isinstance(val, EWSDate):
        return item.date_to_datetime(field_name=self.field.name)
    return val
def get_value(self, item)
Expand source code
def get_value(self, item):
    # For indexed properties, get either the full property set, the property with matching label, or a particular
    # subfield.
    if self.label:
        for subitem in getattr(item, self.field.name):
            if subitem.label == self.label:
                if self.subfield:
                    return getattr(subitem, self.subfield.name)
                return subitem
        return None  # No item with this label
    return getattr(item, self.field.name)
def to_xml(self)
Expand source code
def to_xml(self):
    if isinstance(self.field, IndexedField):
        if not self.label or not self.subfield:
            raise ValueError("Field path for indexed field '%s' is missing label and/or subfield" % self.field.name)
        return self.subfield.field_uri_xml(field_uri=self.field.field_uri, label=self.label)
    return self.field.field_uri_xml()
class FieldURIField (*args, **kwargs)

A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be 'itemtype:FieldName'

Expand source code
class FieldURIField(Field):
    """A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It
    may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be
    'itemtype:FieldName'
    """

    def __init__(self, *args, **kwargs):
        self.field_uri = kwargs.pop('field_uri', None)
        self.namespace = kwargs.pop('namespace', TNS)
        super().__init__(*args, **kwargs)
        # See all valid FieldURI values at
        # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/fielduri
        # The field_uri has a prefix when the FieldURI points to an Item field.
        if self.field_uri is None:
            self.field_uri_postfix = None
        elif ':' in self.field_uri:
            self.field_uri_postfix = self.field_uri.split(':')[1]
        else:
            self.field_uri_postfix = self.field_uri

    def _get_val_from_elem(self, elem):
        if self.is_attribute:
            return elem.get(self.field_uri) or None
        return get_xml_attr(elem, self.response_tag())

    def from_xml(self, elem, account):
        val = self._get_val_from_elem(elem)
        if val is not None:
            try:
                return xml_text_to_value(val, self.value_cls)
            except (ValueError, InvalidOperation):
                log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
                return None
        return self.default

    def to_xml(self, value, version):
        field_elem = create_element(self.request_tag())
        return set_xml_value(field_elem, value, version=version)

    def field_uri_xml(self):
        from .properties import FieldURI
        if not self.field_uri:
            raise ValueError("'field_uri' value is missing")
        return FieldURI(field_uri=self.field_uri).to_xml(version=None)

    def request_tag(self):
        if not self.field_uri_postfix:
            raise ValueError("'field_uri_postfix' value is missing")
        return 't:%s' % self.field_uri_postfix

    def response_tag(self):
        if not self.field_uri_postfix:
            raise ValueError("'field_uri_postfix' value is missing")
        return '{%s}%s' % (self.namespace, self.field_uri_postfix)

    def __hash__(self):
        return hash(self.field_uri)

Ancestors

Subclasses

Methods

def field_uri_xml(self)
Expand source code
def field_uri_xml(self):
    from .properties import FieldURI
    if not self.field_uri:
        raise ValueError("'field_uri' value is missing")
    return FieldURI(field_uri=self.field_uri).to_xml(version=None)
def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    val = self._get_val_from_elem(elem)
    if val is not None:
        try:
            return xml_text_to_value(val, self.value_cls)
        except (ValueError, InvalidOperation):
            log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
            return None
    return self.default
def request_tag(self)
Expand source code
def request_tag(self):
    if not self.field_uri_postfix:
        raise ValueError("'field_uri_postfix' value is missing")
    return 't:%s' % self.field_uri_postfix
def response_tag(self)
Expand source code
def response_tag(self):
    if not self.field_uri_postfix:
        raise ValueError("'field_uri_postfix' value is missing")
    return '{%s}%s' % (self.namespace, self.field_uri_postfix)
def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    field_elem = create_element(self.request_tag())
    return set_xml_value(field_elem, value, version=version)
class FreeBusyStatusField (*args, **kwargs)

Like ChoiceField, but specifically for Free/Busy values.

Expand source code
class FreeBusyStatusField(ChoiceField):
    """Like ChoiceField, but specifically for Free/Busy values."""

    def __init__(self, *args, **kwargs):
        kwargs['choices'] = set(FREE_BUSY_CHOICES)
        super().__init__(*args, **kwargs)

Ancestors

Inherited members

class GenericEventListField (*args, **kwargs)

A list field that can contain all subclasses of Event.

Expand source code
class GenericEventListField(EWSElementField):
    """A list field that can contain all subclasses of Event."""

    is_list = True

    @property
    def _event_types_map(self):
        return {v.response_tag(): v for v in self.value_classes}

    def __init__(self, *args, **kwargs):
        from .properties import CopiedEvent, CreatedEvent, DeletedEvent, ModifiedEvent, MovedEvent, \
            NewMailEvent, StatusEvent, FreeBusyChangedEvent
        kwargs['value_cls'] = None  # Parent class requires this kwarg
        kwargs['namespace'] = None  # Parent class requires this kwarg
        super().__init__(*args, **kwargs)
        self.value_classes = (
            CopiedEvent, CreatedEvent, DeletedEvent, ModifiedEvent, MovedEvent, NewMailEvent, StatusEvent,
            FreeBusyChangedEvent,
        )

    def from_xml(self, elem, account):
        events = []
        for event in elem:
            # This may or may not be an event element. Could also be other child elements of Notification
            try:
                value_cls = self._event_types_map[event.tag]
            except KeyError:
                continue
            events.append(value_cls.from_xml(elem=event, account=account))
        return events or self.default

Ancestors

Class variables

var is_list

Methods

def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    events = []
    for event in elem:
        # This may or may not be an event element. Could also be other child elements of Notification
        try:
            value_cls = self._event_types_map[event.tag]
        except KeyError:
            continue
        events.append(value_cls.from_xml(elem=event, account=account))
    return events or self.default
class IdElementField (*args, **kwargs)

A generic field for any EWSElement object.

Expand source code
class IdElementField(EWSElementField):
    def __init__(self, *args, **kwargs):
        kwargs['is_searchable'] = False
        kwargs['is_read_only'] = True
        super().__init__(*args, **kwargs)

Ancestors

class IdField (*args, **kwargs)

A field to hold the 'Id' and 'Changekey' attributes on 'ItemId' type items. There is no guaranteed max length, but we can assume 512 bytes in practice. See https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/ews-identifiers-in-exchange

Expand source code
class IdField(CharField):
    """A field to hold the 'Id' and 'Changekey' attributes on 'ItemId' type items. There is no guaranteed max length,
    but we can assume 512 bytes in practice. See
    https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/ews-identifiers-in-exchange
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.max_length = 512  # This is above the normal 255 limit, but this is actually an attribute, not a field
        self.is_searchable = False
        self.is_attribute = True

Ancestors

Inherited members

class IndexedField (*args, **kwargs)

A base class for all indexed fields.

Expand source code
class IndexedField(EWSElementField):
    """A base class for all indexed fields."""

    PARENT_ELEMENT_NAME = None

    def __init__(self, *args, **kwargs):
        from .indexed_properties import IndexedElement
        value_cls = kwargs['value_cls']
        if not issubclass(value_cls, IndexedElement):
            raise ValueError("'value_cls' %r must be a subclass of IndexedElement" % value_cls)
        super().__init__(*args, **kwargs)

    def to_xml(self, value, version):
        return set_xml_value(create_element('t:%s' % self.PARENT_ELEMENT_NAME), value, version)

    def response_tag(self):
        return '{%s}%s' % (self.namespace, self.PARENT_ELEMENT_NAME)

    def __hash__(self):
        return hash(self.field_uri)

Ancestors

Subclasses

Class variables

var PARENT_ELEMENT_NAME

Methods

def response_tag(self)
Expand source code
def response_tag(self):
    return '{%s}%s' % (self.namespace, self.PARENT_ELEMENT_NAME)
def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    return set_xml_value(create_element('t:%s' % self.PARENT_ELEMENT_NAME), value, version)
class IntegerField (*args, **kwargs)

A field that handles integer values.

Expand source code
class IntegerField(FieldURIField):
    """A field that handles integer values."""

    value_cls = int

    def __init__(self, *args, **kwargs):
        self.min = kwargs.pop('min', None)
        self.max = kwargs.pop('max', None)
        super().__init__(*args, **kwargs)

    def _clean_single_value(self, v):
        if self.min is not None and v < self.min:
            raise ValueError(
                "Value %r on field '%s' must be greater than %s" % (v, self.name, self.min))
        if self.max is not None and v > self.max:
            raise ValueError("Value %r on field '%s' must be less than %s" % (v, self.name, self.max))

    def clean(self, value, version=None):
        value = super().clean(value, version=version)
        if value is not None:
            if self.is_list:
                for v in value:
                    self._clean_single_value(v)
            else:
                self._clean_single_value(value)
        return value

Ancestors

Subclasses

Class variables

var value_cls

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.

>>> int('0b100', base=0)
4

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    value = super().clean(value, version=version)
    if value is not None:
        if self.is_list:
            for v in value:
                self._clean_single_value(v)
        else:
            self._clean_single_value(value)
    return value
class InvalidChoiceForVersion (*args, **kwargs)

Used when a value is not valid for an enum-type field.

Expand source code
class InvalidChoiceForVersion(ValueError):
    """Used when a value is not valid for an enum-type field."""

Ancestors

  • builtins.ValueError
  • builtins.Exception
  • builtins.BaseException
class InvalidField (*args, **kwargs)

Used when a field name does not match any defined fields.

Expand source code
class InvalidField(ValueError):
    """Used when a field name does not match any defined fields."""

Ancestors

  • builtins.ValueError
  • builtins.Exception
  • builtins.BaseException
class InvalidFieldForVersion (*args, **kwargs)

Used when a field is not supported on the given Exchnage version.

Expand source code
class InvalidFieldForVersion(ValueError):
    """Used when a field is not supported on the given Exchnage version."""

Ancestors

  • builtins.ValueError
  • builtins.Exception
  • builtins.BaseException
class ItemField (*args, **kwargs)

A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be 'itemtype:FieldName'

Expand source code
class ItemField(FieldURIField):
    @property
    def value_cls(self):
        from .items import Item
        return Item

    def from_xml(self, elem, account):
        from .items import ITEM_CLASSES
        for item_cls in ITEM_CLASSES:
            item_elem = elem.find(item_cls.response_tag())
            if item_elem is not None:
                return item_cls.from_xml(elem=item_elem, account=account)
        return None

    def to_xml(self, value, version):
        # We don't want to wrap in an Item element
        return value.to_xml(version=version)

Ancestors

Instance variables

var value_cls
Expand source code
@property
def value_cls(self):
    from .items import Item
    return Item

Methods

def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    from .items import ITEM_CLASSES
    for item_cls in ITEM_CLASSES:
        item_elem = elem.find(item_cls.response_tag())
        if item_elem is not None:
            return item_cls.from_xml(elem=item_elem, account=account)
    return None
def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    # We don't want to wrap in an Item element
    return value.to_xml(version=version)
class LabelField (*args, **kwargs)

A field to hold the label on an IndexedElement.

Expand source code
class LabelField(ChoiceField):
    """A field to hold the label on an IndexedElement."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.is_attribute = True

    def from_xml(self, elem, account):
        return elem.get(self.field_uri)

Ancestors

Methods

def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    return elem.get(self.field_uri)

Inherited members

class MailboxField (*args, **kwargs)

A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for.

Expand source code
class MailboxField(BaseEmailField):
    def __init__(self, *args, **kwargs):
        from .properties import Mailbox
        kwargs['value_cls'] = Mailbox
        super().__init__(*args, **kwargs)

Ancestors

class MailboxListField (*args, **kwargs)

Like EWSElementField, but for lists of EWSElement objects.

Expand source code
class MailboxListField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import Mailbox
        kwargs['value_cls'] = Mailbox
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        if value is not None:
            value = [self.value_cls(email_address=s) if isinstance(s, str) else s for s in value]
        return super().clean(value, version=version)

Ancestors

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    if value is not None:
        value = [self.value_cls(email_address=s) if isinstance(s, str) else s for s in value]
    return super().clean(value, version=version)
class MemberListField (*args, **kwargs)

Like EWSElementField, but for lists of EWSElement objects.

Expand source code
class MemberListField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import Member
        kwargs['value_cls'] = Member
        super().__init__(*args, **kwargs)

    def clean(self, value, version=None):
        from .properties import Mailbox
        if value is not None:
            value = [
                self.value_cls(mailbox=Mailbox(email_address=s)) if isinstance(s, str) else s for s in value
            ]
        return super().clean(value, version=version)

Ancestors

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    from .properties import Mailbox
    if value is not None:
        value = [
            self.value_cls(mailbox=Mailbox(email_address=s)) if isinstance(s, str) else s for s in value
        ]
    return super().clean(value, version=version)
class MessageField (*args, **kwargs)

A field that handles the Message element.

Expand source code
class MessageField(TextField):
    """A field that handles the Message element."""

    INNER_ELEMENT_NAME = 'Message'

    def from_xml(self, elem, account):
        reply = elem.find(self.response_tag())
        if reply is None:
            return None
        message = reply.find('{%s}%s' % (TNS, self.INNER_ELEMENT_NAME))
        if message is None:
            return None
        return message.text

    def to_xml(self, value, version):
        field_elem = create_element(self.request_tag())
        message = create_element('t:%s' % self.INNER_ELEMENT_NAME)
        message.text = value
        return set_xml_value(field_elem, message, version=version)

Ancestors

Class variables

var INNER_ELEMENT_NAME

Methods

def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    reply = elem.find(self.response_tag())
    if reply is None:
        return None
    message = reply.find('{%s}%s' % (TNS, self.INNER_ELEMENT_NAME))
    if message is None:
        return None
    return message.text
def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    field_elem = create_element(self.request_tag())
    message = create_element('t:%s' % self.INNER_ELEMENT_NAME)
    message.text = value
    return set_xml_value(field_elem, message, version=version)

Inherited members

class MessageHeaderField (*args, **kwargs)

Like EWSElementField, but for lists of EWSElement objects.

Expand source code
class MessageHeaderField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import MessageHeader
        kwargs['value_cls'] = MessageHeader
        super().__init__(*args, **kwargs)

Ancestors

class MimeContentField (*args, **kwargs)

Like Base64Field. This element has an optional 'CharacterSet' attribute, but it specifies the encoding of the base64-encoded string (which doesn't make sense since base64-encoded strings are always ASCII). We ignore it here because the decoded data could be in some other encoding, specified in the "Content-Type" HTTP header.

Expand source code
class MimeContentField(Base64Field):
    """Like Base64Field. This element has an optional 'CharacterSet' attribute, but it specifies the encoding of the
    base64-encoded string (which doesn't make sense since base64-encoded strings are always ASCII). We ignore it here
    because the decoded data could be in some other encoding, specified in the "Content-Type" HTTP header.
    """

Ancestors

Inherited members

class NamedSubField (*args, **kwargs)

A field to hold the value on an MultiFieldIndexedElement.

Expand source code
class NamedSubField(SubField):
    """A field to hold the value on an MultiFieldIndexedElement."""

    value_cls = str

    def __init__(self, *args, **kwargs):
        self.field_uri = kwargs.pop('field_uri')
        if ':' in self.field_uri:
            raise ValueError("'field_uri' value must not contain a colon")
        super().__init__(*args, **kwargs)

    def from_xml(self, elem, account):
        field_elem = elem.find(self.response_tag())
        val = None if field_elem is None else field_elem.text or None
        if val is not None:
            return val
        return self.default

    def to_xml(self, value, version):
        field_elem = create_element(self.request_tag())
        return set_xml_value(field_elem, value, version=version)

    def field_uri_xml(self, field_uri, label):
        from .properties import IndexedFieldURI
        return IndexedFieldURI(field_uri='%s:%s' % (field_uri, self.field_uri), field_index=label).to_xml(version=None)

    def request_tag(self):
        return 't:%s' % self.field_uri

    def response_tag(self):
        return '{%s}%s' % (self.namespace, self.field_uri)

Ancestors

Methods

def field_uri_xml(self, field_uri, label)
Expand source code
def field_uri_xml(self, field_uri, label):
    from .properties import IndexedFieldURI
    return IndexedFieldURI(field_uri='%s:%s' % (field_uri, self.field_uri), field_index=label).to_xml(version=None)
def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    field_elem = elem.find(self.response_tag())
    val = None if field_elem is None else field_elem.text or None
    if val is not None:
        return val
    return self.default
def request_tag(self)
Expand source code
def request_tag(self):
    return 't:%s' % self.field_uri
def response_tag(self)
Expand source code
def response_tag(self):
    return '{%s}%s' % (self.namespace, self.field_uri)
def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    field_elem = create_element(self.request_tag())
    return set_xml_value(field_elem, value, version=version)

Inherited members

class OccurrenceField (*args, **kwargs)

A generic field for any EWSElement object.

Expand source code
class OccurrenceField(EWSElementField):
    is_complex = True

Ancestors

Subclasses

Class variables

var is_complex
class OccurrenceListField (*args, **kwargs)

A generic field for any EWSElement object.

Expand source code
class OccurrenceListField(OccurrenceField):
    is_list = True

Ancestors

Class variables

var is_list
class OnOffField (*args, **kwargs)

A field that handles boolean values that are On/Off instead of True/False.

Expand source code
class OnOffField(BooleanField):
    """A field that handles boolean values that are On/Off instead of True/False."""

Ancestors

Inherited members

class PermissionSetField (*args, **kwargs)

A generic field for any EWSElement object.

Expand source code
class PermissionSetField(EWSElementField):
    is_complex = True

    def __init__(self, *args, **kwargs):
        from .properties import PermissionSet
        kwargs['value_cls'] = PermissionSet
        super().__init__(*args, **kwargs)

Ancestors

Class variables

var is_complex
class PersonaPhoneNumberField (*args, **kwargs)

A generic field for any EWSElement object.

Expand source code
class PersonaPhoneNumberField(EWSElementField):
    is_complex = True

    def __init__(self, *args, **kwargs):
        from .properties import PhoneNumber
        kwargs['value_cls'] = PhoneNumber
        super().__init__(*args, **kwargs)

Ancestors

Class variables

var is_complex
class PhoneNumberAttributedValueField (*args, **kwargs)

Like EWSElementField, but for lists of EWSElement objects.

Expand source code
class PhoneNumberAttributedValueField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import PhoneNumberAttributedValue
        kwargs['value_cls'] = PhoneNumberAttributedValue
        super().__init__(*args, **kwargs)

Ancestors

class PhoneNumberField (*args, **kwargs)

A base class for all indexed fields.

Expand source code
class PhoneNumberField(IndexedField):
    is_list = True

    PARENT_ELEMENT_NAME = 'PhoneNumbers'

    def __init__(self, *args, **kwargs):
        from .indexed_properties import PhoneNumber
        kwargs['value_cls'] = PhoneNumber
        super().__init__(*args, **kwargs)

Ancestors

Class variables

var PARENT_ELEMENT_NAME
var is_list
class PhysicalAddressField (*args, **kwargs)

A base class for all indexed fields.

Expand source code
class PhysicalAddressField(IndexedField):
    is_list = True

    PARENT_ELEMENT_NAME = 'PhysicalAddresses'

    def __init__(self, *args, **kwargs):
        from .indexed_properties import PhysicalAddress
        kwargs['value_cls'] = PhysicalAddress
        super().__init__(*args, **kwargs)

Ancestors

Class variables

var PARENT_ELEMENT_NAME
var is_list
class PostalAddressAttributedValueField (*args, **kwargs)

Like EWSElementField, but for lists of EWSElement objects.

Expand source code
class PostalAddressAttributedValueField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import PostalAddressAttributedValue
        kwargs['value_cls'] = PostalAddressAttributedValue
        super().__init__(*args, **kwargs)

Ancestors

class ProtocolListField (*args, **kwargs)

Like EWSElementField, but for lists of EWSElement objects.

Expand source code
class ProtocolListField(EWSElementListField):
    # There is not containing element for this field. Just multiple 'Protocol' elements on the 'Account' element.
    def __init__(self, *args, **kwargs):
        from .autodiscover.properties import Protocol
        kwargs['value_cls'] = Protocol
        super().__init__(*args, **kwargs)

    def from_xml(self, elem, account):
        return [self.value_cls.from_xml(elem=e, account=account) for e in elem.findall(self.value_cls.response_tag())]

Ancestors

Methods

def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    return [self.value_cls.from_xml(elem=e, account=account) for e in elem.findall(self.value_cls.response_tag())]
class RecipientAddressField (*args, **kwargs)

A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for.

Expand source code
class RecipientAddressField(BaseEmailField):
    def __init__(self, *args, **kwargs):
        from .properties import RecipientAddress
        kwargs['value_cls'] = RecipientAddress
        super().__init__(*args, **kwargs)

Ancestors

class RecurrenceField (*args, **kwargs)

A generic field for any EWSElement object.

Expand source code
class RecurrenceField(EWSElementField):
    is_complex = True

    def __init__(self, *args, **kwargs):
        from .recurrence import Recurrence
        kwargs['value_cls'] = Recurrence
        super().__init__(*args, **kwargs)

    def to_xml(self, value, version):
        return value.to_xml(version=version)

Ancestors

Class variables

var is_complex

Methods

def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    return value.to_xml(version=version)
class ReferenceItemIdField (*args, **kwargs)

A generic field for any EWSElement object.

Expand source code
class ReferenceItemIdField(EWSElementField):
    is_complex = True

    def __init__(self, *args, **kwargs):
        from .properties import ReferenceItemId
        kwargs['value_cls'] = ReferenceItemId
        super().__init__(*args, **kwargs)

    def to_xml(self, value, version):
        return value.to_xml(version=version)

Ancestors

Class variables

var is_complex

Methods

def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    return value.to_xml(version=version)
class RoutingTypeField (*args, **kwargs)

Like CharField, but restricts the value to a limited set of strings.

Expand source code
class RoutingTypeField(ChoiceField):
    def __init__(self, *args, **kwargs):
        kwargs['choices'] = {Choice('SMTP'), Choice('EX')}
        kwargs['default'] = 'SMTP'
        super().__init__(*args, **kwargs)

Ancestors

Inherited members

class StringAttributedValueField (*args, **kwargs)

Like EWSElementField, but for lists of EWSElement objects.

Expand source code
class StringAttributedValueField(EWSElementListField):
    def __init__(self, *args, **kwargs):
        from .properties import StringAttributedValue
        kwargs['value_cls'] = StringAttributedValue
        super().__init__(*args, **kwargs)

Ancestors

class SubField (name=None, is_required=False, is_required_after_save=False, is_read_only=False, is_read_only_after_send=False, is_searchable=True, is_attribute=False, default=None, supported_from=None, deprecated_from=None)

A field to hold the value on an IndexedElement.

Expand source code
class SubField(Field):
    """A field to hold the value on an IndexedElement."""

    namespace = TNS
    value_cls = str

    def from_xml(self, elem, account):
        return elem.text

    def to_xml(self, value, version):
        return value

    @staticmethod
    def field_uri_xml(field_uri, label):
        from .properties import IndexedFieldURI
        return IndexedFieldURI(field_uri=field_uri, field_index=label).to_xml(version=None)

    def clean(self, value, version=None):
        value = super().clean(value, version=version)
        if self.is_required and not value:
            raise ValueError('Value for subfield %r must be non-empty' % self.name)
        return value

    def __hash__(self):
        return hash(self.name)

Ancestors

Subclasses

Class variables

var namespace
var value_cls

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

Static methods

def field_uri_xml(field_uri, label)
Expand source code
@staticmethod
def field_uri_xml(field_uri, label):
    from .properties import IndexedFieldURI
    return IndexedFieldURI(field_uri=field_uri, field_index=label).to_xml(version=None)

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    value = super().clean(value, version=version)
    if self.is_required and not value:
        raise ValueError('Value for subfield %r must be non-empty' % self.name)
    return value
def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    return elem.text
def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    return value
class TaskRecurrenceField (*args, **kwargs)

A generic field for any EWSElement object.

Expand source code
class TaskRecurrenceField(EWSElementField):
    is_complex = True

    def __init__(self, *args, **kwargs):
        from .recurrence import TaskRecurrence
        kwargs['value_cls'] = TaskRecurrence
        super().__init__(*args, **kwargs)

    def to_xml(self, value, version):
        return value.to_xml(version=version)

Ancestors

Class variables

var is_complex

Methods

def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    return value.to_xml(version=version)
class TextField (*args, **kwargs)

A field that stores a string value with no length limit.

Expand source code
class TextField(FieldURIField):
    """A field that stores a string value with no length limit."""

    value_cls = str
    is_complex = True

Ancestors

Subclasses

Class variables

var is_complex
var value_cls

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

class TextListField (*args, **kwargs)

Like TextField, but for lists of text.

Expand source code
class TextListField(TextField):
    """Like TextField, but for lists of text."""

    is_list = True

    def from_xml(self, elem, account):
        iter_elem = elem.find(self.response_tag())
        if iter_elem is not None:
            return get_xml_attrs(iter_elem, '{%s}String' % TNS)
        return self.default

Ancestors

Class variables

var is_list

Methods

def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    iter_elem = elem.find(self.response_tag())
    if iter_elem is not None:
        return get_xml_attrs(iter_elem, '{%s}String' % TNS)
    return self.default

Inherited members

class TimeField (*args, **kwargs)

A field that handles time values.

Expand source code
class TimeField(FieldURIField):
    """A field that handles time values."""

    value_cls = datetime.time

    def from_xml(self, elem, account):
        val = self._get_val_from_elem(elem)
        if val is not None:
            try:
                if ':' in val:
                    # Assume a string of the form HH:MM:SS
                    return datetime.datetime.strptime(val, '%H:%M:%S').time()
                # Assume an integer in minutes since midnight
                return (datetime.datetime(2000, 1, 1) + datetime.timedelta(minutes=int(val))).time()
            except ValueError:
                pass
        return self.default

Ancestors

Class variables

var value_cls

time([hour[, minute[, second[, microsecond[, tzinfo]]]]]) –> a time object

All arguments are optional. tzinfo may be None, or an instance of a tzinfo subclass. The remaining arguments may be ints.

Methods

def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    val = self._get_val_from_elem(elem)
    if val is not None:
        try:
            if ':' in val:
                # Assume a string of the form HH:MM:SS
                return datetime.datetime.strptime(val, '%H:%M:%S').time()
            # Assume an integer in minutes since midnight
            return (datetime.datetime(2000, 1, 1) + datetime.timedelta(minutes=int(val))).time()
        except ValueError:
            pass
    return self.default
class TimeZoneField (*args, **kwargs)

A field that handles timezone values.

Expand source code
class TimeZoneField(FieldURIField):
    """A field that handles timezone values."""

    value_cls = EWSTimeZone

    def clean(self, value, version=None):
        # Allow other timezone implementations as input
        if value is not None:
            value = self.value_cls.from_timezone(value)
        return super().clean(value=value, version=version)

    def from_xml(self, elem, account):
        field_elem = elem.find(self.response_tag())
        if field_elem is not None:
            ms_id = field_elem.get('Id')
            ms_name = field_elem.get('Name')
            try:
                return self.value_cls.from_ms_id(ms_id or ms_name)
            except UnknownTimeZone:
                log.warning(
                    "Cannot convert value '%s' on field '%s' to type %s (unknown timezone ID)",
                    (ms_id or ms_name), self.name, self.value_cls
                )
                return None
        return self.default

    def to_xml(self, value, version):
        attrs = OrderedDict([('Id', value.ms_id)])
        if value.ms_name:
            attrs['Name'] = value.ms_name
        return create_element(self.request_tag(), attrs=attrs)

Ancestors

Class variables

var value_cls

Represents a timezone as expected by the EWS TimezoneContext / TimezoneDefinition XML element, and returned by services.GetServerTimeZones.

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    # Allow other timezone implementations as input
    if value is not None:
        value = self.value_cls.from_timezone(value)
    return super().clean(value=value, version=version)
def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    field_elem = elem.find(self.response_tag())
    if field_elem is not None:
        ms_id = field_elem.get('Id')
        ms_name = field_elem.get('Name')
        try:
            return self.value_cls.from_ms_id(ms_id or ms_name)
        except UnknownTimeZone:
            log.warning(
                "Cannot convert value '%s' on field '%s' to type %s (unknown timezone ID)",
                (ms_id or ms_name), self.name, self.value_cls
            )
            return None
    return self.default
def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    attrs = OrderedDict([('Id', value.ms_id)])
    if value.ms_name:
        attrs['Name'] = value.ms_name
    return create_element(self.request_tag(), attrs=attrs)
class TypeValueField (*args, **kwargs)

This field type has no value_cls because values may have many different types.

Expand source code
class TypeValueField(FieldURIField):
    """This field type has no value_cls because values may have many different types."""

    TYPES_MAP = {
        'Boolean': bool,
        'Integer32': int,
        'UnsignedInteger32': int,
        'Integer64': int,
        'UnsignedInteger64': int,
        # Python doesn't have a single-byte type to represent 'Byte'
        'ByteArray': bytes,
        'String': str,
        'StringArray': str,  # A list of strings
        'DateTime': EWSDateTime,
    }
    TYPES_MAP_REVERSED = {
        bool: 'Boolean',
        int: 'Integer64',
        # Python doesn't have a single-byte type to represent 'Byte'
        bytes: 'ByteArray',
        str: 'String',
        datetime.datetime: 'DateTime',
        EWSDateTime: 'DateTime',
    }

    @classmethod
    def get_type(cls, value):
        if isinstance(value, bytes) and len(value) == 1:
            # This is a single byte. Translate it to the 'Byte' type
            return 'Byte'
        if is_iterable(value):
            # We don't allow generators as values, so keep the logic simple
            try:
                first = next(iter(value))
            except StopIteration:
                first = None
            value_type = '%sArray' % cls.TYPES_MAP_REVERSED[type(first)]
            if value_type not in cls.TYPES_MAP:
                raise ValueError('%r is not a supported type' % value)
            return value_type
        return cls.TYPES_MAP_REVERSED[type(value)]

    @classmethod
    def is_array_type(cls, value_type):
        return value_type == 'StringArray'

    def clean(self, value, version=None):
        if value is None:
            if self.is_required and self.default is None:
                raise ValueError("'%s' is a required field with no default" % self.name)
            return self.default
        return value

    def from_xml(self, elem, account):
        field_elem = elem.find(self.response_tag())
        if field_elem is None:
            return self.default
        value_type_str = get_xml_attr(field_elem, '{%s}Type' % TNS)
        value = get_xml_attr(field_elem, '{%s}Value' % TNS)
        if value_type_str == 'Byte':
            try:
                # The value is an unsigned integer in the range 0 -> 255. Convert it to a single byte
                return xml_text_to_value(value, int).to_bytes(1, 'little', signed=False)
            except OverflowError as e:
                log.warning('Invalid byte value %r (%e)', value, e)
                return None
        value_type = self.TYPES_MAP[value_type_str]
        if self. is_array_type(value_type_str):
            return tuple(xml_text_to_value(value=v, value_type=value_type) for v in value.split(' '))
        return xml_text_to_value(value=value, value_type=value_type)

    def to_xml(self, value, version):
        value_type_str = self.get_type(value)
        if value_type_str == 'Byte':
            # A single byte is encoded to an unsigned integer in the range 0 -> 255
            value = int.from_bytes(value, byteorder='little', signed=False)
        elif is_iterable(value):
            value = ' '.join(value_to_xml_text(v) for v in value)
        field_elem = create_element(self.request_tag())
        field_elem.append(set_xml_value(create_element('t:Type'), value_type_str, version=version))
        field_elem.append(set_xml_value(create_element('t:Value'), value, version=version))
        return field_elem

Ancestors

Class variables

var TYPES_MAP
var TYPES_MAP_REVERSED

Static methods

def get_type(value)
Expand source code
@classmethod
def get_type(cls, value):
    if isinstance(value, bytes) and len(value) == 1:
        # This is a single byte. Translate it to the 'Byte' type
        return 'Byte'
    if is_iterable(value):
        # We don't allow generators as values, so keep the logic simple
        try:
            first = next(iter(value))
        except StopIteration:
            first = None
        value_type = '%sArray' % cls.TYPES_MAP_REVERSED[type(first)]
        if value_type not in cls.TYPES_MAP:
            raise ValueError('%r is not a supported type' % value)
        return value_type
    return cls.TYPES_MAP_REVERSED[type(value)]
def is_array_type(value_type)
Expand source code
@classmethod
def is_array_type(cls, value_type):
    return value_type == 'StringArray'

Methods

def clean(self, value, version=None)
Expand source code
def clean(self, value, version=None):
    if value is None:
        if self.is_required and self.default is None:
            raise ValueError("'%s' is a required field with no default" % self.name)
        return self.default
    return value
def from_xml(self, elem, account)
Expand source code
def from_xml(self, elem, account):
    field_elem = elem.find(self.response_tag())
    if field_elem is None:
        return self.default
    value_type_str = get_xml_attr(field_elem, '{%s}Type' % TNS)
    value = get_xml_attr(field_elem, '{%s}Value' % TNS)
    if value_type_str == 'Byte':
        try:
            # The value is an unsigned integer in the range 0 -> 255. Convert it to a single byte
            return xml_text_to_value(value, int).to_bytes(1, 'little', signed=False)
        except OverflowError as e:
            log.warning('Invalid byte value %r (%e)', value, e)
            return None
    value_type = self.TYPES_MAP[value_type_str]
    if self. is_array_type(value_type_str):
        return tuple(xml_text_to_value(value=v, value_type=value_type) for v in value.split(' '))
    return xml_text_to_value(value=value, value_type=value_type)
def to_xml(self, value, version)
Expand source code
def to_xml(self, value, version):
    value_type_str = self.get_type(value)
    if value_type_str == 'Byte':
        # A single byte is encoded to an unsigned integer in the range 0 -> 255
        value = int.from_bytes(value, byteorder='little', signed=False)
    elif is_iterable(value):
        value = ' '.join(value_to_xml_text(v) for v in value)
    field_elem = create_element(self.request_tag())
    field_elem.append(set_xml_value(create_element('t:Type'), value_type_str, version=version))
    field_elem.append(set_xml_value(create_element('t:Value'), value, version=version))
    return field_elem
class URIField (*args, **kwargs)

Helper to mark strings that must conform to xsd:anyURI. If we want an URI validator, see http://stackoverflow.com/questions/14466585/is-this-regex-correct-for-xsdanyuri

Expand source code
class URIField(TextField):
    """Helper to mark strings that must conform to xsd:anyURI.
    If we want an URI validator, see http://stackoverflow.com/questions/14466585/is-this-regex-correct-for-xsdanyuri
    """

Ancestors

Inherited members

class UnknownEntriesField (*args, **kwargs)

Like CharField, but for lists of strings.

Expand source code
class UnknownEntriesField(CharListField):
    def list_elem_tag(self):
        return '{%s}UnknownEntry' % self.namespace

Ancestors

Methods

def list_elem_tag(self)
Expand source code
def list_elem_tag(self):
    return '{%s}UnknownEntry' % self.namespace

Inherited members

exchangelib-4.6.1/docs/exchangelib/folders/000077500000000000000000000000001414601472700206405ustar00rootroot00000000000000exchangelib-4.6.1/docs/exchangelib/folders/base.html000066400000000000000000005311421414601472700224460ustar00rootroot00000000000000 exchangelib.folders.base API documentation

Module exchangelib.folders.base

Expand source code
import abc
import logging
from fnmatch import fnmatch
from operator import attrgetter

from .collections import FolderCollection, SyncCompleted
from .queryset import SingleFolderQuerySet, SHALLOW as SHALLOW_FOLDERS, DEEP as DEEP_FOLDERS
from ..errors import ErrorAccessDenied, ErrorFolderNotFound, ErrorCannotEmptyFolder, ErrorCannotDeleteObject, \
    ErrorDeleteDistinguishedFolder, ErrorInvalidSubscription, ErrorNoPublicFolderReplicaAvailable, ErrorItemNotFound
from ..fields import IntegerField, CharField, FieldPath, EffectiveRightsField, PermissionSetField, EWSElementField, \
    Field, IdElementField, InvalidField
from ..items import CalendarItem, RegisterMixIn, ITEM_CLASSES, DELETE_TYPE_CHOICES, HARD_DELETE, \
    SHALLOW as SHALLOW_ITEMS
from ..properties import Mailbox, FolderId, ParentFolderId, DistinguishedFolderId, UserConfiguration, \
    UserConfigurationName, UserConfigurationNameMNS, EWSMeta
from ..queryset import SearchableMixIn, DoesNotExist
from ..services import CreateFolder, UpdateFolder, DeleteFolder, EmptyFolder, GetUserConfiguration, \
    CreateUserConfiguration, UpdateUserConfiguration, DeleteUserConfiguration, SubscribeToPush, SubscribeToPull, \
    Unsubscribe, GetEvents, GetStreamingEvents, MoveFolder
from ..services.get_user_configuration import ALL
from ..util import TNS, require_id
from ..version import Version, EXCHANGE_2007_SP1, EXCHANGE_2010

log = logging.getLogger(__name__)

MISSING_FOLDER_ERRORS = (ErrorFolderNotFound, ErrorItemNotFound, ErrorNoPublicFolderReplicaAvailable)


class BaseFolder(RegisterMixIn, SearchableMixIn, metaclass=EWSMeta):
    """Base class for all classes that implement a folder."""

    ELEMENT_NAME = 'Folder'
    NAMESPACE = TNS
    # See https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid
    DISTINGUISHED_FOLDER_ID = None
    # Default item type for this folder. See
    # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxosfld/68a85898-84fe-43c4-b166-4711c13cdd61
    CONTAINER_CLASS = None
    supported_item_models = ITEM_CLASSES  # The Item types that this folder can contain. Default is all
    # Marks the version from which a distinguished folder was introduced. A possibly authoritative source is:
    # https://github.com/OfficeDev/ews-managed-api/blob/master/Enumerations/WellKnownFolderName.cs
    supported_from = None
    # Whether this folder type is allowed with the GetFolder service
    get_folder_allowed = True
    DEFAULT_FOLDER_TRAVERSAL_DEPTH = DEEP_FOLDERS
    DEFAULT_ITEM_TRAVERSAL_DEPTH = SHALLOW_ITEMS
    LOCALIZED_NAMES = {}  # A map of (str)locale: (tuple)localized_folder_names
    ITEM_MODEL_MAP = {cls.response_tag(): cls for cls in ITEM_CLASSES}
    ID_ELEMENT_CLS = FolderId

    _id = IdElementField(field_uri='folder:FolderId', value_cls=ID_ELEMENT_CLS)
    parent_folder_id = EWSElementField(field_uri='folder:ParentFolderId', value_cls=ParentFolderId,
                                       is_read_only=True)
    folder_class = CharField(field_uri='folder:FolderClass', is_required_after_save=True)
    name = CharField(field_uri='folder:DisplayName')
    total_count = IntegerField(field_uri='folder:TotalCount', is_read_only=True)
    child_folder_count = IntegerField(field_uri='folder:ChildFolderCount', is_read_only=True)
    unread_count = IntegerField(field_uri='folder:UnreadCount', is_read_only=True)

    __slots__ = 'is_distinguished', 'item_sync_state', 'folder_sync_state'

    # Used to register extended properties
    INSERT_AFTER_FIELD = 'child_folder_count'

    def __init__(self, **kwargs):
        self.is_distinguished = kwargs.pop('is_distinguished', False)
        self.item_sync_state = kwargs.pop('item_sync_state', None)
        self.folder_sync_state = kwargs.pop('folder_sync_state', None)
        super().__init__(**kwargs)

    @property
    @abc.abstractmethod
    def account(self):
        pass

    @property
    @abc.abstractmethod
    def root(self):
        pass

    @property
    @abc.abstractmethod
    def parent(self):
        pass

    @property
    def is_deletable(self):
        return not self.is_distinguished

    def clean(self, version=None):
        super().clean(version=version)
        # Set a default folder class for new folders. A folder class cannot be changed after saving.
        if self.id is None and self.folder_class is None:
            self.folder_class = self.CONTAINER_CLASS

    @property
    def children(self):
        # It's dangerous to return a generator here because we may then call methods on a child that result in the
        # cache being updated while it's iterated.
        return FolderCollection(account=self.account, folders=self.root.get_children(self))

    @property
    def parts(self):
        parts = [self]
        f = self.parent
        while f:
            parts.insert(0, f)
            f = f.parent
        return parts

    @property
    def absolute(self):
        return ''.join('/%s' % p.name for p in self.parts)

    def _walk(self):
        for c in self.children:
            yield c
            yield from c.walk()

    def walk(self):
        return FolderCollection(account=self.account, folders=self._walk())

    def _glob(self, pattern):
        split_pattern = pattern.rsplit('/', 1)
        head, tail = (split_pattern[0], None) if len(split_pattern) == 1 else split_pattern
        if head == '':
            # We got an absolute path. Restart globbing at root
            yield from self.root.glob(tail or '*')
        elif head == '..':
            # Relative path with reference to parent. Restart globbing at parent
            if not self.parent:
                raise ValueError('Already at top')
            yield from self.parent.glob(tail or '*')
        elif head == '**':
            # Match anything here or in any subfolder at arbitrary depth
            for c in self.walk():
                # fnmatch() may be case-sensitive depending on operating system:
                # force a case-insensitive match since case appears not to
                # matter for folders in Exchange
                if fnmatch(c.name.lower(), (tail or '*').lower()):
                    yield c
        else:
            # Regular pattern
            for c in self.children:
                # See note above on fnmatch() case-sensitivity
                if not fnmatch(c.name.lower(), head.lower()):
                    continue
                if tail is None:
                    yield c
                    continue
                yield from c.glob(tail)

    def glob(self, pattern):
        return FolderCollection(account=self.account, folders=self._glob(pattern))

    def tree(self):
        """Return a string representation of the folder structure of this folder. Example:

        root
        ├── inbox
        │   └── todos
        └── archive
            ├── Last Job
            ├── exchangelib issues
            └── Mom
        """
        tree = '%s\n' % self.name
        children = list(self.children)
        for i, c in enumerate(sorted(children, key=attrgetter('name')), start=1):
            nodes = c.tree().split('\n')
            for j, node in enumerate(nodes, start=1):
                if i != len(children) and j == 1:
                    # Not the last child, but the first node, which is the name of the child
                    tree += '├── %s\n' % node
                elif i != len(children) and j > 1:
                    # Not the last child, and not name of child
                    tree += '│   %s\n' % node
                elif i == len(children) and j == 1:
                    # Not the last child, but the first node, which is the name of the child
                    tree += '└── %s\n' % node
                else:  # Last child, and not name of child
                    tree += '    %s\n' % node
        return tree.strip()

    @classmethod
    def supports_version(cls, version):
        # 'version' is a Version instance, for convenience by callers
        if not isinstance(version, Version):
            raise ValueError("'version' %r must be a Version instance" % version)
        if not cls.supported_from:
            return True
        return version.build >= cls.supported_from

    @property
    def has_distinguished_name(self):
        return self.name and self.DISTINGUISHED_FOLDER_ID and self.name.lower() == self.DISTINGUISHED_FOLDER_ID.lower()

    @classmethod
    def localized_names(cls, locale):
        # Return localized names for a specific locale. If no locale-specific names exist, return the default names,
        # if any.
        return tuple(s.lower() for s in cls.LOCALIZED_NAMES.get(locale, cls.LOCALIZED_NAMES.get(None, [])))

    @staticmethod
    def folder_cls_from_container_class(container_class):
        """Return a reasonable folder class given a container class, e.g. 'IPF.Note'. Don't iterate WELLKNOWN_FOLDERS
        because many folder classes have the same CONTAINER_CLASS.

        :param container_class:
        :return:
        """
        from .known_folders import Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, \
            RecipientCache, RSSFeeds
        for folder_cls in (
                Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, RecipientCache,
                RSSFeeds):
            if folder_cls.CONTAINER_CLASS == container_class:
                return folder_cls
        raise KeyError()

    @classmethod
    def item_model_from_tag(cls, tag):
        try:
            return cls.ITEM_MODEL_MAP[tag]
        except KeyError:
            raise ValueError('Item type %s was unexpected in a %s folder' % (tag, cls.__name__))

    @classmethod
    def allowed_item_fields(cls, version):
        # Return non-ID fields of all item classes allowed in this folder type
        fields = set()
        for item_model in cls.supported_item_models:
            fields.update(
                set(item_model.supported_fields(version=version))
            )
        return fields

    def validate_item_field(self, field, version):
        # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
        # for the item types supported by this folder.

        # For each field, check if the field is valid for any of the item models supported by this folder
        for item_model in self.supported_item_models:
            try:
                item_model.validate_field(field=field, version=version)
                break
            except InvalidField:
                continue
        else:
            raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))

    def normalize_fields(self, fields):
        # Takes a list of fieldnames, Field or FieldPath objects pointing to item fields. Turns them into FieldPath
        # objects and adds internal timezone fields if necessary. Assume fields are already validated.
        fields = list(fields)
        has_start, has_end = False, False
        for i, field_path in enumerate(fields):
            # Allow both Field and FieldPath instances and string field paths as input
            if isinstance(field_path, str):
                field_path = FieldPath.from_string(field_path=field_path, folder=self)
                fields[i] = field_path
            elif isinstance(field_path, Field):
                field_path = FieldPath(field=field_path)
                fields[i] = field_path
            if not isinstance(field_path, FieldPath):
                raise ValueError("Field %r must be a string or FieldPath instance" % field_path)
            if field_path.field.name == 'start':
                has_start = True
            elif field_path.field.name == 'end':
                has_end = True

        # For CalendarItem items, we want to inject internal timezone fields. See also CalendarItem.clean()
        if CalendarItem in self.supported_item_models:
            meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
            if self.account.version.build < EXCHANGE_2010:
                if has_start or has_end:
                    fields.append(FieldPath(field=meeting_tz_field))
            else:
                if has_start:
                    fields.append(FieldPath(field=start_tz_field))
                if has_end:
                    fields.append(FieldPath(field=end_tz_field))
        return fields

    @classmethod
    def get_item_field_by_fieldname(cls, fieldname):
        for item_model in cls.supported_item_models:
            try:
                return item_model.get_field_by_fieldname(fieldname)
            except InvalidField:
                pass
        raise InvalidField("%r is not a valid field name on %s" % (fieldname, cls.supported_item_models))

    def get(self, *args, **kwargs):
        return FolderCollection(account=self.account, folders=[self]).get(*args, **kwargs)

    def all(self):
        return FolderCollection(account=self.account, folders=[self]).all()

    def none(self):
        return FolderCollection(account=self.account, folders=[self]).none()

    def filter(self, *args, **kwargs):
        return FolderCollection(account=self.account, folders=[self]).filter(*args, **kwargs)

    def exclude(self, *args, **kwargs):
        return FolderCollection(account=self.account, folders=[self]).exclude(*args, **kwargs)

    def people(self):
        # No point in using a FolderCollection because FindPeople only supports one folder
        return FolderCollection(account=self.account, folders=[self]).people()

    def bulk_create(self, items, *args, **kwargs):
        return self.account.bulk_create(folder=self, items=items, *args, **kwargs)

    def save(self, update_fields=None):
        if self.id is None:
            # New folder
            if update_fields:
                raise ValueError("'update_fields' is only valid for updates")
            res = CreateFolder(account=self.account).get(parent_folder=self.parent, folders=[self])
            self._id = self.ID_ELEMENT_CLS(res.id, res.changekey)
            self.root.add_folder(self)  # Add this folder to the cache
            return self

        # Update folder
        if not update_fields:
            # The fields to update was not specified explicitly. Update all fields where update is possible
            update_fields = []
            for f in self.supported_fields(version=self.account.version):
                if f.is_read_only:
                    # These cannot be changed
                    continue
                if (f.is_required or f.is_required_after_save) and (
                        getattr(self, f.name) is None or (f.is_list and not getattr(self, f.name))
                ):
                    # These are required and cannot be deleted
                    continue
                update_fields.append(f.name)
        res = UpdateFolder(account=self.account).get(folders=[(self, update_fields)])
        folder_id, changekey = res.id, res.changekey
        if self.id != folder_id:
            raise ValueError('ID mismatch')
        # Don't check changekey value. It may not change on no-op updates
        self.changekey = changekey
        self.root.update_folder(self)  # Update the folder in the cache
        return self

    def move(self, to_folder):
        res = MoveFolder(account=self.account).get(folders=[self], to_folder=to_folder)
        folder_id, changekey = res.id, res.changekey
        if self.id != folder_id:
            raise ValueError('ID mismatch')
        # Don't check changekey value. It may not change on no-op moves
        self.changekey = changekey
        self.parent_folder_id = ParentFolderId(id=to_folder.id, changekey=to_folder.changekey)
        self.root.update_folder(self)  # Update the folder in the cache

    def delete(self, delete_type=HARD_DELETE):
        if delete_type not in DELETE_TYPE_CHOICES:
            raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
        DeleteFolder(account=self.account).get(folders=[self], delete_type=delete_type)
        self.root.remove_folder(self)  # Remove the updated folder from the cache
        self._id = None

    def empty(self, delete_type=HARD_DELETE, delete_sub_folders=False):
        if delete_type not in DELETE_TYPE_CHOICES:
            raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
        EmptyFolder(account=self.account).get(
            folders=[self], delete_type=delete_type, delete_sub_folders=delete_sub_folders
        )
        if delete_sub_folders:
            # We don't know exactly what was deleted, so invalidate the entire folder cache to be safe
            self.root.clear_cache()

    def wipe(self, page_size=None, _seen=None, _level=0):
        # Recursively deletes all items in this folder, and all subfolders and their content. Attempts to protect
        # distinguished folders from being deleted. Use with caution!
        _seen = _seen or set()
        if self.id in _seen:
            raise RecursionError('We already tried to wipe %s' % self)
        if _level > 16:
            raise RecursionError('Max recursion level reached: %s' % _level)
        _seen.add(self.id)
        log.warning('Wiping %s', self)
        has_distinguished_subfolders = any(f.is_distinguished for f in self.children)
        try:
            if has_distinguished_subfolders:
                self.empty(delete_sub_folders=False)
            else:
                self.empty(delete_sub_folders=True)
        except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
            try:
                if has_distinguished_subfolders:
                    raise  # We already tried this
                self.empty(delete_sub_folders=False)
            except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
                log.warning('Not allowed to empty %s. Trying to delete items instead', self)
                try:
                    self.all().delete(**dict(page_size=page_size) if page_size else {})
                except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound):
                    log.warning('Not allowed to delete items in %s', self)
        _level += 1
        for f in self.children:
            f.wipe(page_size=page_size, _seen=_seen, _level=_level)
            # Remove non-distinguished children that are empty and have no subfolders
            if f.is_deletable and not f.children:
                log.warning('Deleting folder %s', f)
                try:
                    f.delete()
                except ErrorDeleteDistinguishedFolder:
                    log.warning('Tried to delete a distinguished folder (%s)', f)

    def test_access(self):
        """Does a simple FindItem to test (read) access to the folder. Maybe the account doesn't exist, maybe the
        service user doesn't have access to the calendar. This will throw the most common errors.
        """
        self.all().exists()
        return True

    @classmethod
    def _kwargs_from_elem(cls, elem, account):
        # Check for 'DisplayName' element before collecting kwargs because because that clears the elements
        has_name_elem = elem.find(cls.get_field_by_fieldname('name').response_tag()) is not None
        kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
        if has_name_elem and not kwargs['name']:
            # When we request the 'DisplayName' property, some folders may still be returned with an empty value.
            # Assign a default name to these folders.
            kwargs['name'] = cls.DISTINGUISHED_FOLDER_ID
        return kwargs

    def to_folder_id(self):
        if self.is_distinguished:
            # Don't add the changekey here. When modifying folder content, we usually don't care if others have changed
            # the folder content since we fetched the changekey.
            if self.account:
                return DistinguishedFolderId(
                    id=self.DISTINGUISHED_FOLDER_ID,
                    mailbox=Mailbox(email_address=self.account.primary_smtp_address)
                )
            return DistinguishedFolderId(id=self.DISTINGUISHED_FOLDER_ID)
        if self.id:
            return FolderId(id=self.id, changekey=self.changekey)
        raise ValueError('Must be a distinguished folder or have an ID')

    def to_xml(self, version):
        try:
            return self.to_folder_id().to_xml(version=version)
        except ValueError:
            return super().to_xml(version=version)

    def to_id_xml(self, version):
        # Folder(name='Foo') is a perfectly valid ID to e.g. create a folder
        return self.to_xml(version=version)

    @classmethod
    def resolve(cls, account, folder):
        # Resolve a single folder
        folders = list(FolderCollection(account=account, folders=[folder]).resolve())
        if not folders:
            raise ErrorFolderNotFound('Could not find folder %r' % folder)
        if len(folders) != 1:
            raise ValueError('Expected result length 1, but got %s' % folders)
        f = folders[0]
        if isinstance(f, Exception):
            raise f
        if f.__class__ != cls:
            raise ValueError("Expected folder %r to be a %s instance" % (f, cls))
        return f

    @require_id
    def refresh(self):
        fresh_folder = self.resolve(account=self.account, folder=self)
        if self.id != fresh_folder.id:
            raise ValueError('ID mismatch')
        # Apparently, the changekey may get updated
        for f in self.FIELDS:
            setattr(self, f.name, getattr(fresh_folder, f.name))
        return self

    @require_id
    def get_user_configuration(self, name, properties=ALL):
        return GetUserConfiguration(account=self.account).get(
            user_configuration_name=UserConfigurationNameMNS(name=name, folder=self),
            properties=properties,
        )

    @require_id
    def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
        user_configuration = UserConfiguration(
            user_configuration_name=UserConfigurationName(name=name, folder=self),
            dictionary=dictionary,
            xml_data=xml_data,
            binary_data=binary_data,
        )
        return CreateUserConfiguration(account=self.account).get(user_configuration=user_configuration)

    @require_id
    def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
        user_configuration = UserConfiguration(
            user_configuration_name=UserConfigurationName(name=name, folder=self),
            dictionary=dictionary,
            xml_data=xml_data,
            binary_data=binary_data,
        )
        return UpdateUserConfiguration(account=self.account).get(user_configuration=user_configuration)

    @require_id
    def delete_user_configuration(self, name):
        return DeleteUserConfiguration(account=self.account).get(
            user_configuration_name=UserConfigurationNameMNS(name=name, folder=self)
        )

    @require_id
    def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, 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
        """
        s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
            event_types=event_types, watermark=watermark, timeout=timeout,
        ))
        if len(s_ids) != 1:
            raise ValueError('Expected result length 1, but got %s' % s_ids)
        s_id = s_ids[0]
        if isinstance(s_id, Exception):
            raise s_id
        return s_id

    @require_id
    def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, 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
        """
        s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
            event_types=event_types, watermark=watermark, status_frequency=status_frequency, callback_url=callback_url,
        ))
        if len(s_ids) != 1:
            raise ValueError('Expected result length 1, but got %s' % s_ids)
        s_id = s_ids[0]
        if isinstance(s_id, Exception):
            raise s_id
        return s_id

    @require_id
    def subscribe_to_streaming(self, event_types=SubscribeToPush.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
        """
        s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(
            event_types=event_types,
        ))
        if len(s_ids) != 1:
            raise ValueError('Expected result length 1, but got %s' % s_ids)
        s_id = s_ids[0]
        if isinstance(s_id, Exception):
            raise s_id
        return s_id

    @require_id
    def pull_subscription(self, **kwargs):
        return PullSubscription(folder=self, **kwargs)

    @require_id
    def push_subscription(self, **kwargs):
        return PushSubscription(folder=self, **kwargs)

    @require_id
    def streaming_subscription(self, **kwargs):
        return StreamingSubscription(folder=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 folder instance, but it makes sense to keep the method along the other
        sync methods.
        """
        return Unsubscribe(account=self.account).get(subscription_id=subscription_id)

    def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
        """Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after
        this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.

        :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
        :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
        :param ignore: A list of Item IDs to ignore in the sync
        :param max_changes_returned: The max number of change
        :param sync_scope: Specify whether to return just items, or items and folder associated information. Possible
           values are specified in SyncFolderitems.SYNC_SCOPES
        :return: A generator of (change_type, item) tuples
        """
        if not sync_state:
            sync_state = self.item_sync_state
        try:
            yield from FolderCollection(account=self.account, folders=[self]).sync_items(
                sync_state=sync_state,
                only_fields=only_fields,
                ignore=ignore,
                max_changes_returned=max_changes_returned,
                sync_scope=sync_scope,
            )
        except SyncCompleted as e:
            # Set the new sync state on the folder instance
            self.item_sync_state = e.sync_state

    def sync_hierarchy(self, sync_state=None, only_fields=None):
        """Return all folder changes to a folder hierarchy, as a generator. If sync_state is specified, get all folder
        changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new
        sync state.

        :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
        :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
        :return:
        """
        if not sync_state:
            sync_state = self.folder_sync_state
        try:
            yield from FolderCollection(account=self.account, folders=[self]).sync_hierarchy(
                sync_state=sync_state,
                only_fields=only_fields,
            )
        except SyncCompleted as e:
            # Set the new sync state on the folder instance
            self.folder_sync_state = e.sync_state

    def get_events(self, subscription_id, watermark):
        """Get events since the given watermark. Non-blocking.

        :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|push]()
        :param watermark: Either the watermark from the subscription, or as returned in the last .get_events() call.
        :return: A Notification object containing a list of events

        This method doesn't need the current folder instance, but it makes sense to keep the method along the other
        sync methods.
        """
        svc = GetEvents(account=self.account)
        while True:
            notification = svc.get(subscription_id=subscription_id, watermark=watermark)
            yield notification
            if not notification.more_events:
                break

    def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None):
        """Get events since the subscription was created, in streaming mode. This method will block as many minutes
        as specified by 'connection_timeout'.

        :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming()
        :param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout
        is reached.
        :param max_notifications_returned: If specified, will exit after receiving this number of notifications
        :return: A generator of Notification objects, each containing a list of events

        This method doesn't need the current folder instance, but it makes sense to keep the method along the other
        sync methods.
        """
        # Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
        request_timeout = connection_timeout*60 + 60
        svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
        for i, notification in enumerate(
                svc.call(subscription_ids=[subscription_id], connection_timeout=connection_timeout),
                start=1
        ):
            yield notification
            if max_notifications_returned and i >= max_notifications_returned:
                svc.stop_streaming()
                break
        if svc.error_subscription_ids:
            raise ErrorInvalidSubscription('Invalid subscription IDs: %s' % svc.error_subscription_ids)

    def __floordiv__(self, other):
        """Support the some_folder // 'child_folder' // 'child_of_child_folder' navigation syntax.

        Works like as __truediv__ but does not touch the folder cache.

        This is useful if the folder hierarchy contains a huge number of folders and you don't want to fetch them all

        :param other:
        :return:
        """
        if other == '..':
            raise ValueError('Cannot get parent without a folder cache')

        if other == '.':
            return self

        # Assume an exact match on the folder name in a shallow search will only return at most one folder
        try:
            return SingleFolderQuerySet(account=self.account, folder=self).depth(SHALLOW_FOLDERS).get(name=other)
        except DoesNotExist:
            raise ErrorFolderNotFound("No subfolder with name '%s'" % other)

    def __truediv__(self, other):
        """Support the some_folder / 'child_folder' / 'child_of_child_folder' navigation syntax."""
        if other == '..':
            if not self.parent:
                raise ValueError('Already at top')
            return self.parent
        if other == '.':
            return self
        for c in self.children:
            if c.name == other:
                return c
        raise ErrorFolderNotFound("No subfolder with name '%s'" % other)

    def __repr__(self):
        return self.__class__.__name__ + \
               repr((self.root, self.name, self.total_count, self.unread_count, self.child_folder_count,
                     self.folder_class, self.id, self.changekey))

    def __str__(self):
        return '%s (%s)' % (self.__class__.__name__, self.name)


class Folder(BaseFolder):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/folder"""

    permission_set = PermissionSetField(field_uri='folder:PermissionSet', supported_from=EXCHANGE_2007_SP1)
    effective_rights = EffectiveRightsField(field_uri='folder:EffectiveRights', is_read_only=True,
                                            supported_from=EXCHANGE_2007_SP1)

    __slots__ = '_root',

    def __init__(self, **kwargs):
        self._root = kwargs.pop('root', None)  # This is a pointer to the root of the folder hierarchy
        parent = kwargs.pop('parent', None)
        if parent:
            if self.root:
                if parent.root != self.root:
                    raise ValueError("'parent.root' must match 'root'")
            else:
                self.root = parent.root
            if 'parent_folder_id' in kwargs and parent.id != kwargs['parent_folder_id']:
                raise ValueError("'parent_folder_id' must match 'parent' ID")
            kwargs['parent_folder_id'] = ParentFolderId(id=parent.id, changekey=parent.changekey)
        super().__init__(**kwargs)

    @property
    def account(self):
        if self.root is None:
            return None
        return self.root.account

    @property
    def root(self):
        return self._root

    @root.setter
    def root(self, value):
        self._root = value

    @classmethod
    def register(cls, *args, **kwargs):
        if cls is not Folder:
            raise TypeError('For folders, custom fields must be registered on the Folder class')
        return super().register(*args, **kwargs)

    @classmethod
    def deregister(cls, *args, **kwargs):
        if cls is not Folder:
            raise TypeError('For folders, custom fields must be registered on the Folder class')
        return super().deregister(*args, **kwargs)

    @classmethod
    def get_distinguished(cls, root):
        """Get the distinguished folder for this folder class.

        :param root:
        :return:
        """
        try:
            return cls.resolve(
                account=root.account,
                folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
            )
        except MISSING_FOLDER_ERRORS:
            raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID)

    @property
    def parent(self):
        if not self.parent_folder_id:
            return None
        if self.parent_folder_id.id == self.id:
            # Some folders have a parent that references itself. Avoid circular references here
            return None
        return self.root.get_folder(self.parent_folder_id)

    @parent.setter
    def parent(self, value):
        if value is None:
            self.parent_folder_id = None
        else:
            if not isinstance(value, BaseFolder):
                raise ValueError("'value' %r must be a Folder instance" % value)
            self.root = value.root
            self.parent_folder_id = ParentFolderId(id=value.id, changekey=value.changekey)

    def clean(self, version=None):
        from .roots import RootOfHierarchy
        super().clean(version=version)
        if self.root and not isinstance(self.root, RootOfHierarchy):
            raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root)

    @classmethod
    def from_xml_with_root(cls, elem, root):
        folder = cls.from_xml(elem=elem, account=root.account)
        folder_cls = cls
        if cls == Folder:
            # We were called on the generic Folder class. Try to find a more specific class to return objects as.
            #
            # The "FolderClass" element value is the only indication we have in the FindFolder response of which
            # folder class we should create the folder with. And many folders share the same 'FolderClass' value, e.g.
            # Inbox and DeletedItems. We want to distinguish between these because otherwise we can't locate the right
            # folders types for e.g. Account.inbox and Account.trash.
            #
            # We should be able to just use the name, but apparently default folder names can be renamed to a set of
            # localized names using a PowerShell command:
            # https://docs.microsoft.com/en-us/powershell/module/exchange/client-access/Set-MailboxRegionalConfiguration
            #
            # Instead, search for a folder class using the localized name. If none are found, fall back to getting the
            # folder class by the "FolderClass" value.
            #
            # The returned XML may contain neither folder class nor name. In that case, we default to the generic
            # Folder class.
            if folder.name:
                try:
                    # TODO: fld_class.LOCALIZED_NAMES is most definitely neither complete nor authoritative
                    folder_cls = root.folder_cls_from_folder_name(folder_name=folder.name,
                                                                  locale=root.account.locale)
                    log.debug('Folder class %s matches localized folder name %s', folder_cls, folder.name)
                except KeyError:
                    pass
            if folder.folder_class and folder_cls == Folder:
                try:
                    folder_cls = cls.folder_cls_from_container_class(container_class=folder.folder_class)
                    log.debug('Folder class %s matches container class %s (%s)', folder_cls, folder.folder_class,
                              folder.name)
                except KeyError:
                    pass
            if folder_cls == Folder:
                log.debug('Fallback to class Folder (folder_class %s, name %s)', folder.folder_class, folder.name)
        return folder_cls(root=root, **{f.name: getattr(folder, f.name) for f in folder.FIELDS})


class BaseSubscription(metaclass=abc.ABCMeta):
    def __init__(self, folder, **subscription_kwargs):
        self.folder = folder
        self.subscription_kwargs = subscription_kwargs
        self.subscription_id = None

    def __enter__(self):
        pass

    def __exit__(self, *args, **kwargs):
        self.folder.unsubscribe(subscription_id=self.subscription_id)
        self.subscription_id = None


class PullSubscription(BaseSubscription):
    def __enter__(self):
        self.subscription_id, watermark = self.folder.subscribe_to_pull(**self.subscription_kwargs)
        return self.subscription_id, watermark


class PushSubscription(BaseSubscription):
    def __enter__(self):
        self.subscription_id, watermark = self.folder.subscribe_to_push(**self.subscription_kwargs)
        return self.subscription_id, watermark

    def __exit__(self, *args, **kwargs):
        # Cannot unsubscribe to push subscriptions
        pass


class StreamingSubscription(BaseSubscription):
    def __enter__(self):
        self.subscription_id = self.folder.subscribe_to_streaming(**self.subscription_kwargs)
        return self.subscription_id

Classes

class BaseFolder (**kwargs)

Base class for all classes that implement a folder.

Expand source code
class BaseFolder(RegisterMixIn, SearchableMixIn, metaclass=EWSMeta):
    """Base class for all classes that implement a folder."""

    ELEMENT_NAME = 'Folder'
    NAMESPACE = TNS
    # See https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid
    DISTINGUISHED_FOLDER_ID = None
    # Default item type for this folder. See
    # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxosfld/68a85898-84fe-43c4-b166-4711c13cdd61
    CONTAINER_CLASS = None
    supported_item_models = ITEM_CLASSES  # The Item types that this folder can contain. Default is all
    # Marks the version from which a distinguished folder was introduced. A possibly authoritative source is:
    # https://github.com/OfficeDev/ews-managed-api/blob/master/Enumerations/WellKnownFolderName.cs
    supported_from = None
    # Whether this folder type is allowed with the GetFolder service
    get_folder_allowed = True
    DEFAULT_FOLDER_TRAVERSAL_DEPTH = DEEP_FOLDERS
    DEFAULT_ITEM_TRAVERSAL_DEPTH = SHALLOW_ITEMS
    LOCALIZED_NAMES = {}  # A map of (str)locale: (tuple)localized_folder_names
    ITEM_MODEL_MAP = {cls.response_tag(): cls for cls in ITEM_CLASSES}
    ID_ELEMENT_CLS = FolderId

    _id = IdElementField(field_uri='folder:FolderId', value_cls=ID_ELEMENT_CLS)
    parent_folder_id = EWSElementField(field_uri='folder:ParentFolderId', value_cls=ParentFolderId,
                                       is_read_only=True)
    folder_class = CharField(field_uri='folder:FolderClass', is_required_after_save=True)
    name = CharField(field_uri='folder:DisplayName')
    total_count = IntegerField(field_uri='folder:TotalCount', is_read_only=True)
    child_folder_count = IntegerField(field_uri='folder:ChildFolderCount', is_read_only=True)
    unread_count = IntegerField(field_uri='folder:UnreadCount', is_read_only=True)

    __slots__ = 'is_distinguished', 'item_sync_state', 'folder_sync_state'

    # Used to register extended properties
    INSERT_AFTER_FIELD = 'child_folder_count'

    def __init__(self, **kwargs):
        self.is_distinguished = kwargs.pop('is_distinguished', False)
        self.item_sync_state = kwargs.pop('item_sync_state', None)
        self.folder_sync_state = kwargs.pop('folder_sync_state', None)
        super().__init__(**kwargs)

    @property
    @abc.abstractmethod
    def account(self):
        pass

    @property
    @abc.abstractmethod
    def root(self):
        pass

    @property
    @abc.abstractmethod
    def parent(self):
        pass

    @property
    def is_deletable(self):
        return not self.is_distinguished

    def clean(self, version=None):
        super().clean(version=version)
        # Set a default folder class for new folders. A folder class cannot be changed after saving.
        if self.id is None and self.folder_class is None:
            self.folder_class = self.CONTAINER_CLASS

    @property
    def children(self):
        # It's dangerous to return a generator here because we may then call methods on a child that result in the
        # cache being updated while it's iterated.
        return FolderCollection(account=self.account, folders=self.root.get_children(self))

    @property
    def parts(self):
        parts = [self]
        f = self.parent
        while f:
            parts.insert(0, f)
            f = f.parent
        return parts

    @property
    def absolute(self):
        return ''.join('/%s' % p.name for p in self.parts)

    def _walk(self):
        for c in self.children:
            yield c
            yield from c.walk()

    def walk(self):
        return FolderCollection(account=self.account, folders=self._walk())

    def _glob(self, pattern):
        split_pattern = pattern.rsplit('/', 1)
        head, tail = (split_pattern[0], None) if len(split_pattern) == 1 else split_pattern
        if head == '':
            # We got an absolute path. Restart globbing at root
            yield from self.root.glob(tail or '*')
        elif head == '..':
            # Relative path with reference to parent. Restart globbing at parent
            if not self.parent:
                raise ValueError('Already at top')
            yield from self.parent.glob(tail or '*')
        elif head == '**':
            # Match anything here or in any subfolder at arbitrary depth
            for c in self.walk():
                # fnmatch() may be case-sensitive depending on operating system:
                # force a case-insensitive match since case appears not to
                # matter for folders in Exchange
                if fnmatch(c.name.lower(), (tail or '*').lower()):
                    yield c
        else:
            # Regular pattern
            for c in self.children:
                # See note above on fnmatch() case-sensitivity
                if not fnmatch(c.name.lower(), head.lower()):
                    continue
                if tail is None:
                    yield c
                    continue
                yield from c.glob(tail)

    def glob(self, pattern):
        return FolderCollection(account=self.account, folders=self._glob(pattern))

    def tree(self):
        """Return a string representation of the folder structure of this folder. Example:

        root
        ├── inbox
        │   └── todos
        └── archive
            ├── Last Job
            ├── exchangelib issues
            └── Mom
        """
        tree = '%s\n' % self.name
        children = list(self.children)
        for i, c in enumerate(sorted(children, key=attrgetter('name')), start=1):
            nodes = c.tree().split('\n')
            for j, node in enumerate(nodes, start=1):
                if i != len(children) and j == 1:
                    # Not the last child, but the first node, which is the name of the child
                    tree += '├── %s\n' % node
                elif i != len(children) and j > 1:
                    # Not the last child, and not name of child
                    tree += '│   %s\n' % node
                elif i == len(children) and j == 1:
                    # Not the last child, but the first node, which is the name of the child
                    tree += '└── %s\n' % node
                else:  # Last child, and not name of child
                    tree += '    %s\n' % node
        return tree.strip()

    @classmethod
    def supports_version(cls, version):
        # 'version' is a Version instance, for convenience by callers
        if not isinstance(version, Version):
            raise ValueError("'version' %r must be a Version instance" % version)
        if not cls.supported_from:
            return True
        return version.build >= cls.supported_from

    @property
    def has_distinguished_name(self):
        return self.name and self.DISTINGUISHED_FOLDER_ID and self.name.lower() == self.DISTINGUISHED_FOLDER_ID.lower()

    @classmethod
    def localized_names(cls, locale):
        # Return localized names for a specific locale. If no locale-specific names exist, return the default names,
        # if any.
        return tuple(s.lower() for s in cls.LOCALIZED_NAMES.get(locale, cls.LOCALIZED_NAMES.get(None, [])))

    @staticmethod
    def folder_cls_from_container_class(container_class):
        """Return a reasonable folder class given a container class, e.g. 'IPF.Note'. Don't iterate WELLKNOWN_FOLDERS
        because many folder classes have the same CONTAINER_CLASS.

        :param container_class:
        :return:
        """
        from .known_folders import Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, \
            RecipientCache, RSSFeeds
        for folder_cls in (
                Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, RecipientCache,
                RSSFeeds):
            if folder_cls.CONTAINER_CLASS == container_class:
                return folder_cls
        raise KeyError()

    @classmethod
    def item_model_from_tag(cls, tag):
        try:
            return cls.ITEM_MODEL_MAP[tag]
        except KeyError:
            raise ValueError('Item type %s was unexpected in a %s folder' % (tag, cls.__name__))

    @classmethod
    def allowed_item_fields(cls, version):
        # Return non-ID fields of all item classes allowed in this folder type
        fields = set()
        for item_model in cls.supported_item_models:
            fields.update(
                set(item_model.supported_fields(version=version))
            )
        return fields

    def validate_item_field(self, field, version):
        # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
        # for the item types supported by this folder.

        # For each field, check if the field is valid for any of the item models supported by this folder
        for item_model in self.supported_item_models:
            try:
                item_model.validate_field(field=field, version=version)
                break
            except InvalidField:
                continue
        else:
            raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))

    def normalize_fields(self, fields):
        # Takes a list of fieldnames, Field or FieldPath objects pointing to item fields. Turns them into FieldPath
        # objects and adds internal timezone fields if necessary. Assume fields are already validated.
        fields = list(fields)
        has_start, has_end = False, False
        for i, field_path in enumerate(fields):
            # Allow both Field and FieldPath instances and string field paths as input
            if isinstance(field_path, str):
                field_path = FieldPath.from_string(field_path=field_path, folder=self)
                fields[i] = field_path
            elif isinstance(field_path, Field):
                field_path = FieldPath(field=field_path)
                fields[i] = field_path
            if not isinstance(field_path, FieldPath):
                raise ValueError("Field %r must be a string or FieldPath instance" % field_path)
            if field_path.field.name == 'start':
                has_start = True
            elif field_path.field.name == 'end':
                has_end = True

        # For CalendarItem items, we want to inject internal timezone fields. See also CalendarItem.clean()
        if CalendarItem in self.supported_item_models:
            meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
            if self.account.version.build < EXCHANGE_2010:
                if has_start or has_end:
                    fields.append(FieldPath(field=meeting_tz_field))
            else:
                if has_start:
                    fields.append(FieldPath(field=start_tz_field))
                if has_end:
                    fields.append(FieldPath(field=end_tz_field))
        return fields

    @classmethod
    def get_item_field_by_fieldname(cls, fieldname):
        for item_model in cls.supported_item_models:
            try:
                return item_model.get_field_by_fieldname(fieldname)
            except InvalidField:
                pass
        raise InvalidField("%r is not a valid field name on %s" % (fieldname, cls.supported_item_models))

    def get(self, *args, **kwargs):
        return FolderCollection(account=self.account, folders=[self]).get(*args, **kwargs)

    def all(self):
        return FolderCollection(account=self.account, folders=[self]).all()

    def none(self):
        return FolderCollection(account=self.account, folders=[self]).none()

    def filter(self, *args, **kwargs):
        return FolderCollection(account=self.account, folders=[self]).filter(*args, **kwargs)

    def exclude(self, *args, **kwargs):
        return FolderCollection(account=self.account, folders=[self]).exclude(*args, **kwargs)

    def people(self):
        # No point in using a FolderCollection because FindPeople only supports one folder
        return FolderCollection(account=self.account, folders=[self]).people()

    def bulk_create(self, items, *args, **kwargs):
        return self.account.bulk_create(folder=self, items=items, *args, **kwargs)

    def save(self, update_fields=None):
        if self.id is None:
            # New folder
            if update_fields:
                raise ValueError("'update_fields' is only valid for updates")
            res = CreateFolder(account=self.account).get(parent_folder=self.parent, folders=[self])
            self._id = self.ID_ELEMENT_CLS(res.id, res.changekey)
            self.root.add_folder(self)  # Add this folder to the cache
            return self

        # Update folder
        if not update_fields:
            # The fields to update was not specified explicitly. Update all fields where update is possible
            update_fields = []
            for f in self.supported_fields(version=self.account.version):
                if f.is_read_only:
                    # These cannot be changed
                    continue
                if (f.is_required or f.is_required_after_save) and (
                        getattr(self, f.name) is None or (f.is_list and not getattr(self, f.name))
                ):
                    # These are required and cannot be deleted
                    continue
                update_fields.append(f.name)
        res = UpdateFolder(account=self.account).get(folders=[(self, update_fields)])
        folder_id, changekey = res.id, res.changekey
        if self.id != folder_id:
            raise ValueError('ID mismatch')
        # Don't check changekey value. It may not change on no-op updates
        self.changekey = changekey
        self.root.update_folder(self)  # Update the folder in the cache
        return self

    def move(self, to_folder):
        res = MoveFolder(account=self.account).get(folders=[self], to_folder=to_folder)
        folder_id, changekey = res.id, res.changekey
        if self.id != folder_id:
            raise ValueError('ID mismatch')
        # Don't check changekey value. It may not change on no-op moves
        self.changekey = changekey
        self.parent_folder_id = ParentFolderId(id=to_folder.id, changekey=to_folder.changekey)
        self.root.update_folder(self)  # Update the folder in the cache

    def delete(self, delete_type=HARD_DELETE):
        if delete_type not in DELETE_TYPE_CHOICES:
            raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
        DeleteFolder(account=self.account).get(folders=[self], delete_type=delete_type)
        self.root.remove_folder(self)  # Remove the updated folder from the cache
        self._id = None

    def empty(self, delete_type=HARD_DELETE, delete_sub_folders=False):
        if delete_type not in DELETE_TYPE_CHOICES:
            raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
        EmptyFolder(account=self.account).get(
            folders=[self], delete_type=delete_type, delete_sub_folders=delete_sub_folders
        )
        if delete_sub_folders:
            # We don't know exactly what was deleted, so invalidate the entire folder cache to be safe
            self.root.clear_cache()

    def wipe(self, page_size=None, _seen=None, _level=0):
        # Recursively deletes all items in this folder, and all subfolders and their content. Attempts to protect
        # distinguished folders from being deleted. Use with caution!
        _seen = _seen or set()
        if self.id in _seen:
            raise RecursionError('We already tried to wipe %s' % self)
        if _level > 16:
            raise RecursionError('Max recursion level reached: %s' % _level)
        _seen.add(self.id)
        log.warning('Wiping %s', self)
        has_distinguished_subfolders = any(f.is_distinguished for f in self.children)
        try:
            if has_distinguished_subfolders:
                self.empty(delete_sub_folders=False)
            else:
                self.empty(delete_sub_folders=True)
        except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
            try:
                if has_distinguished_subfolders:
                    raise  # We already tried this
                self.empty(delete_sub_folders=False)
            except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
                log.warning('Not allowed to empty %s. Trying to delete items instead', self)
                try:
                    self.all().delete(**dict(page_size=page_size) if page_size else {})
                except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound):
                    log.warning('Not allowed to delete items in %s', self)
        _level += 1
        for f in self.children:
            f.wipe(page_size=page_size, _seen=_seen, _level=_level)
            # Remove non-distinguished children that are empty and have no subfolders
            if f.is_deletable and not f.children:
                log.warning('Deleting folder %s', f)
                try:
                    f.delete()
                except ErrorDeleteDistinguishedFolder:
                    log.warning('Tried to delete a distinguished folder (%s)', f)

    def test_access(self):
        """Does a simple FindItem to test (read) access to the folder. Maybe the account doesn't exist, maybe the
        service user doesn't have access to the calendar. This will throw the most common errors.
        """
        self.all().exists()
        return True

    @classmethod
    def _kwargs_from_elem(cls, elem, account):
        # Check for 'DisplayName' element before collecting kwargs because because that clears the elements
        has_name_elem = elem.find(cls.get_field_by_fieldname('name').response_tag()) is not None
        kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
        if has_name_elem and not kwargs['name']:
            # When we request the 'DisplayName' property, some folders may still be returned with an empty value.
            # Assign a default name to these folders.
            kwargs['name'] = cls.DISTINGUISHED_FOLDER_ID
        return kwargs

    def to_folder_id(self):
        if self.is_distinguished:
            # Don't add the changekey here. When modifying folder content, we usually don't care if others have changed
            # the folder content since we fetched the changekey.
            if self.account:
                return DistinguishedFolderId(
                    id=self.DISTINGUISHED_FOLDER_ID,
                    mailbox=Mailbox(email_address=self.account.primary_smtp_address)
                )
            return DistinguishedFolderId(id=self.DISTINGUISHED_FOLDER_ID)
        if self.id:
            return FolderId(id=self.id, changekey=self.changekey)
        raise ValueError('Must be a distinguished folder or have an ID')

    def to_xml(self, version):
        try:
            return self.to_folder_id().to_xml(version=version)
        except ValueError:
            return super().to_xml(version=version)

    def to_id_xml(self, version):
        # Folder(name='Foo') is a perfectly valid ID to e.g. create a folder
        return self.to_xml(version=version)

    @classmethod
    def resolve(cls, account, folder):
        # Resolve a single folder
        folders = list(FolderCollection(account=account, folders=[folder]).resolve())
        if not folders:
            raise ErrorFolderNotFound('Could not find folder %r' % folder)
        if len(folders) != 1:
            raise ValueError('Expected result length 1, but got %s' % folders)
        f = folders[0]
        if isinstance(f, Exception):
            raise f
        if f.__class__ != cls:
            raise ValueError("Expected folder %r to be a %s instance" % (f, cls))
        return f

    @require_id
    def refresh(self):
        fresh_folder = self.resolve(account=self.account, folder=self)
        if self.id != fresh_folder.id:
            raise ValueError('ID mismatch')
        # Apparently, the changekey may get updated
        for f in self.FIELDS:
            setattr(self, f.name, getattr(fresh_folder, f.name))
        return self

    @require_id
    def get_user_configuration(self, name, properties=ALL):
        return GetUserConfiguration(account=self.account).get(
            user_configuration_name=UserConfigurationNameMNS(name=name, folder=self),
            properties=properties,
        )

    @require_id
    def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
        user_configuration = UserConfiguration(
            user_configuration_name=UserConfigurationName(name=name, folder=self),
            dictionary=dictionary,
            xml_data=xml_data,
            binary_data=binary_data,
        )
        return CreateUserConfiguration(account=self.account).get(user_configuration=user_configuration)

    @require_id
    def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
        user_configuration = UserConfiguration(
            user_configuration_name=UserConfigurationName(name=name, folder=self),
            dictionary=dictionary,
            xml_data=xml_data,
            binary_data=binary_data,
        )
        return UpdateUserConfiguration(account=self.account).get(user_configuration=user_configuration)

    @require_id
    def delete_user_configuration(self, name):
        return DeleteUserConfiguration(account=self.account).get(
            user_configuration_name=UserConfigurationNameMNS(name=name, folder=self)
        )

    @require_id
    def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, 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
        """
        s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
            event_types=event_types, watermark=watermark, timeout=timeout,
        ))
        if len(s_ids) != 1:
            raise ValueError('Expected result length 1, but got %s' % s_ids)
        s_id = s_ids[0]
        if isinstance(s_id, Exception):
            raise s_id
        return s_id

    @require_id
    def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, 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
        """
        s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
            event_types=event_types, watermark=watermark, status_frequency=status_frequency, callback_url=callback_url,
        ))
        if len(s_ids) != 1:
            raise ValueError('Expected result length 1, but got %s' % s_ids)
        s_id = s_ids[0]
        if isinstance(s_id, Exception):
            raise s_id
        return s_id

    @require_id
    def subscribe_to_streaming(self, event_types=SubscribeToPush.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
        """
        s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(
            event_types=event_types,
        ))
        if len(s_ids) != 1:
            raise ValueError('Expected result length 1, but got %s' % s_ids)
        s_id = s_ids[0]
        if isinstance(s_id, Exception):
            raise s_id
        return s_id

    @require_id
    def pull_subscription(self, **kwargs):
        return PullSubscription(folder=self, **kwargs)

    @require_id
    def push_subscription(self, **kwargs):
        return PushSubscription(folder=self, **kwargs)

    @require_id
    def streaming_subscription(self, **kwargs):
        return StreamingSubscription(folder=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 folder instance, but it makes sense to keep the method along the other
        sync methods.
        """
        return Unsubscribe(account=self.account).get(subscription_id=subscription_id)

    def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
        """Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after
        this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.

        :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
        :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
        :param ignore: A list of Item IDs to ignore in the sync
        :param max_changes_returned: The max number of change
        :param sync_scope: Specify whether to return just items, or items and folder associated information. Possible
           values are specified in SyncFolderitems.SYNC_SCOPES
        :return: A generator of (change_type, item) tuples
        """
        if not sync_state:
            sync_state = self.item_sync_state
        try:
            yield from FolderCollection(account=self.account, folders=[self]).sync_items(
                sync_state=sync_state,
                only_fields=only_fields,
                ignore=ignore,
                max_changes_returned=max_changes_returned,
                sync_scope=sync_scope,
            )
        except SyncCompleted as e:
            # Set the new sync state on the folder instance
            self.item_sync_state = e.sync_state

    def sync_hierarchy(self, sync_state=None, only_fields=None):
        """Return all folder changes to a folder hierarchy, as a generator. If sync_state is specified, get all folder
        changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new
        sync state.

        :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
        :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
        :return:
        """
        if not sync_state:
            sync_state = self.folder_sync_state
        try:
            yield from FolderCollection(account=self.account, folders=[self]).sync_hierarchy(
                sync_state=sync_state,
                only_fields=only_fields,
            )
        except SyncCompleted as e:
            # Set the new sync state on the folder instance
            self.folder_sync_state = e.sync_state

    def get_events(self, subscription_id, watermark):
        """Get events since the given watermark. Non-blocking.

        :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|push]()
        :param watermark: Either the watermark from the subscription, or as returned in the last .get_events() call.
        :return: A Notification object containing a list of events

        This method doesn't need the current folder instance, but it makes sense to keep the method along the other
        sync methods.
        """
        svc = GetEvents(account=self.account)
        while True:
            notification = svc.get(subscription_id=subscription_id, watermark=watermark)
            yield notification
            if not notification.more_events:
                break

    def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None):
        """Get events since the subscription was created, in streaming mode. This method will block as many minutes
        as specified by 'connection_timeout'.

        :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming()
        :param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout
        is reached.
        :param max_notifications_returned: If specified, will exit after receiving this number of notifications
        :return: A generator of Notification objects, each containing a list of events

        This method doesn't need the current folder instance, but it makes sense to keep the method along the other
        sync methods.
        """
        # Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
        request_timeout = connection_timeout*60 + 60
        svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
        for i, notification in enumerate(
                svc.call(subscription_ids=[subscription_id], connection_timeout=connection_timeout),
                start=1
        ):
            yield notification
            if max_notifications_returned and i >= max_notifications_returned:
                svc.stop_streaming()
                break
        if svc.error_subscription_ids:
            raise ErrorInvalidSubscription('Invalid subscription IDs: %s' % svc.error_subscription_ids)

    def __floordiv__(self, other):
        """Support the some_folder // 'child_folder' // 'child_of_child_folder' navigation syntax.

        Works like as __truediv__ but does not touch the folder cache.

        This is useful if the folder hierarchy contains a huge number of folders and you don't want to fetch them all

        :param other:
        :return:
        """
        if other == '..':
            raise ValueError('Cannot get parent without a folder cache')

        if other == '.':
            return self

        # Assume an exact match on the folder name in a shallow search will only return at most one folder
        try:
            return SingleFolderQuerySet(account=self.account, folder=self).depth(SHALLOW_FOLDERS).get(name=other)
        except DoesNotExist:
            raise ErrorFolderNotFound("No subfolder with name '%s'" % other)

    def __truediv__(self, other):
        """Support the some_folder / 'child_folder' / 'child_of_child_folder' navigation syntax."""
        if other == '..':
            if not self.parent:
                raise ValueError('Already at top')
            return self.parent
        if other == '.':
            return self
        for c in self.children:
            if c.name == other:
                return c
        raise ErrorFolderNotFound("No subfolder with name '%s'" % other)

    def __repr__(self):
        return self.__class__.__name__ + \
               repr((self.root, self.name, self.total_count, self.unread_count, self.child_folder_count,
                     self.folder_class, self.id, self.changekey))

    def __str__(self):
        return '%s (%s)' % (self.__class__.__name__, self.name)

Ancestors

Subclasses

Class variables

var CONTAINER_CLASS
var DEFAULT_FOLDER_TRAVERSAL_DEPTH
var DEFAULT_ITEM_TRAVERSAL_DEPTH
var DISTINGUISHED_FOLDER_ID
var ELEMENT_NAME
var FIELDS
var ID_ELEMENT_CLS
var INSERT_AFTER_FIELD
var ITEM_MODEL_MAP
var LOCALIZED_NAMES
var NAMESPACE
var get_folder_allowed
var supported_from
var supported_item_models

Static methods

def allowed_item_fields(version)
Expand source code
@classmethod
def allowed_item_fields(cls, version):
    # Return non-ID fields of all item classes allowed in this folder type
    fields = set()
    for item_model in cls.supported_item_models:
        fields.update(
            set(item_model.supported_fields(version=version))
        )
    return fields
def folder_cls_from_container_class(container_class)

Return a reasonable folder class given a container class, e.g. 'IPF.Note'. Don't iterate WELLKNOWN_FOLDERS because many folder classes have the same CONTAINER_CLASS.

:param container_class: :return:

Expand source code
@staticmethod
def folder_cls_from_container_class(container_class):
    """Return a reasonable folder class given a container class, e.g. 'IPF.Note'. Don't iterate WELLKNOWN_FOLDERS
    because many folder classes have the same CONTAINER_CLASS.

    :param container_class:
    :return:
    """
    from .known_folders import Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, \
        RecipientCache, RSSFeeds
    for folder_cls in (
            Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, RecipientCache,
            RSSFeeds):
        if folder_cls.CONTAINER_CLASS == container_class:
            return folder_cls
    raise KeyError()
def get_item_field_by_fieldname(fieldname)
Expand source code
@classmethod
def get_item_field_by_fieldname(cls, fieldname):
    for item_model in cls.supported_item_models:
        try:
            return item_model.get_field_by_fieldname(fieldname)
        except InvalidField:
            pass
    raise InvalidField("%r is not a valid field name on %s" % (fieldname, cls.supported_item_models))
def item_model_from_tag(tag)
Expand source code
@classmethod
def item_model_from_tag(cls, tag):
    try:
        return cls.ITEM_MODEL_MAP[tag]
    except KeyError:
        raise ValueError('Item type %s was unexpected in a %s folder' % (tag, cls.__name__))
def localized_names(locale)
Expand source code
@classmethod
def localized_names(cls, locale):
    # Return localized names for a specific locale. If no locale-specific names exist, return the default names,
    # if any.
    return tuple(s.lower() for s in cls.LOCALIZED_NAMES.get(locale, cls.LOCALIZED_NAMES.get(None, [])))
def resolve(account, folder)
Expand source code
@classmethod
def resolve(cls, account, folder):
    # Resolve a single folder
    folders = list(FolderCollection(account=account, folders=[folder]).resolve())
    if not folders:
        raise ErrorFolderNotFound('Could not find folder %r' % folder)
    if len(folders) != 1:
        raise ValueError('Expected result length 1, but got %s' % folders)
    f = folders[0]
    if isinstance(f, Exception):
        raise f
    if f.__class__ != cls:
        raise ValueError("Expected folder %r to be a %s instance" % (f, cls))
    return f
def supports_version(version)
Expand source code
@classmethod
def supports_version(cls, version):
    # 'version' is a Version instance, for convenience by callers
    if not isinstance(version, Version):
        raise ValueError("'version' %r must be a Version instance" % version)
    if not cls.supported_from:
        return True
    return version.build >= cls.supported_from

Instance variables

var absolute
Expand source code
@property
def absolute(self):
    return ''.join('/%s' % p.name for p in self.parts)
var account
Expand source code
@property
@abc.abstractmethod
def account(self):
    pass
var child_folder_count
var children
Expand source code
@property
def children(self):
    # It's dangerous to return a generator here because we may then call methods on a child that result in the
    # cache being updated while it's iterated.
    return FolderCollection(account=self.account, folders=self.root.get_children(self))
var folder_class
var folder_sync_state

Return an attribute of instance, which is of type owner.

var has_distinguished_name
Expand source code
@property
def has_distinguished_name(self):
    return self.name and self.DISTINGUISHED_FOLDER_ID and self.name.lower() == self.DISTINGUISHED_FOLDER_ID.lower()
var is_deletable
Expand source code
@property
def is_deletable(self):
    return not self.is_distinguished
var is_distinguished

Return an attribute of instance, which is of type owner.

var item_sync_state

Return an attribute of instance, which is of type owner.

var name
var parent
Expand source code
@property
@abc.abstractmethod
def parent(self):
    pass
var parent_folder_id
var parts
Expand source code
@property
def parts(self):
    parts = [self]
    f = self.parent
    while f:
        parts.insert(0, f)
        f = f.parent
    return parts
var root
Expand source code
@property
@abc.abstractmethod
def root(self):
    pass
var total_count
var unread_count

Methods

def all(self)
Expand source code
def all(self):
    return FolderCollection(account=self.account, folders=[self]).all()
def bulk_create(self, items, *args, **kwargs)
Expand source code
def bulk_create(self, items, *args, **kwargs):
    return self.account.bulk_create(folder=self, items=items, *args, **kwargs)
def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)
    # Set a default folder class for new folders. A folder class cannot be changed after saving.
    if self.id is None and self.folder_class is None:
        self.folder_class = self.CONTAINER_CLASS
def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None)
Expand source code
@require_id
def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
    user_configuration = UserConfiguration(
        user_configuration_name=UserConfigurationName(name=name, folder=self),
        dictionary=dictionary,
        xml_data=xml_data,
        binary_data=binary_data,
    )
    return CreateUserConfiguration(account=self.account).get(user_configuration=user_configuration)
def delete(self, delete_type='HardDelete')
Expand source code
def delete(self, delete_type=HARD_DELETE):
    if delete_type not in DELETE_TYPE_CHOICES:
        raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
    DeleteFolder(account=self.account).get(folders=[self], delete_type=delete_type)
    self.root.remove_folder(self)  # Remove the updated folder from the cache
    self._id = None
def delete_user_configuration(self, name)
Expand source code
@require_id
def delete_user_configuration(self, name):
    return DeleteUserConfiguration(account=self.account).get(
        user_configuration_name=UserConfigurationNameMNS(name=name, folder=self)
    )
def empty(self, delete_type='HardDelete', delete_sub_folders=False)
Expand source code
def empty(self, delete_type=HARD_DELETE, delete_sub_folders=False):
    if delete_type not in DELETE_TYPE_CHOICES:
        raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
    EmptyFolder(account=self.account).get(
        folders=[self], delete_type=delete_type, delete_sub_folders=delete_sub_folders
    )
    if delete_sub_folders:
        # We don't know exactly what was deleted, so invalidate the entire folder cache to be safe
        self.root.clear_cache()
def exclude(self, *args, **kwargs)
Expand source code
def exclude(self, *args, **kwargs):
    return FolderCollection(account=self.account, folders=[self]).exclude(*args, **kwargs)
def filter(self, *args, **kwargs)
Expand source code
def filter(self, *args, **kwargs):
    return FolderCollection(account=self.account, folders=[self]).filter(*args, **kwargs)
def get(self, *args, **kwargs)
Expand source code
def get(self, *args, **kwargs):
    return FolderCollection(account=self.account, folders=[self]).get(*args, **kwargs)
def get_events(self, subscription_id, watermark)

Get events since the given watermark. Non-blocking.

:param subscription_id: A subscription ID as acquired by .subscribe_to_pull|push :param watermark: Either the watermark from the subscription, or as returned in the last .get_events() call. :return: A Notification object containing a list of events

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

Expand source code
def get_events(self, subscription_id, watermark):
    """Get events since the given watermark. Non-blocking.

    :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|push]()
    :param watermark: Either the watermark from the subscription, or as returned in the last .get_events() call.
    :return: A Notification object containing a list of events

    This method doesn't need the current folder instance, but it makes sense to keep the method along the other
    sync methods.
    """
    svc = GetEvents(account=self.account)
    while True:
        notification = svc.get(subscription_id=subscription_id, watermark=watermark)
        yield notification
        if not notification.more_events:
            break
def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None)

Get events since the subscription was created, in streaming mode. This method will block as many minutes as specified by 'connection_timeout'.

:param subscription_id: A subscription ID as acquired by .subscribe_to_streaming() :param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout is reached. :param max_notifications_returned: If specified, will exit after receiving this number of notifications :return: A generator of Notification objects, each containing a list of events

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

Expand source code
def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None):
    """Get events since the subscription was created, in streaming mode. This method will block as many minutes
    as specified by 'connection_timeout'.

    :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming()
    :param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout
    is reached.
    :param max_notifications_returned: If specified, will exit after receiving this number of notifications
    :return: A generator of Notification objects, each containing a list of events

    This method doesn't need the current folder instance, but it makes sense to keep the method along the other
    sync methods.
    """
    # Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
    request_timeout = connection_timeout*60 + 60
    svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
    for i, notification in enumerate(
            svc.call(subscription_ids=[subscription_id], connection_timeout=connection_timeout),
            start=1
    ):
        yield notification
        if max_notifications_returned and i >= max_notifications_returned:
            svc.stop_streaming()
            break
    if svc.error_subscription_ids:
        raise ErrorInvalidSubscription('Invalid subscription IDs: %s' % svc.error_subscription_ids)
def get_user_configuration(self, name, properties='All')
Expand source code
@require_id
def get_user_configuration(self, name, properties=ALL):
    return GetUserConfiguration(account=self.account).get(
        user_configuration_name=UserConfigurationNameMNS(name=name, folder=self),
        properties=properties,
    )
def glob(self, pattern)
Expand source code
def glob(self, pattern):
    return FolderCollection(account=self.account, folders=self._glob(pattern))
def move(self, to_folder)
Expand source code
def move(self, to_folder):
    res = MoveFolder(account=self.account).get(folders=[self], to_folder=to_folder)
    folder_id, changekey = res.id, res.changekey
    if self.id != folder_id:
        raise ValueError('ID mismatch')
    # Don't check changekey value. It may not change on no-op moves
    self.changekey = changekey
    self.parent_folder_id = ParentFolderId(id=to_folder.id, changekey=to_folder.changekey)
    self.root.update_folder(self)  # Update the folder in the cache
def none(self)
Expand source code
def none(self):
    return FolderCollection(account=self.account, folders=[self]).none()
def normalize_fields(self, fields)
Expand source code
def normalize_fields(self, fields):
    # Takes a list of fieldnames, Field or FieldPath objects pointing to item fields. Turns them into FieldPath
    # objects and adds internal timezone fields if necessary. Assume fields are already validated.
    fields = list(fields)
    has_start, has_end = False, False
    for i, field_path in enumerate(fields):
        # Allow both Field and FieldPath instances and string field paths as input
        if isinstance(field_path, str):
            field_path = FieldPath.from_string(field_path=field_path, folder=self)
            fields[i] = field_path
        elif isinstance(field_path, Field):
            field_path = FieldPath(field=field_path)
            fields[i] = field_path
        if not isinstance(field_path, FieldPath):
            raise ValueError("Field %r must be a string or FieldPath instance" % field_path)
        if field_path.field.name == 'start':
            has_start = True
        elif field_path.field.name == 'end':
            has_end = True

    # For CalendarItem items, we want to inject internal timezone fields. See also CalendarItem.clean()
    if CalendarItem in self.supported_item_models:
        meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
        if self.account.version.build < EXCHANGE_2010:
            if has_start or has_end:
                fields.append(FieldPath(field=meeting_tz_field))
        else:
            if has_start:
                fields.append(FieldPath(field=start_tz_field))
            if has_end:
                fields.append(FieldPath(field=end_tz_field))
    return fields
def people(self)
Expand source code
def people(self):
    # No point in using a FolderCollection because FindPeople only supports one folder
    return FolderCollection(account=self.account, folders=[self]).people()
def pull_subscription(self, **kwargs)
Expand source code
@require_id
def pull_subscription(self, **kwargs):
    return PullSubscription(folder=self, **kwargs)
def push_subscription(self, **kwargs)
Expand source code
@require_id
def push_subscription(self, **kwargs):
    return PushSubscription(folder=self, **kwargs)
def refresh(self)
Expand source code
@require_id
def refresh(self):
    fresh_folder = self.resolve(account=self.account, folder=self)
    if self.id != fresh_folder.id:
        raise ValueError('ID mismatch')
    # Apparently, the changekey may get updated
    for f in self.FIELDS:
        setattr(self, f.name, getattr(fresh_folder, f.name))
    return self
def save(self, update_fields=None)
Expand source code
def save(self, update_fields=None):
    if self.id is None:
        # New folder
        if update_fields:
            raise ValueError("'update_fields' is only valid for updates")
        res = CreateFolder(account=self.account).get(parent_folder=self.parent, folders=[self])
        self._id = self.ID_ELEMENT_CLS(res.id, res.changekey)
        self.root.add_folder(self)  # Add this folder to the cache
        return self

    # Update folder
    if not update_fields:
        # The fields to update was not specified explicitly. Update all fields where update is possible
        update_fields = []
        for f in self.supported_fields(version=self.account.version):
            if f.is_read_only:
                # These cannot be changed
                continue
            if (f.is_required or f.is_required_after_save) and (
                    getattr(self, f.name) is None or (f.is_list and not getattr(self, f.name))
            ):
                # These are required and cannot be deleted
                continue
            update_fields.append(f.name)
    res = UpdateFolder(account=self.account).get(folders=[(self, update_fields)])
    folder_id, changekey = res.id, res.changekey
    if self.id != folder_id:
        raise ValueError('ID mismatch')
    # Don't check changekey value. It may not change on no-op updates
    self.changekey = changekey
    self.root.update_folder(self)  # Update the folder in the cache
    return self
def streaming_subscription(self, **kwargs)
Expand source code
@require_id
def streaming_subscription(self, **kwargs):
    return StreamingSubscription(folder=self, **kwargs)
def subscribe_to_pull(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), 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

Expand source code
@require_id
def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, 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
    """
    s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
        event_types=event_types, watermark=watermark, timeout=timeout,
    ))
    if len(s_ids) != 1:
        raise ValueError('Expected result length 1, but got %s' % s_ids)
    s_id = s_ids[0]
    if isinstance(s_id, Exception):
        raise s_id
    return s_id
def subscribe_to_push(self, callback_url, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), 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

Expand source code
@require_id
def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, 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
    """
    s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
        event_types=event_types, watermark=watermark, status_frequency=status_frequency, callback_url=callback_url,
    ))
    if len(s_ids) != 1:
        raise ValueError('Expected result length 1, but got %s' % s_ids)
    s_id = s_ids[0]
    if isinstance(s_id, Exception):
        raise s_id
    return s_id
def subscribe_to_streaming(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'))

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

Expand source code
@require_id
def subscribe_to_streaming(self, event_types=SubscribeToPush.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
    """
    s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(
        event_types=event_types,
    ))
    if len(s_ids) != 1:
        raise ValueError('Expected result length 1, but got %s' % s_ids)
    s_id = s_ids[0]
    if isinstance(s_id, Exception):
        raise s_id
    return s_id
def sync_hierarchy(self, sync_state=None, only_fields=None)

Return all folder changes to a folder hierarchy, as a generator. If sync_state is specified, get all folder changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new sync state.

:param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service. :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields :return:

Expand source code
def sync_hierarchy(self, sync_state=None, only_fields=None):
    """Return all folder changes to a folder hierarchy, as a generator. If sync_state is specified, get all folder
    changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new
    sync state.

    :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
    :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
    :return:
    """
    if not sync_state:
        sync_state = self.folder_sync_state
    try:
        yield from FolderCollection(account=self.account, folders=[self]).sync_hierarchy(
            sync_state=sync_state,
            only_fields=only_fields,
        )
    except SyncCompleted as e:
        # Set the new sync state on the folder instance
        self.folder_sync_state = e.sync_state
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None)

Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.

:param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service. :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields :param ignore: A list of Item IDs to ignore in the sync :param max_changes_returned: The max number of change :param sync_scope: Specify whether to return just items, or items and folder associated information. Possible values are specified in SyncFolderitems.SYNC_SCOPES :return: A generator of (change_type, item) tuples

Expand source code
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
    """Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after
    this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.

    :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
    :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
    :param ignore: A list of Item IDs to ignore in the sync
    :param max_changes_returned: The max number of change
    :param sync_scope: Specify whether to return just items, or items and folder associated information. Possible
       values are specified in SyncFolderitems.SYNC_SCOPES
    :return: A generator of (change_type, item) tuples
    """
    if not sync_state:
        sync_state = self.item_sync_state
    try:
        yield from FolderCollection(account=self.account, folders=[self]).sync_items(
            sync_state=sync_state,
            only_fields=only_fields,
            ignore=ignore,
            max_changes_returned=max_changes_returned,
            sync_scope=sync_scope,
        )
    except SyncCompleted as e:
        # Set the new sync state on the folder instance
        self.item_sync_state = e.sync_state
def test_access(self)

Does a simple FindItem to test (read) access to the folder. Maybe the account doesn't exist, maybe the service user doesn't have access to the calendar. This will throw the most common errors.

Expand source code
def test_access(self):
    """Does a simple FindItem to test (read) access to the folder. Maybe the account doesn't exist, maybe the
    service user doesn't have access to the calendar. This will throw the most common errors.
    """
    self.all().exists()
    return True
def to_folder_id(self)
Expand source code
def to_folder_id(self):
    if self.is_distinguished:
        # Don't add the changekey here. When modifying folder content, we usually don't care if others have changed
        # the folder content since we fetched the changekey.
        if self.account:
            return DistinguishedFolderId(
                id=self.DISTINGUISHED_FOLDER_ID,
                mailbox=Mailbox(email_address=self.account.primary_smtp_address)
            )
        return DistinguishedFolderId(id=self.DISTINGUISHED_FOLDER_ID)
    if self.id:
        return FolderId(id=self.id, changekey=self.changekey)
    raise ValueError('Must be a distinguished folder or have an ID')
def to_id_xml(self, version)
Expand source code
def to_id_xml(self, version):
    # Folder(name='Foo') is a perfectly valid ID to e.g. create a folder
    return self.to_xml(version=version)
def to_xml(self, version)
Expand source code
def to_xml(self, version):
    try:
        return self.to_folder_id().to_xml(version=version)
    except ValueError:
        return super().to_xml(version=version)
def tree(self)

Return a string representation of the folder structure of this folder. Example:

root ├── inbox │ └── todos └── archive ├── Last Job ├── exchangelib issues └── Mom

Expand source code
def tree(self):
    """Return a string representation of the folder structure of this folder. Example:

    root
    ├── inbox
    │   └── todos
    └── archive
        ├── Last Job
        ├── exchangelib issues
        └── Mom
    """
    tree = '%s\n' % self.name
    children = list(self.children)
    for i, c in enumerate(sorted(children, key=attrgetter('name')), start=1):
        nodes = c.tree().split('\n')
        for j, node in enumerate(nodes, start=1):
            if i != len(children) and j == 1:
                # Not the last child, but the first node, which is the name of the child
                tree += '├── %s\n' % node
            elif i != len(children) and j > 1:
                # Not the last child, and not name of child
                tree += '│   %s\n' % node
            elif i == len(children) and j == 1:
                # Not the last child, but the first node, which is the name of the child
                tree += '└── %s\n' % node
            else:  # Last child, and not name of child
                tree += '    %s\n' % node
    return tree.strip()
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 folder instance, but it makes sense to keep the method along the other sync methods.

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 folder instance, but it makes sense to keep the method along the other
    sync methods.
    """
    return Unsubscribe(account=self.account).get(subscription_id=subscription_id)
def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None)
Expand source code
@require_id
def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
    user_configuration = UserConfiguration(
        user_configuration_name=UserConfigurationName(name=name, folder=self),
        dictionary=dictionary,
        xml_data=xml_data,
        binary_data=binary_data,
    )
    return UpdateUserConfiguration(account=self.account).get(user_configuration=user_configuration)
def validate_item_field(self, field, version)
Expand source code
def validate_item_field(self, field, version):
    # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
    # for the item types supported by this folder.

    # For each field, check if the field is valid for any of the item models supported by this folder
    for item_model in self.supported_item_models:
        try:
            item_model.validate_field(field=field, version=version)
            break
        except InvalidField:
            continue
    else:
        raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))
def walk(self)
Expand source code
def walk(self):
    return FolderCollection(account=self.account, folders=self._walk())
def wipe(self, page_size=None)
Expand source code
def wipe(self, page_size=None, _seen=None, _level=0):
    # Recursively deletes all items in this folder, and all subfolders and their content. Attempts to protect
    # distinguished folders from being deleted. Use with caution!
    _seen = _seen or set()
    if self.id in _seen:
        raise RecursionError('We already tried to wipe %s' % self)
    if _level > 16:
        raise RecursionError('Max recursion level reached: %s' % _level)
    _seen.add(self.id)
    log.warning('Wiping %s', self)
    has_distinguished_subfolders = any(f.is_distinguished for f in self.children)
    try:
        if has_distinguished_subfolders:
            self.empty(delete_sub_folders=False)
        else:
            self.empty(delete_sub_folders=True)
    except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
        try:
            if has_distinguished_subfolders:
                raise  # We already tried this
            self.empty(delete_sub_folders=False)
        except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
            log.warning('Not allowed to empty %s. Trying to delete items instead', self)
            try:
                self.all().delete(**dict(page_size=page_size) if page_size else {})
            except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound):
                log.warning('Not allowed to delete items in %s', self)
    _level += 1
    for f in self.children:
        f.wipe(page_size=page_size, _seen=_seen, _level=_level)
        # Remove non-distinguished children that are empty and have no subfolders
        if f.is_deletable and not f.children:
            log.warning('Deleting folder %s', f)
            try:
                f.delete()
            except ErrorDeleteDistinguishedFolder:
                log.warning('Tried to delete a distinguished folder (%s)', f)

Inherited members

class BaseSubscription (folder, **subscription_kwargs)
Expand source code
class BaseSubscription(metaclass=abc.ABCMeta):
    def __init__(self, folder, **subscription_kwargs):
        self.folder = folder
        self.subscription_kwargs = subscription_kwargs
        self.subscription_id = None

    def __enter__(self):
        pass

    def __exit__(self, *args, **kwargs):
        self.folder.unsubscribe(subscription_id=self.subscription_id)
        self.subscription_id = None

Subclasses

class Folder (**kwargs)
Expand source code
class Folder(BaseFolder):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/folder"""

    permission_set = PermissionSetField(field_uri='folder:PermissionSet', supported_from=EXCHANGE_2007_SP1)
    effective_rights = EffectiveRightsField(field_uri='folder:EffectiveRights', is_read_only=True,
                                            supported_from=EXCHANGE_2007_SP1)

    __slots__ = '_root',

    def __init__(self, **kwargs):
        self._root = kwargs.pop('root', None)  # This is a pointer to the root of the folder hierarchy
        parent = kwargs.pop('parent', None)
        if parent:
            if self.root:
                if parent.root != self.root:
                    raise ValueError("'parent.root' must match 'root'")
            else:
                self.root = parent.root
            if 'parent_folder_id' in kwargs and parent.id != kwargs['parent_folder_id']:
                raise ValueError("'parent_folder_id' must match 'parent' ID")
            kwargs['parent_folder_id'] = ParentFolderId(id=parent.id, changekey=parent.changekey)
        super().__init__(**kwargs)

    @property
    def account(self):
        if self.root is None:
            return None
        return self.root.account

    @property
    def root(self):
        return self._root

    @root.setter
    def root(self, value):
        self._root = value

    @classmethod
    def register(cls, *args, **kwargs):
        if cls is not Folder:
            raise TypeError('For folders, custom fields must be registered on the Folder class')
        return super().register(*args, **kwargs)

    @classmethod
    def deregister(cls, *args, **kwargs):
        if cls is not Folder:
            raise TypeError('For folders, custom fields must be registered on the Folder class')
        return super().deregister(*args, **kwargs)

    @classmethod
    def get_distinguished(cls, root):
        """Get the distinguished folder for this folder class.

        :param root:
        :return:
        """
        try:
            return cls.resolve(
                account=root.account,
                folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
            )
        except MISSING_FOLDER_ERRORS:
            raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID)

    @property
    def parent(self):
        if not self.parent_folder_id:
            return None
        if self.parent_folder_id.id == self.id:
            # Some folders have a parent that references itself. Avoid circular references here
            return None
        return self.root.get_folder(self.parent_folder_id)

    @parent.setter
    def parent(self, value):
        if value is None:
            self.parent_folder_id = None
        else:
            if not isinstance(value, BaseFolder):
                raise ValueError("'value' %r must be a Folder instance" % value)
            self.root = value.root
            self.parent_folder_id = ParentFolderId(id=value.id, changekey=value.changekey)

    def clean(self, version=None):
        from .roots import RootOfHierarchy
        super().clean(version=version)
        if self.root and not isinstance(self.root, RootOfHierarchy):
            raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root)

    @classmethod
    def from_xml_with_root(cls, elem, root):
        folder = cls.from_xml(elem=elem, account=root.account)
        folder_cls = cls
        if cls == Folder:
            # We were called on the generic Folder class. Try to find a more specific class to return objects as.
            #
            # The "FolderClass" element value is the only indication we have in the FindFolder response of which
            # folder class we should create the folder with. And many folders share the same 'FolderClass' value, e.g.
            # Inbox and DeletedItems. We want to distinguish between these because otherwise we can't locate the right
            # folders types for e.g. Account.inbox and Account.trash.
            #
            # We should be able to just use the name, but apparently default folder names can be renamed to a set of
            # localized names using a PowerShell command:
            # https://docs.microsoft.com/en-us/powershell/module/exchange/client-access/Set-MailboxRegionalConfiguration
            #
            # Instead, search for a folder class using the localized name. If none are found, fall back to getting the
            # folder class by the "FolderClass" value.
            #
            # The returned XML may contain neither folder class nor name. In that case, we default to the generic
            # Folder class.
            if folder.name:
                try:
                    # TODO: fld_class.LOCALIZED_NAMES is most definitely neither complete nor authoritative
                    folder_cls = root.folder_cls_from_folder_name(folder_name=folder.name,
                                                                  locale=root.account.locale)
                    log.debug('Folder class %s matches localized folder name %s', folder_cls, folder.name)
                except KeyError:
                    pass
            if folder.folder_class and folder_cls == Folder:
                try:
                    folder_cls = cls.folder_cls_from_container_class(container_class=folder.folder_class)
                    log.debug('Folder class %s matches container class %s (%s)', folder_cls, folder.folder_class,
                              folder.name)
                except KeyError:
                    pass
            if folder_cls == Folder:
                log.debug('Fallback to class Folder (folder_class %s, name %s)', folder.folder_class, folder.name)
        return folder_cls(root=root, **{f.name: getattr(folder, f.name) for f in folder.FIELDS})

Ancestors

Subclasses

Class variables

var FIELDS

Static methods

def from_xml_with_root(elem, root)
Expand source code
@classmethod
def from_xml_with_root(cls, elem, root):
    folder = cls.from_xml(elem=elem, account=root.account)
    folder_cls = cls
    if cls == Folder:
        # We were called on the generic Folder class. Try to find a more specific class to return objects as.
        #
        # The "FolderClass" element value is the only indication we have in the FindFolder response of which
        # folder class we should create the folder with. And many folders share the same 'FolderClass' value, e.g.
        # Inbox and DeletedItems. We want to distinguish between these because otherwise we can't locate the right
        # folders types for e.g. Account.inbox and Account.trash.
        #
        # We should be able to just use the name, but apparently default folder names can be renamed to a set of
        # localized names using a PowerShell command:
        # https://docs.microsoft.com/en-us/powershell/module/exchange/client-access/Set-MailboxRegionalConfiguration
        #
        # Instead, search for a folder class using the localized name. If none are found, fall back to getting the
        # folder class by the "FolderClass" value.
        #
        # The returned XML may contain neither folder class nor name. In that case, we default to the generic
        # Folder class.
        if folder.name:
            try:
                # TODO: fld_class.LOCALIZED_NAMES is most definitely neither complete nor authoritative
                folder_cls = root.folder_cls_from_folder_name(folder_name=folder.name,
                                                              locale=root.account.locale)
                log.debug('Folder class %s matches localized folder name %s', folder_cls, folder.name)
            except KeyError:
                pass
        if folder.folder_class and folder_cls == Folder:
            try:
                folder_cls = cls.folder_cls_from_container_class(container_class=folder.folder_class)
                log.debug('Folder class %s matches container class %s (%s)', folder_cls, folder.folder_class,
                          folder.name)
            except KeyError:
                pass
        if folder_cls == Folder:
            log.debug('Fallback to class Folder (folder_class %s, name %s)', folder.folder_class, folder.name)
    return folder_cls(root=root, **{f.name: getattr(folder, f.name) for f in folder.FIELDS})
def get_distinguished(root)

Get the distinguished folder for this folder class.

:param root: :return:

Expand source code
@classmethod
def get_distinguished(cls, root):
    """Get the distinguished folder for this folder class.

    :param root:
    :return:
    """
    try:
        return cls.resolve(
            account=root.account,
            folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
        )
    except MISSING_FOLDER_ERRORS:
        raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID)

Instance variables

var account
Expand source code
@property
def account(self):
    if self.root is None:
        return None
    return self.root.account
var effective_rights
var parent
Expand source code
@property
def parent(self):
    if not self.parent_folder_id:
        return None
    if self.parent_folder_id.id == self.id:
        # Some folders have a parent that references itself. Avoid circular references here
        return None
    return self.root.get_folder(self.parent_folder_id)
var permission_set
var root
Expand source code
@property
def root(self):
    return self._root

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    from .roots import RootOfHierarchy
    super().clean(version=version)
    if self.root and not isinstance(self.root, RootOfHierarchy):
        raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root)

Inherited members

class PullSubscription (folder, **subscription_kwargs)
Expand source code
class PullSubscription(BaseSubscription):
    def __enter__(self):
        self.subscription_id, watermark = self.folder.subscribe_to_pull(**self.subscription_kwargs)
        return self.subscription_id, watermark

Ancestors

class PushSubscription (folder, **subscription_kwargs)
Expand source code
class PushSubscription(BaseSubscription):
    def __enter__(self):
        self.subscription_id, watermark = self.folder.subscribe_to_push(**self.subscription_kwargs)
        return self.subscription_id, watermark

    def __exit__(self, *args, **kwargs):
        # Cannot unsubscribe to push subscriptions
        pass

Ancestors

class StreamingSubscription (folder, **subscription_kwargs)
Expand source code
class StreamingSubscription(BaseSubscription):
    def __enter__(self):
        self.subscription_id = self.folder.subscribe_to_streaming(**self.subscription_kwargs)
        return self.subscription_id

Ancestors

exchangelib-4.6.1/docs/exchangelib/folders/collections.html000066400000000000000000002557641414601472700240670ustar00rootroot00000000000000 exchangelib.folders.collections API documentation

Module exchangelib.folders.collections

Expand source code
import logging

from cached_property import threaded_cached_property

from .queryset import FOLDER_TRAVERSAL_CHOICES
from ..fields import FieldPath, InvalidField
from ..items import Persona, ITEM_TRAVERSAL_CHOICES, SHAPE_CHOICES, ID_ONLY
from ..properties import CalendarView
from ..queryset import QuerySet, SearchableMixIn, Q
from ..restriction import Restriction
from ..services import FindFolder, GetFolder, FindItem, FindPeople, SyncFolderItems, SyncFolderHierarchy, \
    SubscribeToPull, SubscribeToPush, SubscribeToStreaming
from ..util import require_account

log = logging.getLogger(__name__)


class SyncCompleted(Exception):
    """This is a really ugly way of returning the sync state."""

    def __init__(self, sync_state):
        super().__init__(sync_state)
        self.sync_state = sync_state


class FolderCollection(SearchableMixIn):
    """A class that implements an API for searching folders."""

    # These fields are required in a FindFolder or GetFolder call to properly identify folder types
    REQUIRED_FOLDER_FIELDS = ('name', 'folder_class')

    def __init__(self, account, folders):
        """Implement a search API on a collection of folders.

        :param account: An Account object
        :param folders: An iterable of folders, e.g. Folder.walk(), Folder.glob(), or [a.calendar, a.inbox]
        """
        self.account = account
        self._folders = folders

    @threaded_cached_property
    def folders(self):
        # Resolve the list of folders, in case it's a generator
        return list(self._folders)

    def __len__(self):
        return len(self.folders)

    def __iter__(self):
        yield from self.folders

    def get(self, *args, **kwargs):
        return QuerySet(self).get(*args, **kwargs)

    def all(self):
        return QuerySet(self).all()

    def none(self):
        return QuerySet(self).none()

    def filter(self, *args, **kwargs):
        """Find items in the folder(s).

        Non-keyword args may be a list of Q instances.

        Optional extra keyword arguments follow a Django-like QuerySet filter syntax (see
           https://docs.djangoproject.com/en/1.10/ref/models/querysets/#field-lookups).

        We don't support '__year' and other date-related lookups. We also don't support '__endswith' or '__iendswith'.

        We support the additional '__not' lookup in place of Django's exclude() for simple cases. For more complicated
        cases you need to create a Q object and use ~Q().

        Examples:

            my_account.inbox.filter(datetime_received__gt=EWSDateTime(2016, 1, 1))
            my_account.calendar.filter(start__range=(EWSDateTime(2016, 1, 1), EWSDateTime(2017, 1, 1)))
            my_account.tasks.filter(subject='Hi mom')
            my_account.tasks.filter(subject__not='Hi mom')
            my_account.tasks.filter(subject__contains='Foo')
            my_account.tasks.filter(subject__icontains='foo')

        'endswith' and 'iendswith' could be emulated by searching with 'contains' or 'icontains' and then
        post-processing items. Fetch the field in question with additional_fields and remove items where the search
        string is not a postfix.
        """
        return QuerySet(self).filter(*args, **kwargs)

    def exclude(self, *args, **kwargs):
        return QuerySet(self).exclude(*args, **kwargs)

    def people(self):
        return QuerySet(self).people()

    def view(self, start, end, max_items=None, *args, **kwargs):
        """Implement the CalendarView option to FindItem. The difference between 'filter' and 'view' is that 'filter'
        only returns the master CalendarItem for recurring items, while 'view' unfolds recurring items and returns all
        CalendarItem occurrences as one would normally expect when presenting a calendar.

        Supports the same semantics as filter, except for 'start' and 'end' keyword attributes which are both required
        and behave differently than filter. Here, they denote the start and end of the timespan of the view. All items
        the overlap the timespan are returned (items that end exactly on 'start' are also returned, for some reason).

        EWS does not allow combining CalendarView with search restrictions (filter and exclude).

        'max_items' defines the maximum number of items returned in this view. Optional.

        :param start:
        :param end:
        :param max_items:  (Default value = None)
        :return:
        """
        qs = QuerySet(self).filter(*args, **kwargs)
        qs.calendar_view = CalendarView(start=start, end=end, max_items=max_items)
        return qs

    def allowed_item_fields(self):
        # Return non-ID fields of all item classes allowed in this folder type
        fields = set()
        for item_model in self.supported_item_models:
            fields.update(set(item_model.supported_fields(version=self.account.version)))
        return fields

    @property
    def supported_item_models(self):
        return tuple(item_model for folder in self.folders for item_model in folder.supported_item_models)

    def validate_item_field(self, field, version):
        # For each field, check if the field is valid for any of the item models supported by this folder
        for item_model in self.supported_item_models:
            try:
                item_model.validate_field(field=field, version=version)
                break
            except InvalidField:
                continue
        else:
            raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))

    def find_items(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
                   calendar_view=None, page_size=None, max_items=None, offset=0):
        """Private method to call the FindItem service.

        :param q: a Q instance containing any restrictions
        :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is
          non-null, we always return Item objects. (Default value = ID_ONLY)
        :param depth: controls the whether to return soft-deleted items or not. (Default value = None)
        :param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware
          that complex fields can only be fetched with fetch() (i.e. the GetItem service).
        :param order_fields: the SortOrder fields, if any (Default value = None)
        :param calendar_view: a CalendarView instance, if any (Default value = None)
        :param page_size: the requested number of items per page (Default value = None)
        :param max_items: the max number of items to return (Default value = None)
        :param offset: the offset relative to the first item in the item collection (Default value = 0)

        :return: a generator for the returned item IDs or items
        """
        if not self.folders:
            log.debug('Folder list is empty')
            return
        if q.is_never():
            log.debug('Query will never return results')
            return
        if shape not in SHAPE_CHOICES:
            raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
        if depth is None:
            depth = self._get_default_item_traversal_depth()
        if depth not in ITEM_TRAVERSAL_CHOICES:
            raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
        if additional_fields:
            for f in additional_fields:
                self.validate_item_field(field=f, version=self.account.version)
                if f.field.is_complex:
                    raise ValueError("find_items() does not support field '%s'. Use fetch() instead" % f.field.name)
        if calendar_view is not None and not isinstance(calendar_view, CalendarView):
            raise ValueError("'calendar_view' %s must be a CalendarView instance" % calendar_view)

        # Build up any restrictions
        if q.is_empty():
            restriction = None
            query_string = None
        elif q.query_string:
            restriction = None
            query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
        else:
            restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
            query_string = None
        log.debug(
            'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
            self.account,
            self.folders,
            shape,
            depth,
            additional_fields,
            restriction.q if restriction else None,
        )
        yield from FindItem(account=self.account, chunk_size=page_size).call(
            folders=self.folders,
            additional_fields=additional_fields,
            restriction=restriction,
            order_fields=order_fields,
            shape=shape,
            query_string=query_string,
            depth=depth,
            calendar_view=calendar_view,
            max_items=calendar_view.max_items if calendar_view else max_items,
            offset=offset,
        )

    def _get_single_folder(self):
        if len(self.folders) > 1:
            raise ValueError('Syncing folder hierarchy can only be done on a single folder')
        if not self.folders:
            log.debug('Folder list is empty')
            return None
        return self.folders[0]

    def find_people(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
                    page_size=None, max_items=None, offset=0):
        """Private method to call the FindPeople service.

        :param q: a Q instance containing any restrictions
        :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is
          non-null, we always return Persona objects. (Default value = ID_ONLY)
        :param depth: controls the whether to return soft-deleted items or not. (Default value = None)
        :param additional_fields: the extra properties we want on the return objects. Default is no properties.
        :param order_fields: the SortOrder fields, if any (Default value = None)
        :param page_size: the requested number of items per page (Default value = None)
        :param max_items: the max number of items to return (Default value = None)
        :param offset: the offset relative to the first item in the item collection (Default value = 0)

        :return: a generator for the returned personas
        """
        folder = self._get_single_folder()
        if not folder:
            return
        if q.is_never():
            log.debug('Query will never return results')
            return
        if shape not in SHAPE_CHOICES:
            raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
        if depth is None:
            depth = self._get_default_item_traversal_depth()
        if depth not in ITEM_TRAVERSAL_CHOICES:
            raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
        if additional_fields:
            for f in additional_fields:
                Persona.validate_field(field=f, version=self.account.version)
                if f.field.is_complex:
                    raise ValueError("find_people() does not support field '%s'" % f.field.name)

        # Build up any restrictions
        if q.is_empty():
            restriction = None
            query_string = None
        elif q.query_string:
            restriction = None
            query_string = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
        else:
            restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
            query_string = None
        yield from FindPeople(account=self.account, chunk_size=page_size).call(
                folder=folder,
                additional_fields=additional_fields,
                restriction=restriction,
                order_fields=order_fields,
                shape=shape,
                query_string=query_string,
                depth=depth,
                max_items=max_items,
                offset=offset,
        )

    def get_folder_fields(self, target_cls, is_complex=None):
        return {
            FieldPath(field=f) for f in target_cls.supported_fields(version=self.account.version)
            if is_complex is None or f.is_complex is is_complex
        }

    def _get_target_cls(self):
        # We may have root folders that don't support the same set of fields as normal folders. If there is a mix of
        # both folder types in self.folders, raise an error so we don't risk losing some fields in the query.
        from .base import Folder
        from .roots import RootOfHierarchy
        has_roots = False
        has_non_roots = False
        for f in self.folders:
            if isinstance(f, RootOfHierarchy):
                if has_non_roots:
                    raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders))
                has_roots = True
            else:
                if has_roots:
                    raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders))
                has_non_roots = True
        return RootOfHierarchy if has_roots else Folder

    def _get_default_traversal_depth(self, traversal_attr):
        unique_depths = {getattr(f, traversal_attr) for f in self.folders}
        if len(unique_depths) == 1:
            return unique_depths.pop()
        raise ValueError(
            'Folders in this collection do not have a common %s value. You need to define an explicit traversal depth'
            'with QuerySet.depth() (values: %s)' % (traversal_attr, unique_depths)
        )

    def _get_default_item_traversal_depth(self):
        # When searching folders, some folders require 'Shallow' and others 'Associated' traversal depth.
        return self._get_default_traversal_depth('DEFAULT_ITEM_TRAVERSAL_DEPTH')

    def _get_default_folder_traversal_depth(self):
        # When searching folders, some folders require 'Shallow' and others 'Deep' traversal depth.
        return self._get_default_traversal_depth('DEFAULT_FOLDER_TRAVERSAL_DEPTH')

    def resolve(self):
        # Looks up the folders or folder IDs in the collection and returns full Folder instances with all fields set.
        from .base import BaseFolder
        resolveable_folders = []
        for f in self.folders:
            if isinstance(f, BaseFolder) and not f.get_folder_allowed:
                log.debug('GetFolder not allowed on folder %s. Non-complex fields must be fetched with FindFolder', f)
                yield f
            else:
                resolveable_folders.append(f)
        # Fetch all properties for the remaining folders of folder IDs
        additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=None)
        yield from self.__class__(account=self.account, folders=resolveable_folders).get_folders(
                additional_fields=additional_fields
        )

    @require_account
    def find_folders(self, q=None, shape=ID_ONLY, depth=None, additional_fields=None, page_size=None, max_items=None,
                     offset=0):
        # 'depth' controls whether to return direct children or recurse into sub-folders
        from .base import BaseFolder, Folder
        if q is None:
            q = Q()
        if not self.folders:
            log.debug('Folder list is empty')
            return
        if q.is_never():
            log.debug('Query will never return results')
            return
        if q.is_empty():
            restriction = None
        else:
            restriction = Restriction(q, folders=self.folders, applies_to=Restriction.FOLDERS)
        if shape not in SHAPE_CHOICES:
            raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
        if depth is None:
            depth = self._get_default_folder_traversal_depth()
        if depth not in FOLDER_TRAVERSAL_CHOICES:
            raise ValueError("'depth' %s must be one of %s" % (depth, FOLDER_TRAVERSAL_CHOICES))
        if additional_fields is None:
            # Default to all non-complex properties. Subfolders will always be of class Folder
            additional_fields = self.get_folder_fields(target_cls=Folder, is_complex=False)
        else:
            for f in additional_fields:
                if f.field.is_complex:
                    raise ValueError("find_folders() does not support field '%s'. Use get_folders()." % f.field.name)

        # Add required fields
        additional_fields.update(
            (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
        )

        yield from FindFolder(account=self.account, chunk_size=page_size).call(
                folders=self.folders,
                additional_fields=additional_fields,
                restriction=restriction,
                shape=shape,
                depth=depth,
                max_items=max_items,
                offset=offset,
        )

    def get_folders(self, additional_fields=None):
        # Expand folders with their full set of properties
        from .base import BaseFolder
        if not self.folders:
            log.debug('Folder list is empty')
            return
        if additional_fields is None:
            # Default to all complex properties
            additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=True)

        # Add required fields
        additional_fields.update(
            (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
        )

        yield from GetFolder(account=self.account).call(
                folders=self.folders,
                additional_fields=additional_fields,
                shape=ID_ONLY,
        )

    def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
        if not self.folders:
            log.debug('Folder list is empty')
            return
        yield from SubscribeToPull(account=self.account).call(
            folders=self.folders, event_types=event_types, watermark=watermark, timeout=timeout,
        )

    def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
                          status_frequency=1):
        if not self.folders:
            log.debug('Folder list is empty')
            return
        yield from SubscribeToPush(account=self.account).call(
            folders=self.folders, event_types=event_types, watermark=watermark, status_frequency=status_frequency,
            url=callback_url,
        )

    def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
        if not self.folders:
            log.debug('Folder list is empty')
            return
        yield from SubscribeToStreaming(account=self.account).call(folders=self.folders, event_types=event_types)

    def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
        folder = self._get_single_folder()
        if not folder:
            return
        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 folder.allowed_item_fields(version=self.account.version)}
        else:
            for field in only_fields:
                folder.validate_item_field(field=field, version=self.account.version)
            # Remove ItemId and ChangeKey. We get them unconditionally
            additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}

        svc = SyncFolderItems(account=self.account)
        while True:
            yield from svc.call(
                folder=folder,
                shape=ID_ONLY,
                additional_fields=additional_fields,
                sync_state=sync_state,
                ignore=ignore,
                max_changes_returned=max_changes_returned,
                sync_scope=sync_scope,
            )
            if svc.sync_state == sync_state:
                # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here
                break
            sync_state = svc.sync_state  # Set the new sync state in the next call
            if svc.includes_last_item_in_range:  # Try again if there are more items
                break
        raise SyncCompleted(sync_state=svc.sync_state)

    def sync_hierarchy(self, sync_state=None, only_fields=None):
        folder = self._get_single_folder()
        if not folder:
            return
        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 folder.supported_fields(version=self.account.version)}
        else:
            for f in only_fields:
                folder.validate_field(field=f, version=self.account.version)
            # Remove ItemId and ChangeKey. We get them unconditionally
            additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}

        # Add required fields
        additional_fields.update(
            (FieldPath(field=folder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
        )

        svc = SyncFolderHierarchy(account=self.account)
        while True:
            yield from svc.call(
                folder=folder,
                shape=ID_ONLY,
                additional_fields=additional_fields,
                sync_state=sync_state,
            )
            if svc.sync_state == sync_state:
                # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here
                break
            sync_state = svc.sync_state  # Set the new sync state in the next call
            if svc.includes_last_item_in_range:  # Try again if there are more items
                break
        raise SyncCompleted(sync_state=svc.sync_state)

Classes

class FolderCollection (account, folders)

A class that implements an API for searching folders.

Implement a search API on a collection of folders.

:param account: An Account object :param folders: An iterable of folders, e.g. Folder.walk(), Folder.glob(), or [a.calendar, a.inbox]

Expand source code
class FolderCollection(SearchableMixIn):
    """A class that implements an API for searching folders."""

    # These fields are required in a FindFolder or GetFolder call to properly identify folder types
    REQUIRED_FOLDER_FIELDS = ('name', 'folder_class')

    def __init__(self, account, folders):
        """Implement a search API on a collection of folders.

        :param account: An Account object
        :param folders: An iterable of folders, e.g. Folder.walk(), Folder.glob(), or [a.calendar, a.inbox]
        """
        self.account = account
        self._folders = folders

    @threaded_cached_property
    def folders(self):
        # Resolve the list of folders, in case it's a generator
        return list(self._folders)

    def __len__(self):
        return len(self.folders)

    def __iter__(self):
        yield from self.folders

    def get(self, *args, **kwargs):
        return QuerySet(self).get(*args, **kwargs)

    def all(self):
        return QuerySet(self).all()

    def none(self):
        return QuerySet(self).none()

    def filter(self, *args, **kwargs):
        """Find items in the folder(s).

        Non-keyword args may be a list of Q instances.

        Optional extra keyword arguments follow a Django-like QuerySet filter syntax (see
           https://docs.djangoproject.com/en/1.10/ref/models/querysets/#field-lookups).

        We don't support '__year' and other date-related lookups. We also don't support '__endswith' or '__iendswith'.

        We support the additional '__not' lookup in place of Django's exclude() for simple cases. For more complicated
        cases you need to create a Q object and use ~Q().

        Examples:

            my_account.inbox.filter(datetime_received__gt=EWSDateTime(2016, 1, 1))
            my_account.calendar.filter(start__range=(EWSDateTime(2016, 1, 1), EWSDateTime(2017, 1, 1)))
            my_account.tasks.filter(subject='Hi mom')
            my_account.tasks.filter(subject__not='Hi mom')
            my_account.tasks.filter(subject__contains='Foo')
            my_account.tasks.filter(subject__icontains='foo')

        'endswith' and 'iendswith' could be emulated by searching with 'contains' or 'icontains' and then
        post-processing items. Fetch the field in question with additional_fields and remove items where the search
        string is not a postfix.
        """
        return QuerySet(self).filter(*args, **kwargs)

    def exclude(self, *args, **kwargs):
        return QuerySet(self).exclude(*args, **kwargs)

    def people(self):
        return QuerySet(self).people()

    def view(self, start, end, max_items=None, *args, **kwargs):
        """Implement the CalendarView option to FindItem. The difference between 'filter' and 'view' is that 'filter'
        only returns the master CalendarItem for recurring items, while 'view' unfolds recurring items and returns all
        CalendarItem occurrences as one would normally expect when presenting a calendar.

        Supports the same semantics as filter, except for 'start' and 'end' keyword attributes which are both required
        and behave differently than filter. Here, they denote the start and end of the timespan of the view. All items
        the overlap the timespan are returned (items that end exactly on 'start' are also returned, for some reason).

        EWS does not allow combining CalendarView with search restrictions (filter and exclude).

        'max_items' defines the maximum number of items returned in this view. Optional.

        :param start:
        :param end:
        :param max_items:  (Default value = None)
        :return:
        """
        qs = QuerySet(self).filter(*args, **kwargs)
        qs.calendar_view = CalendarView(start=start, end=end, max_items=max_items)
        return qs

    def allowed_item_fields(self):
        # Return non-ID fields of all item classes allowed in this folder type
        fields = set()
        for item_model in self.supported_item_models:
            fields.update(set(item_model.supported_fields(version=self.account.version)))
        return fields

    @property
    def supported_item_models(self):
        return tuple(item_model for folder in self.folders for item_model in folder.supported_item_models)

    def validate_item_field(self, field, version):
        # For each field, check if the field is valid for any of the item models supported by this folder
        for item_model in self.supported_item_models:
            try:
                item_model.validate_field(field=field, version=version)
                break
            except InvalidField:
                continue
        else:
            raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))

    def find_items(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
                   calendar_view=None, page_size=None, max_items=None, offset=0):
        """Private method to call the FindItem service.

        :param q: a Q instance containing any restrictions
        :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is
          non-null, we always return Item objects. (Default value = ID_ONLY)
        :param depth: controls the whether to return soft-deleted items or not. (Default value = None)
        :param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware
          that complex fields can only be fetched with fetch() (i.e. the GetItem service).
        :param order_fields: the SortOrder fields, if any (Default value = None)
        :param calendar_view: a CalendarView instance, if any (Default value = None)
        :param page_size: the requested number of items per page (Default value = None)
        :param max_items: the max number of items to return (Default value = None)
        :param offset: the offset relative to the first item in the item collection (Default value = 0)

        :return: a generator for the returned item IDs or items
        """
        if not self.folders:
            log.debug('Folder list is empty')
            return
        if q.is_never():
            log.debug('Query will never return results')
            return
        if shape not in SHAPE_CHOICES:
            raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
        if depth is None:
            depth = self._get_default_item_traversal_depth()
        if depth not in ITEM_TRAVERSAL_CHOICES:
            raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
        if additional_fields:
            for f in additional_fields:
                self.validate_item_field(field=f, version=self.account.version)
                if f.field.is_complex:
                    raise ValueError("find_items() does not support field '%s'. Use fetch() instead" % f.field.name)
        if calendar_view is not None and not isinstance(calendar_view, CalendarView):
            raise ValueError("'calendar_view' %s must be a CalendarView instance" % calendar_view)

        # Build up any restrictions
        if q.is_empty():
            restriction = None
            query_string = None
        elif q.query_string:
            restriction = None
            query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
        else:
            restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
            query_string = None
        log.debug(
            'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
            self.account,
            self.folders,
            shape,
            depth,
            additional_fields,
            restriction.q if restriction else None,
        )
        yield from FindItem(account=self.account, chunk_size=page_size).call(
            folders=self.folders,
            additional_fields=additional_fields,
            restriction=restriction,
            order_fields=order_fields,
            shape=shape,
            query_string=query_string,
            depth=depth,
            calendar_view=calendar_view,
            max_items=calendar_view.max_items if calendar_view else max_items,
            offset=offset,
        )

    def _get_single_folder(self):
        if len(self.folders) > 1:
            raise ValueError('Syncing folder hierarchy can only be done on a single folder')
        if not self.folders:
            log.debug('Folder list is empty')
            return None
        return self.folders[0]

    def find_people(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
                    page_size=None, max_items=None, offset=0):
        """Private method to call the FindPeople service.

        :param q: a Q instance containing any restrictions
        :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is
          non-null, we always return Persona objects. (Default value = ID_ONLY)
        :param depth: controls the whether to return soft-deleted items or not. (Default value = None)
        :param additional_fields: the extra properties we want on the return objects. Default is no properties.
        :param order_fields: the SortOrder fields, if any (Default value = None)
        :param page_size: the requested number of items per page (Default value = None)
        :param max_items: the max number of items to return (Default value = None)
        :param offset: the offset relative to the first item in the item collection (Default value = 0)

        :return: a generator for the returned personas
        """
        folder = self._get_single_folder()
        if not folder:
            return
        if q.is_never():
            log.debug('Query will never return results')
            return
        if shape not in SHAPE_CHOICES:
            raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
        if depth is None:
            depth = self._get_default_item_traversal_depth()
        if depth not in ITEM_TRAVERSAL_CHOICES:
            raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
        if additional_fields:
            for f in additional_fields:
                Persona.validate_field(field=f, version=self.account.version)
                if f.field.is_complex:
                    raise ValueError("find_people() does not support field '%s'" % f.field.name)

        # Build up any restrictions
        if q.is_empty():
            restriction = None
            query_string = None
        elif q.query_string:
            restriction = None
            query_string = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
        else:
            restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
            query_string = None
        yield from FindPeople(account=self.account, chunk_size=page_size).call(
                folder=folder,
                additional_fields=additional_fields,
                restriction=restriction,
                order_fields=order_fields,
                shape=shape,
                query_string=query_string,
                depth=depth,
                max_items=max_items,
                offset=offset,
        )

    def get_folder_fields(self, target_cls, is_complex=None):
        return {
            FieldPath(field=f) for f in target_cls.supported_fields(version=self.account.version)
            if is_complex is None or f.is_complex is is_complex
        }

    def _get_target_cls(self):
        # We may have root folders that don't support the same set of fields as normal folders. If there is a mix of
        # both folder types in self.folders, raise an error so we don't risk losing some fields in the query.
        from .base import Folder
        from .roots import RootOfHierarchy
        has_roots = False
        has_non_roots = False
        for f in self.folders:
            if isinstance(f, RootOfHierarchy):
                if has_non_roots:
                    raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders))
                has_roots = True
            else:
                if has_roots:
                    raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders))
                has_non_roots = True
        return RootOfHierarchy if has_roots else Folder

    def _get_default_traversal_depth(self, traversal_attr):
        unique_depths = {getattr(f, traversal_attr) for f in self.folders}
        if len(unique_depths) == 1:
            return unique_depths.pop()
        raise ValueError(
            'Folders in this collection do not have a common %s value. You need to define an explicit traversal depth'
            'with QuerySet.depth() (values: %s)' % (traversal_attr, unique_depths)
        )

    def _get_default_item_traversal_depth(self):
        # When searching folders, some folders require 'Shallow' and others 'Associated' traversal depth.
        return self._get_default_traversal_depth('DEFAULT_ITEM_TRAVERSAL_DEPTH')

    def _get_default_folder_traversal_depth(self):
        # When searching folders, some folders require 'Shallow' and others 'Deep' traversal depth.
        return self._get_default_traversal_depth('DEFAULT_FOLDER_TRAVERSAL_DEPTH')

    def resolve(self):
        # Looks up the folders or folder IDs in the collection and returns full Folder instances with all fields set.
        from .base import BaseFolder
        resolveable_folders = []
        for f in self.folders:
            if isinstance(f, BaseFolder) and not f.get_folder_allowed:
                log.debug('GetFolder not allowed on folder %s. Non-complex fields must be fetched with FindFolder', f)
                yield f
            else:
                resolveable_folders.append(f)
        # Fetch all properties for the remaining folders of folder IDs
        additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=None)
        yield from self.__class__(account=self.account, folders=resolveable_folders).get_folders(
                additional_fields=additional_fields
        )

    @require_account
    def find_folders(self, q=None, shape=ID_ONLY, depth=None, additional_fields=None, page_size=None, max_items=None,
                     offset=0):
        # 'depth' controls whether to return direct children or recurse into sub-folders
        from .base import BaseFolder, Folder
        if q is None:
            q = Q()
        if not self.folders:
            log.debug('Folder list is empty')
            return
        if q.is_never():
            log.debug('Query will never return results')
            return
        if q.is_empty():
            restriction = None
        else:
            restriction = Restriction(q, folders=self.folders, applies_to=Restriction.FOLDERS)
        if shape not in SHAPE_CHOICES:
            raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
        if depth is None:
            depth = self._get_default_folder_traversal_depth()
        if depth not in FOLDER_TRAVERSAL_CHOICES:
            raise ValueError("'depth' %s must be one of %s" % (depth, FOLDER_TRAVERSAL_CHOICES))
        if additional_fields is None:
            # Default to all non-complex properties. Subfolders will always be of class Folder
            additional_fields = self.get_folder_fields(target_cls=Folder, is_complex=False)
        else:
            for f in additional_fields:
                if f.field.is_complex:
                    raise ValueError("find_folders() does not support field '%s'. Use get_folders()." % f.field.name)

        # Add required fields
        additional_fields.update(
            (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
        )

        yield from FindFolder(account=self.account, chunk_size=page_size).call(
                folders=self.folders,
                additional_fields=additional_fields,
                restriction=restriction,
                shape=shape,
                depth=depth,
                max_items=max_items,
                offset=offset,
        )

    def get_folders(self, additional_fields=None):
        # Expand folders with their full set of properties
        from .base import BaseFolder
        if not self.folders:
            log.debug('Folder list is empty')
            return
        if additional_fields is None:
            # Default to all complex properties
            additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=True)

        # Add required fields
        additional_fields.update(
            (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
        )

        yield from GetFolder(account=self.account).call(
                folders=self.folders,
                additional_fields=additional_fields,
                shape=ID_ONLY,
        )

    def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
        if not self.folders:
            log.debug('Folder list is empty')
            return
        yield from SubscribeToPull(account=self.account).call(
            folders=self.folders, event_types=event_types, watermark=watermark, timeout=timeout,
        )

    def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
                          status_frequency=1):
        if not self.folders:
            log.debug('Folder list is empty')
            return
        yield from SubscribeToPush(account=self.account).call(
            folders=self.folders, event_types=event_types, watermark=watermark, status_frequency=status_frequency,
            url=callback_url,
        )

    def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
        if not self.folders:
            log.debug('Folder list is empty')
            return
        yield from SubscribeToStreaming(account=self.account).call(folders=self.folders, event_types=event_types)

    def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
        folder = self._get_single_folder()
        if not folder:
            return
        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 folder.allowed_item_fields(version=self.account.version)}
        else:
            for field in only_fields:
                folder.validate_item_field(field=field, version=self.account.version)
            # Remove ItemId and ChangeKey. We get them unconditionally
            additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}

        svc = SyncFolderItems(account=self.account)
        while True:
            yield from svc.call(
                folder=folder,
                shape=ID_ONLY,
                additional_fields=additional_fields,
                sync_state=sync_state,
                ignore=ignore,
                max_changes_returned=max_changes_returned,
                sync_scope=sync_scope,
            )
            if svc.sync_state == sync_state:
                # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here
                break
            sync_state = svc.sync_state  # Set the new sync state in the next call
            if svc.includes_last_item_in_range:  # Try again if there are more items
                break
        raise SyncCompleted(sync_state=svc.sync_state)

    def sync_hierarchy(self, sync_state=None, only_fields=None):
        folder = self._get_single_folder()
        if not folder:
            return
        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 folder.supported_fields(version=self.account.version)}
        else:
            for f in only_fields:
                folder.validate_field(field=f, version=self.account.version)
            # Remove ItemId and ChangeKey. We get them unconditionally
            additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}

        # Add required fields
        additional_fields.update(
            (FieldPath(field=folder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
        )

        svc = SyncFolderHierarchy(account=self.account)
        while True:
            yield from svc.call(
                folder=folder,
                shape=ID_ONLY,
                additional_fields=additional_fields,
                sync_state=sync_state,
            )
            if svc.sync_state == sync_state:
                # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here
                break
            sync_state = svc.sync_state  # Set the new sync state in the next call
            if svc.includes_last_item_in_range:  # Try again if there are more items
                break
        raise SyncCompleted(sync_state=svc.sync_state)

Ancestors

Class variables

var REQUIRED_FOLDER_FIELDS

Instance variables

var folders
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 supported_item_models
Expand source code
@property
def supported_item_models(self):
    return tuple(item_model for folder in self.folders for item_model in folder.supported_item_models)

Methods

def all(self)
Expand source code
def all(self):
    return QuerySet(self).all()
def allowed_item_fields(self)
Expand source code
def allowed_item_fields(self):
    # Return non-ID fields of all item classes allowed in this folder type
    fields = set()
    for item_model in self.supported_item_models:
        fields.update(set(item_model.supported_fields(version=self.account.version)))
    return fields
def exclude(self, *args, **kwargs)
Expand source code
def exclude(self, *args, **kwargs):
    return QuerySet(self).exclude(*args, **kwargs)
def filter(self, *args, **kwargs)

Find items in the folder(s).

Non-keyword args may be a list of Q instances.

Optional extra keyword arguments follow a Django-like QuerySet filter syntax (see https://docs.djangoproject.com/en/1.10/ref/models/querysets/#field-lookups).

We don't support '__year' and other date-related lookups. We also don't support '__endswith' or '__iendswith'.

We support the additional '__not' lookup in place of Django's exclude() for simple cases. For more complicated cases you need to create a Q object and use ~Q().

Examples

my_account.inbox.filter(datetime_received__gt=EWSDateTime(2016, 1, 1)) my_account.calendar.filter(start__range=(EWSDateTime(2016, 1, 1), EWSDateTime(2017, 1, 1))) my_account.tasks.filter(subject='Hi mom') my_account.tasks.filter(subject__not='Hi mom') my_account.tasks.filter(subject__contains='Foo') my_account.tasks.filter(subject__icontains='foo')

'endswith' and 'iendswith' could be emulated by searching with 'contains' or 'icontains' and then post-processing items. Fetch the field in question with additional_fields and remove items where the search string is not a postfix.

Expand source code
def filter(self, *args, **kwargs):
    """Find items in the folder(s).

    Non-keyword args may be a list of Q instances.

    Optional extra keyword arguments follow a Django-like QuerySet filter syntax (see
       https://docs.djangoproject.com/en/1.10/ref/models/querysets/#field-lookups).

    We don't support '__year' and other date-related lookups. We also don't support '__endswith' or '__iendswith'.

    We support the additional '__not' lookup in place of Django's exclude() for simple cases. For more complicated
    cases you need to create a Q object and use ~Q().

    Examples:

        my_account.inbox.filter(datetime_received__gt=EWSDateTime(2016, 1, 1))
        my_account.calendar.filter(start__range=(EWSDateTime(2016, 1, 1), EWSDateTime(2017, 1, 1)))
        my_account.tasks.filter(subject='Hi mom')
        my_account.tasks.filter(subject__not='Hi mom')
        my_account.tasks.filter(subject__contains='Foo')
        my_account.tasks.filter(subject__icontains='foo')

    'endswith' and 'iendswith' could be emulated by searching with 'contains' or 'icontains' and then
    post-processing items. Fetch the field in question with additional_fields and remove items where the search
    string is not a postfix.
    """
    return QuerySet(self).filter(*args, **kwargs)
def find_folders(self, q=None, shape='IdOnly', depth=None, additional_fields=None, page_size=None, max_items=None, offset=0)
Expand source code
@require_account
def find_folders(self, q=None, shape=ID_ONLY, depth=None, additional_fields=None, page_size=None, max_items=None,
                 offset=0):
    # 'depth' controls whether to return direct children or recurse into sub-folders
    from .base import BaseFolder, Folder
    if q is None:
        q = Q()
    if not self.folders:
        log.debug('Folder list is empty')
        return
    if q.is_never():
        log.debug('Query will never return results')
        return
    if q.is_empty():
        restriction = None
    else:
        restriction = Restriction(q, folders=self.folders, applies_to=Restriction.FOLDERS)
    if shape not in SHAPE_CHOICES:
        raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
    if depth is None:
        depth = self._get_default_folder_traversal_depth()
    if depth not in FOLDER_TRAVERSAL_CHOICES:
        raise ValueError("'depth' %s must be one of %s" % (depth, FOLDER_TRAVERSAL_CHOICES))
    if additional_fields is None:
        # Default to all non-complex properties. Subfolders will always be of class Folder
        additional_fields = self.get_folder_fields(target_cls=Folder, is_complex=False)
    else:
        for f in additional_fields:
            if f.field.is_complex:
                raise ValueError("find_folders() does not support field '%s'. Use get_folders()." % f.field.name)

    # Add required fields
    additional_fields.update(
        (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
    )

    yield from FindFolder(account=self.account, chunk_size=page_size).call(
            folders=self.folders,
            additional_fields=additional_fields,
            restriction=restriction,
            shape=shape,
            depth=depth,
            max_items=max_items,
            offset=offset,
    )
def find_items(self, q, shape='IdOnly', depth=None, additional_fields=None, order_fields=None, calendar_view=None, page_size=None, max_items=None, offset=0)

Private method to call the FindItem service.

:param q: a Q instance containing any restrictions :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is non-null, we always return Item objects. (Default value = ID_ONLY) :param depth: controls the whether to return soft-deleted items or not. (Default value = None) :param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware that complex fields can only be fetched with fetch() (i.e. the GetItem service). :param order_fields: the SortOrder fields, if any (Default value = None) :param calendar_view: a CalendarView instance, if any (Default value = None) :param page_size: the requested number of items per page (Default value = None) :param max_items: the max number of items to return (Default value = None) :param offset: the offset relative to the first item in the item collection (Default value = 0)

:return: a generator for the returned item IDs or items

Expand source code
def find_items(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
               calendar_view=None, page_size=None, max_items=None, offset=0):
    """Private method to call the FindItem service.

    :param q: a Q instance containing any restrictions
    :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is
      non-null, we always return Item objects. (Default value = ID_ONLY)
    :param depth: controls the whether to return soft-deleted items or not. (Default value = None)
    :param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware
      that complex fields can only be fetched with fetch() (i.e. the GetItem service).
    :param order_fields: the SortOrder fields, if any (Default value = None)
    :param calendar_view: a CalendarView instance, if any (Default value = None)
    :param page_size: the requested number of items per page (Default value = None)
    :param max_items: the max number of items to return (Default value = None)
    :param offset: the offset relative to the first item in the item collection (Default value = 0)

    :return: a generator for the returned item IDs or items
    """
    if not self.folders:
        log.debug('Folder list is empty')
        return
    if q.is_never():
        log.debug('Query will never return results')
        return
    if shape not in SHAPE_CHOICES:
        raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
    if depth is None:
        depth = self._get_default_item_traversal_depth()
    if depth not in ITEM_TRAVERSAL_CHOICES:
        raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
    if additional_fields:
        for f in additional_fields:
            self.validate_item_field(field=f, version=self.account.version)
            if f.field.is_complex:
                raise ValueError("find_items() does not support field '%s'. Use fetch() instead" % f.field.name)
    if calendar_view is not None and not isinstance(calendar_view, CalendarView):
        raise ValueError("'calendar_view' %s must be a CalendarView instance" % calendar_view)

    # Build up any restrictions
    if q.is_empty():
        restriction = None
        query_string = None
    elif q.query_string:
        restriction = None
        query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
    else:
        restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
        query_string = None
    log.debug(
        'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
        self.account,
        self.folders,
        shape,
        depth,
        additional_fields,
        restriction.q if restriction else None,
    )
    yield from FindItem(account=self.account, chunk_size=page_size).call(
        folders=self.folders,
        additional_fields=additional_fields,
        restriction=restriction,
        order_fields=order_fields,
        shape=shape,
        query_string=query_string,
        depth=depth,
        calendar_view=calendar_view,
        max_items=calendar_view.max_items if calendar_view else max_items,
        offset=offset,
    )
def find_people(self, q, shape='IdOnly', depth=None, additional_fields=None, order_fields=None, page_size=None, max_items=None, offset=0)

Private method to call the FindPeople service.

:param q: a Q instance containing any restrictions :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is non-null, we always return Persona objects. (Default value = ID_ONLY) :param depth: controls the whether to return soft-deleted items or not. (Default value = None) :param additional_fields: the extra properties we want on the return objects. Default is no properties. :param order_fields: the SortOrder fields, if any (Default value = None) :param page_size: the requested number of items per page (Default value = None) :param max_items: the max number of items to return (Default value = None) :param offset: the offset relative to the first item in the item collection (Default value = 0)

:return: a generator for the returned personas

Expand source code
def find_people(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
                page_size=None, max_items=None, offset=0):
    """Private method to call the FindPeople service.

    :param q: a Q instance containing any restrictions
    :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is
      non-null, we always return Persona objects. (Default value = ID_ONLY)
    :param depth: controls the whether to return soft-deleted items or not. (Default value = None)
    :param additional_fields: the extra properties we want on the return objects. Default is no properties.
    :param order_fields: the SortOrder fields, if any (Default value = None)
    :param page_size: the requested number of items per page (Default value = None)
    :param max_items: the max number of items to return (Default value = None)
    :param offset: the offset relative to the first item in the item collection (Default value = 0)

    :return: a generator for the returned personas
    """
    folder = self._get_single_folder()
    if not folder:
        return
    if q.is_never():
        log.debug('Query will never return results')
        return
    if shape not in SHAPE_CHOICES:
        raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
    if depth is None:
        depth = self._get_default_item_traversal_depth()
    if depth not in ITEM_TRAVERSAL_CHOICES:
        raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
    if additional_fields:
        for f in additional_fields:
            Persona.validate_field(field=f, version=self.account.version)
            if f.field.is_complex:
                raise ValueError("find_people() does not support field '%s'" % f.field.name)

    # Build up any restrictions
    if q.is_empty():
        restriction = None
        query_string = None
    elif q.query_string:
        restriction = None
        query_string = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
    else:
        restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
        query_string = None
    yield from FindPeople(account=self.account, chunk_size=page_size).call(
            folder=folder,
            additional_fields=additional_fields,
            restriction=restriction,
            order_fields=order_fields,
            shape=shape,
            query_string=query_string,
            depth=depth,
            max_items=max_items,
            offset=offset,
    )
def get(self, *args, **kwargs)
Expand source code
def get(self, *args, **kwargs):
    return QuerySet(self).get(*args, **kwargs)
def get_folder_fields(self, target_cls, is_complex=None)
Expand source code
def get_folder_fields(self, target_cls, is_complex=None):
    return {
        FieldPath(field=f) for f in target_cls.supported_fields(version=self.account.version)
        if is_complex is None or f.is_complex is is_complex
    }
def get_folders(self, additional_fields=None)
Expand source code
def get_folders(self, additional_fields=None):
    # Expand folders with their full set of properties
    from .base import BaseFolder
    if not self.folders:
        log.debug('Folder list is empty')
        return
    if additional_fields is None:
        # Default to all complex properties
        additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=True)

    # Add required fields
    additional_fields.update(
        (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
    )

    yield from GetFolder(account=self.account).call(
            folders=self.folders,
            additional_fields=additional_fields,
            shape=ID_ONLY,
    )
def none(self)
Expand source code
def none(self):
    return QuerySet(self).none()
def people(self)
Expand source code
def people(self):
    return QuerySet(self).people()
def resolve(self)
Expand source code
def resolve(self):
    # Looks up the folders or folder IDs in the collection and returns full Folder instances with all fields set.
    from .base import BaseFolder
    resolveable_folders = []
    for f in self.folders:
        if isinstance(f, BaseFolder) and not f.get_folder_allowed:
            log.debug('GetFolder not allowed on folder %s. Non-complex fields must be fetched with FindFolder', f)
            yield f
        else:
            resolveable_folders.append(f)
    # Fetch all properties for the remaining folders of folder IDs
    additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=None)
    yield from self.__class__(account=self.account, folders=resolveable_folders).get_folders(
            additional_fields=additional_fields
    )
def subscribe_to_pull(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, timeout=60)
Expand source code
def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
    if not self.folders:
        log.debug('Folder list is empty')
        return
    yield from SubscribeToPull(account=self.account).call(
        folders=self.folders, event_types=event_types, watermark=watermark, timeout=timeout,
    )
def subscribe_to_push(self, callback_url, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, status_frequency=1)
Expand source code
def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
                      status_frequency=1):
    if not self.folders:
        log.debug('Folder list is empty')
        return
    yield from SubscribeToPush(account=self.account).call(
        folders=self.folders, event_types=event_types, watermark=watermark, status_frequency=status_frequency,
        url=callback_url,
    )
def subscribe_to_streaming(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'))
Expand source code
def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
    if not self.folders:
        log.debug('Folder list is empty')
        return
    yield from SubscribeToStreaming(account=self.account).call(folders=self.folders, event_types=event_types)
def sync_hierarchy(self, sync_state=None, only_fields=None)
Expand source code
def sync_hierarchy(self, sync_state=None, only_fields=None):
    folder = self._get_single_folder()
    if not folder:
        return
    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 folder.supported_fields(version=self.account.version)}
    else:
        for f in only_fields:
            folder.validate_field(field=f, version=self.account.version)
        # Remove ItemId and ChangeKey. We get them unconditionally
        additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}

    # Add required fields
    additional_fields.update(
        (FieldPath(field=folder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
    )

    svc = SyncFolderHierarchy(account=self.account)
    while True:
        yield from svc.call(
            folder=folder,
            shape=ID_ONLY,
            additional_fields=additional_fields,
            sync_state=sync_state,
        )
        if svc.sync_state == sync_state:
            # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here
            break
        sync_state = svc.sync_state  # Set the new sync state in the next call
        if svc.includes_last_item_in_range:  # Try again if there are more items
            break
    raise SyncCompleted(sync_state=svc.sync_state)
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None)
Expand source code
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
    folder = self._get_single_folder()
    if not folder:
        return
    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 folder.allowed_item_fields(version=self.account.version)}
    else:
        for field in only_fields:
            folder.validate_item_field(field=field, version=self.account.version)
        # Remove ItemId and ChangeKey. We get them unconditionally
        additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}

    svc = SyncFolderItems(account=self.account)
    while True:
        yield from svc.call(
            folder=folder,
            shape=ID_ONLY,
            additional_fields=additional_fields,
            sync_state=sync_state,
            ignore=ignore,
            max_changes_returned=max_changes_returned,
            sync_scope=sync_scope,
        )
        if svc.sync_state == sync_state:
            # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here
            break
        sync_state = svc.sync_state  # Set the new sync state in the next call
        if svc.includes_last_item_in_range:  # Try again if there are more items
            break
    raise SyncCompleted(sync_state=svc.sync_state)
def validate_item_field(self, field, version)
Expand source code
def validate_item_field(self, field, version):
    # For each field, check if the field is valid for any of the item models supported by this folder
    for item_model in self.supported_item_models:
        try:
            item_model.validate_field(field=field, version=version)
            break
        except InvalidField:
            continue
    else:
        raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))
def view(self, start, end, max_items=None, *args, **kwargs)

Implement the CalendarView option to FindItem. The difference between 'filter' and 'view' is that 'filter' only returns the master CalendarItem for recurring items, while 'view' unfolds recurring items and returns all CalendarItem occurrences as one would normally expect when presenting a calendar.

Supports the same semantics as filter, except for 'start' and 'end' keyword attributes which are both required and behave differently than filter. Here, they denote the start and end of the timespan of the view. All items the overlap the timespan are returned (items that end exactly on 'start' are also returned, for some reason).

EWS does not allow combining CalendarView with search restrictions (filter and exclude).

'max_items' defines the maximum number of items returned in this view. Optional.

:param start: :param end: :param max_items: (Default value = None) :return:

Expand source code
def view(self, start, end, max_items=None, *args, **kwargs):
    """Implement the CalendarView option to FindItem. The difference between 'filter' and 'view' is that 'filter'
    only returns the master CalendarItem for recurring items, while 'view' unfolds recurring items and returns all
    CalendarItem occurrences as one would normally expect when presenting a calendar.

    Supports the same semantics as filter, except for 'start' and 'end' keyword attributes which are both required
    and behave differently than filter. Here, they denote the start and end of the timespan of the view. All items
    the overlap the timespan are returned (items that end exactly on 'start' are also returned, for some reason).

    EWS does not allow combining CalendarView with search restrictions (filter and exclude).

    'max_items' defines the maximum number of items returned in this view. Optional.

    :param start:
    :param end:
    :param max_items:  (Default value = None)
    :return:
    """
    qs = QuerySet(self).filter(*args, **kwargs)
    qs.calendar_view = CalendarView(start=start, end=end, max_items=max_items)
    return qs
class SyncCompleted (sync_state)

This is a really ugly way of returning the sync state.

Expand source code
class SyncCompleted(Exception):
    """This is a really ugly way of returning the sync state."""

    def __init__(self, sync_state):
        super().__init__(sync_state)
        self.sync_state = sync_state

Ancestors

  • builtins.Exception
  • builtins.BaseException
exchangelib-4.6.1/docs/exchangelib/folders/index.html000066400000000000000000030340301414601472700226400ustar00rootroot00000000000000 exchangelib.folders API documentation

Module exchangelib.folders

Expand source code
from .base import BaseFolder, Folder
from .collections import FolderCollection
from .known_folders import AdminAuditLogs, AllContacts, AllItems, ArchiveDeletedItems, ArchiveInbox, \
    ArchiveMsgFolderRoot, ArchiveRecoverableItemsDeletions, ArchiveRecoverableItemsPurges, \
    ArchiveRecoverableItemsRoot, ArchiveRecoverableItemsVersions, Audits, Calendar, CalendarLogging, CommonViews, \
    Conflicts, Contacts, ConversationHistory, ConversationSettings, DefaultFoldersChangeHistory, DeferredAction, \
    DeletedItems, Directory, Drafts, ExchangeSyncData, Favorites, Files, FreebusyData, Friends, GALContacts, \
    GraphAnalytics, IMContactList, Inbox, Journal, JunkEmail, LocalFailures, Location, MailboxAssociations, Messages, \
    MsgFolderRoot, MyContacts, MyContactsExtended, NonDeletableFolderMixin, Notes, Outbox, ParkedMessages, \
    PassThroughSearchResults, PdpProfileV2Secured, PeopleConnect, QuickContacts, RSSFeeds, RecipientCache, \
    RecoverableItemsDeletions, RecoverableItemsPurges, RecoverableItemsRoot, RecoverableItemsVersions, Reminders, \
    Schedule, SearchFolders, SentItems, ServerFailures, Sharing, Shortcuts, Signal, SmsAndChatsSync, SpoolerQueue, \
    SyncIssues, System, Tasks, TemporarySaves, ToDoSearch, Views, VoiceMail, WellknownFolder, WorkingSet, \
    Companies, OrganizationalContacts, PeopleCentricConversationBuddies, NON_DELETABLE_FOLDERS
from .queryset import FolderQuerySet, SingleFolderQuerySet, FOLDER_TRAVERSAL_CHOICES, SHALLOW, DEEP, SOFT_DELETED
from .roots import Root, ArchiveRoot, PublicFoldersRoot, RootOfHierarchy
from ..properties import FolderId, DistinguishedFolderId

__all__ = [
    'FolderId', 'DistinguishedFolderId',
    'FolderCollection',
    'BaseFolder', 'Folder',
    'AdminAuditLogs', 'AllContacts', 'AllItems', 'ArchiveDeletedItems', 'ArchiveInbox', 'ArchiveMsgFolderRoot',
    'ArchiveRecoverableItemsDeletions', 'ArchiveRecoverableItemsPurges', 'ArchiveRecoverableItemsRoot',
    'ArchiveRecoverableItemsVersions', 'Audits', 'Calendar', 'CalendarLogging', 'CommonViews', 'Conflicts',
    'Contacts', 'ConversationHistory', 'ConversationSettings', 'DefaultFoldersChangeHistory', 'DeferredAction',
    'DeletedItems', 'Directory', 'Drafts', 'ExchangeSyncData', 'Favorites', 'Files', 'FreebusyData', 'Friends',
    'GALContacts', 'GraphAnalytics', 'IMContactList', 'Inbox', 'Journal', 'JunkEmail', 'LocalFailures',
    'Location', 'MailboxAssociations', 'Messages', 'MsgFolderRoot', 'MyContacts', 'MyContactsExtended',
    'NonDeletableFolderMixin', 'Notes', 'Outbox', 'ParkedMessages', 'PassThroughSearchResults',
    'PdpProfileV2Secured', 'PeopleConnect', 'QuickContacts', 'RSSFeeds', 'RecipientCache',
    'RecoverableItemsDeletions', 'RecoverableItemsPurges', 'RecoverableItemsRoot', 'RecoverableItemsVersions',
    'Reminders', 'Schedule', 'SearchFolders', 'SentItems', 'ServerFailures', 'Sharing', 'Shortcuts', 'Signal',
    'SmsAndChatsSync', 'SpoolerQueue', 'SyncIssues', 'System', 'Tasks', 'TemporarySaves', 'ToDoSearch', 'Views',
    'VoiceMail', 'WellknownFolder', 'WorkingSet', 'Companies', 'OrganizationalContacts',
    'PeopleCentricConversationBuddies', 'NON_DELETABLE_FOLDERS',
    'FolderQuerySet', 'SingleFolderQuerySet', 'FOLDER_TRAVERSAL_CHOICES', 'SHALLOW', 'DEEP', 'SOFT_DELETED',
    'Root', 'ArchiveRoot', 'PublicFoldersRoot', 'RootOfHierarchy',
]

Sub-modules

exchangelib.folders.base
exchangelib.folders.collections
exchangelib.folders.known_folders
exchangelib.folders.queryset
exchangelib.folders.roots

Classes

class AdminAuditLogs (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class AdminAuditLogs(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'adminauditlogs'
    supported_from = EXCHANGE_2013
    get_folder_allowed = False

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var get_folder_allowed
var supported_from

Inherited members

class AllContacts (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class AllContacts(NonDeletableFolderMixin, Contacts):
    CONTAINER_CLASS = 'IPF.Note'

    LOCALIZED_NAMES = {
        None: ('AllContacts',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class AllItems (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class AllItems(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF'

    LOCALIZED_NAMES = {
        None: ('AllItems',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class ArchiveDeletedItems (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ArchiveDeletedItems(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archivedeleteditems'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class ArchiveInbox (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ArchiveInbox(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archiveinbox'
    supported_from = EXCHANGE_2013_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class ArchiveMsgFolderRoot (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ArchiveMsgFolderRoot(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archivemsgfolderroot'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class ArchiveRecoverableItemsDeletions (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ArchiveRecoverableItemsDeletions(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemsdeletions'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class ArchiveRecoverableItemsPurges (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ArchiveRecoverableItemsPurges(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemspurges'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class ArchiveRecoverableItemsRoot (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ArchiveRecoverableItemsRoot(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemsroot'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class ArchiveRecoverableItemsVersions (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ArchiveRecoverableItemsVersions(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemsversions'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class ArchiveRoot (**kwargs)

The root of the archive folders hierarchy. Not available on all mailboxes.

Expand source code
class ArchiveRoot(RootOfHierarchy):
    """The root of the archive folders hierarchy. Not available on all mailboxes."""

    DISTINGUISHED_FOLDER_ID = 'archiveroot'
    supported_from = EXCHANGE_2010_SP1
    WELLKNOWN_FOLDERS = WELLKNOWN_FOLDERS_IN_ARCHIVE_ROOT

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var WELLKNOWN_FOLDERS
var supported_from

Inherited members

class Audits (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Audits(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Audits',),
    }
    get_folder_allowed = False

Ancestors

Class variables

var LOCALIZED_NAMES
var get_folder_allowed

Inherited members

class BaseFolder (**kwargs)

Base class for all classes that implement a folder.

Expand source code
class BaseFolder(RegisterMixIn, SearchableMixIn, metaclass=EWSMeta):
    """Base class for all classes that implement a folder."""

    ELEMENT_NAME = 'Folder'
    NAMESPACE = TNS
    # See https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid
    DISTINGUISHED_FOLDER_ID = None
    # Default item type for this folder. See
    # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxosfld/68a85898-84fe-43c4-b166-4711c13cdd61
    CONTAINER_CLASS = None
    supported_item_models = ITEM_CLASSES  # The Item types that this folder can contain. Default is all
    # Marks the version from which a distinguished folder was introduced. A possibly authoritative source is:
    # https://github.com/OfficeDev/ews-managed-api/blob/master/Enumerations/WellKnownFolderName.cs
    supported_from = None
    # Whether this folder type is allowed with the GetFolder service
    get_folder_allowed = True
    DEFAULT_FOLDER_TRAVERSAL_DEPTH = DEEP_FOLDERS
    DEFAULT_ITEM_TRAVERSAL_DEPTH = SHALLOW_ITEMS
    LOCALIZED_NAMES = {}  # A map of (str)locale: (tuple)localized_folder_names
    ITEM_MODEL_MAP = {cls.response_tag(): cls for cls in ITEM_CLASSES}
    ID_ELEMENT_CLS = FolderId

    _id = IdElementField(field_uri='folder:FolderId', value_cls=ID_ELEMENT_CLS)
    parent_folder_id = EWSElementField(field_uri='folder:ParentFolderId', value_cls=ParentFolderId,
                                       is_read_only=True)
    folder_class = CharField(field_uri='folder:FolderClass', is_required_after_save=True)
    name = CharField(field_uri='folder:DisplayName')
    total_count = IntegerField(field_uri='folder:TotalCount', is_read_only=True)
    child_folder_count = IntegerField(field_uri='folder:ChildFolderCount', is_read_only=True)
    unread_count = IntegerField(field_uri='folder:UnreadCount', is_read_only=True)

    __slots__ = 'is_distinguished', 'item_sync_state', 'folder_sync_state'

    # Used to register extended properties
    INSERT_AFTER_FIELD = 'child_folder_count'

    def __init__(self, **kwargs):
        self.is_distinguished = kwargs.pop('is_distinguished', False)
        self.item_sync_state = kwargs.pop('item_sync_state', None)
        self.folder_sync_state = kwargs.pop('folder_sync_state', None)
        super().__init__(**kwargs)

    @property
    @abc.abstractmethod
    def account(self):
        pass

    @property
    @abc.abstractmethod
    def root(self):
        pass

    @property
    @abc.abstractmethod
    def parent(self):
        pass

    @property
    def is_deletable(self):
        return not self.is_distinguished

    def clean(self, version=None):
        super().clean(version=version)
        # Set a default folder class for new folders. A folder class cannot be changed after saving.
        if self.id is None and self.folder_class is None:
            self.folder_class = self.CONTAINER_CLASS

    @property
    def children(self):
        # It's dangerous to return a generator here because we may then call methods on a child that result in the
        # cache being updated while it's iterated.
        return FolderCollection(account=self.account, folders=self.root.get_children(self))

    @property
    def parts(self):
        parts = [self]
        f = self.parent
        while f:
            parts.insert(0, f)
            f = f.parent
        return parts

    @property
    def absolute(self):
        return ''.join('/%s' % p.name for p in self.parts)

    def _walk(self):
        for c in self.children:
            yield c
            yield from c.walk()

    def walk(self):
        return FolderCollection(account=self.account, folders=self._walk())

    def _glob(self, pattern):
        split_pattern = pattern.rsplit('/', 1)
        head, tail = (split_pattern[0], None) if len(split_pattern) == 1 else split_pattern
        if head == '':
            # We got an absolute path. Restart globbing at root
            yield from self.root.glob(tail or '*')
        elif head == '..':
            # Relative path with reference to parent. Restart globbing at parent
            if not self.parent:
                raise ValueError('Already at top')
            yield from self.parent.glob(tail or '*')
        elif head == '**':
            # Match anything here or in any subfolder at arbitrary depth
            for c in self.walk():
                # fnmatch() may be case-sensitive depending on operating system:
                # force a case-insensitive match since case appears not to
                # matter for folders in Exchange
                if fnmatch(c.name.lower(), (tail or '*').lower()):
                    yield c
        else:
            # Regular pattern
            for c in self.children:
                # See note above on fnmatch() case-sensitivity
                if not fnmatch(c.name.lower(), head.lower()):
                    continue
                if tail is None:
                    yield c
                    continue
                yield from c.glob(tail)

    def glob(self, pattern):
        return FolderCollection(account=self.account, folders=self._glob(pattern))

    def tree(self):
        """Return a string representation of the folder structure of this folder. Example:

        root
        ├── inbox
        │   └── todos
        └── archive
            ├── Last Job
            ├── exchangelib issues
            └── Mom
        """
        tree = '%s\n' % self.name
        children = list(self.children)
        for i, c in enumerate(sorted(children, key=attrgetter('name')), start=1):
            nodes = c.tree().split('\n')
            for j, node in enumerate(nodes, start=1):
                if i != len(children) and j == 1:
                    # Not the last child, but the first node, which is the name of the child
                    tree += '├── %s\n' % node
                elif i != len(children) and j > 1:
                    # Not the last child, and not name of child
                    tree += '│   %s\n' % node
                elif i == len(children) and j == 1:
                    # Not the last child, but the first node, which is the name of the child
                    tree += '└── %s\n' % node
                else:  # Last child, and not name of child
                    tree += '    %s\n' % node
        return tree.strip()

    @classmethod
    def supports_version(cls, version):
        # 'version' is a Version instance, for convenience by callers
        if not isinstance(version, Version):
            raise ValueError("'version' %r must be a Version instance" % version)
        if not cls.supported_from:
            return True
        return version.build >= cls.supported_from

    @property
    def has_distinguished_name(self):
        return self.name and self.DISTINGUISHED_FOLDER_ID and self.name.lower() == self.DISTINGUISHED_FOLDER_ID.lower()

    @classmethod
    def localized_names(cls, locale):
        # Return localized names for a specific locale. If no locale-specific names exist, return the default names,
        # if any.
        return tuple(s.lower() for s in cls.LOCALIZED_NAMES.get(locale, cls.LOCALIZED_NAMES.get(None, [])))

    @staticmethod
    def folder_cls_from_container_class(container_class):
        """Return a reasonable folder class given a container class, e.g. 'IPF.Note'. Don't iterate WELLKNOWN_FOLDERS
        because many folder classes have the same CONTAINER_CLASS.

        :param container_class:
        :return:
        """
        from .known_folders import Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, \
            RecipientCache, RSSFeeds
        for folder_cls in (
                Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, RecipientCache,
                RSSFeeds):
            if folder_cls.CONTAINER_CLASS == container_class:
                return folder_cls
        raise KeyError()

    @classmethod
    def item_model_from_tag(cls, tag):
        try:
            return cls.ITEM_MODEL_MAP[tag]
        except KeyError:
            raise ValueError('Item type %s was unexpected in a %s folder' % (tag, cls.__name__))

    @classmethod
    def allowed_item_fields(cls, version):
        # Return non-ID fields of all item classes allowed in this folder type
        fields = set()
        for item_model in cls.supported_item_models:
            fields.update(
                set(item_model.supported_fields(version=version))
            )
        return fields

    def validate_item_field(self, field, version):
        # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
        # for the item types supported by this folder.

        # For each field, check if the field is valid for any of the item models supported by this folder
        for item_model in self.supported_item_models:
            try:
                item_model.validate_field(field=field, version=version)
                break
            except InvalidField:
                continue
        else:
            raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))

    def normalize_fields(self, fields):
        # Takes a list of fieldnames, Field or FieldPath objects pointing to item fields. Turns them into FieldPath
        # objects and adds internal timezone fields if necessary. Assume fields are already validated.
        fields = list(fields)
        has_start, has_end = False, False
        for i, field_path in enumerate(fields):
            # Allow both Field and FieldPath instances and string field paths as input
            if isinstance(field_path, str):
                field_path = FieldPath.from_string(field_path=field_path, folder=self)
                fields[i] = field_path
            elif isinstance(field_path, Field):
                field_path = FieldPath(field=field_path)
                fields[i] = field_path
            if not isinstance(field_path, FieldPath):
                raise ValueError("Field %r must be a string or FieldPath instance" % field_path)
            if field_path.field.name == 'start':
                has_start = True
            elif field_path.field.name == 'end':
                has_end = True

        # For CalendarItem items, we want to inject internal timezone fields. See also CalendarItem.clean()
        if CalendarItem in self.supported_item_models:
            meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
            if self.account.version.build < EXCHANGE_2010:
                if has_start or has_end:
                    fields.append(FieldPath(field=meeting_tz_field))
            else:
                if has_start:
                    fields.append(FieldPath(field=start_tz_field))
                if has_end:
                    fields.append(FieldPath(field=end_tz_field))
        return fields

    @classmethod
    def get_item_field_by_fieldname(cls, fieldname):
        for item_model in cls.supported_item_models:
            try:
                return item_model.get_field_by_fieldname(fieldname)
            except InvalidField:
                pass
        raise InvalidField("%r is not a valid field name on %s" % (fieldname, cls.supported_item_models))

    def get(self, *args, **kwargs):
        return FolderCollection(account=self.account, folders=[self]).get(*args, **kwargs)

    def all(self):
        return FolderCollection(account=self.account, folders=[self]).all()

    def none(self):
        return FolderCollection(account=self.account, folders=[self]).none()

    def filter(self, *args, **kwargs):
        return FolderCollection(account=self.account, folders=[self]).filter(*args, **kwargs)

    def exclude(self, *args, **kwargs):
        return FolderCollection(account=self.account, folders=[self]).exclude(*args, **kwargs)

    def people(self):
        # No point in using a FolderCollection because FindPeople only supports one folder
        return FolderCollection(account=self.account, folders=[self]).people()

    def bulk_create(self, items, *args, **kwargs):
        return self.account.bulk_create(folder=self, items=items, *args, **kwargs)

    def save(self, update_fields=None):
        if self.id is None:
            # New folder
            if update_fields:
                raise ValueError("'update_fields' is only valid for updates")
            res = CreateFolder(account=self.account).get(parent_folder=self.parent, folders=[self])
            self._id = self.ID_ELEMENT_CLS(res.id, res.changekey)
            self.root.add_folder(self)  # Add this folder to the cache
            return self

        # Update folder
        if not update_fields:
            # The fields to update was not specified explicitly. Update all fields where update is possible
            update_fields = []
            for f in self.supported_fields(version=self.account.version):
                if f.is_read_only:
                    # These cannot be changed
                    continue
                if (f.is_required or f.is_required_after_save) and (
                        getattr(self, f.name) is None or (f.is_list and not getattr(self, f.name))
                ):
                    # These are required and cannot be deleted
                    continue
                update_fields.append(f.name)
        res = UpdateFolder(account=self.account).get(folders=[(self, update_fields)])
        folder_id, changekey = res.id, res.changekey
        if self.id != folder_id:
            raise ValueError('ID mismatch')
        # Don't check changekey value. It may not change on no-op updates
        self.changekey = changekey
        self.root.update_folder(self)  # Update the folder in the cache
        return self

    def move(self, to_folder):
        res = MoveFolder(account=self.account).get(folders=[self], to_folder=to_folder)
        folder_id, changekey = res.id, res.changekey
        if self.id != folder_id:
            raise ValueError('ID mismatch')
        # Don't check changekey value. It may not change on no-op moves
        self.changekey = changekey
        self.parent_folder_id = ParentFolderId(id=to_folder.id, changekey=to_folder.changekey)
        self.root.update_folder(self)  # Update the folder in the cache

    def delete(self, delete_type=HARD_DELETE):
        if delete_type not in DELETE_TYPE_CHOICES:
            raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
        DeleteFolder(account=self.account).get(folders=[self], delete_type=delete_type)
        self.root.remove_folder(self)  # Remove the updated folder from the cache
        self._id = None

    def empty(self, delete_type=HARD_DELETE, delete_sub_folders=False):
        if delete_type not in DELETE_TYPE_CHOICES:
            raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
        EmptyFolder(account=self.account).get(
            folders=[self], delete_type=delete_type, delete_sub_folders=delete_sub_folders
        )
        if delete_sub_folders:
            # We don't know exactly what was deleted, so invalidate the entire folder cache to be safe
            self.root.clear_cache()

    def wipe(self, page_size=None, _seen=None, _level=0):
        # Recursively deletes all items in this folder, and all subfolders and their content. Attempts to protect
        # distinguished folders from being deleted. Use with caution!
        _seen = _seen or set()
        if self.id in _seen:
            raise RecursionError('We already tried to wipe %s' % self)
        if _level > 16:
            raise RecursionError('Max recursion level reached: %s' % _level)
        _seen.add(self.id)
        log.warning('Wiping %s', self)
        has_distinguished_subfolders = any(f.is_distinguished for f in self.children)
        try:
            if has_distinguished_subfolders:
                self.empty(delete_sub_folders=False)
            else:
                self.empty(delete_sub_folders=True)
        except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
            try:
                if has_distinguished_subfolders:
                    raise  # We already tried this
                self.empty(delete_sub_folders=False)
            except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
                log.warning('Not allowed to empty %s. Trying to delete items instead', self)
                try:
                    self.all().delete(**dict(page_size=page_size) if page_size else {})
                except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound):
                    log.warning('Not allowed to delete items in %s', self)
        _level += 1
        for f in self.children:
            f.wipe(page_size=page_size, _seen=_seen, _level=_level)
            # Remove non-distinguished children that are empty and have no subfolders
            if f.is_deletable and not f.children:
                log.warning('Deleting folder %s', f)
                try:
                    f.delete()
                except ErrorDeleteDistinguishedFolder:
                    log.warning('Tried to delete a distinguished folder (%s)', f)

    def test_access(self):
        """Does a simple FindItem to test (read) access to the folder. Maybe the account doesn't exist, maybe the
        service user doesn't have access to the calendar. This will throw the most common errors.
        """
        self.all().exists()
        return True

    @classmethod
    def _kwargs_from_elem(cls, elem, account):
        # Check for 'DisplayName' element before collecting kwargs because because that clears the elements
        has_name_elem = elem.find(cls.get_field_by_fieldname('name').response_tag()) is not None
        kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
        if has_name_elem and not kwargs['name']:
            # When we request the 'DisplayName' property, some folders may still be returned with an empty value.
            # Assign a default name to these folders.
            kwargs['name'] = cls.DISTINGUISHED_FOLDER_ID
        return kwargs

    def to_folder_id(self):
        if self.is_distinguished:
            # Don't add the changekey here. When modifying folder content, we usually don't care if others have changed
            # the folder content since we fetched the changekey.
            if self.account:
                return DistinguishedFolderId(
                    id=self.DISTINGUISHED_FOLDER_ID,
                    mailbox=Mailbox(email_address=self.account.primary_smtp_address)
                )
            return DistinguishedFolderId(id=self.DISTINGUISHED_FOLDER_ID)
        if self.id:
            return FolderId(id=self.id, changekey=self.changekey)
        raise ValueError('Must be a distinguished folder or have an ID')

    def to_xml(self, version):
        try:
            return self.to_folder_id().to_xml(version=version)
        except ValueError:
            return super().to_xml(version=version)

    def to_id_xml(self, version):
        # Folder(name='Foo') is a perfectly valid ID to e.g. create a folder
        return self.to_xml(version=version)

    @classmethod
    def resolve(cls, account, folder):
        # Resolve a single folder
        folders = list(FolderCollection(account=account, folders=[folder]).resolve())
        if not folders:
            raise ErrorFolderNotFound('Could not find folder %r' % folder)
        if len(folders) != 1:
            raise ValueError('Expected result length 1, but got %s' % folders)
        f = folders[0]
        if isinstance(f, Exception):
            raise f
        if f.__class__ != cls:
            raise ValueError("Expected folder %r to be a %s instance" % (f, cls))
        return f

    @require_id
    def refresh(self):
        fresh_folder = self.resolve(account=self.account, folder=self)
        if self.id != fresh_folder.id:
            raise ValueError('ID mismatch')
        # Apparently, the changekey may get updated
        for f in self.FIELDS:
            setattr(self, f.name, getattr(fresh_folder, f.name))
        return self

    @require_id
    def get_user_configuration(self, name, properties=ALL):
        return GetUserConfiguration(account=self.account).get(
            user_configuration_name=UserConfigurationNameMNS(name=name, folder=self),
            properties=properties,
        )

    @require_id
    def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
        user_configuration = UserConfiguration(
            user_configuration_name=UserConfigurationName(name=name, folder=self),
            dictionary=dictionary,
            xml_data=xml_data,
            binary_data=binary_data,
        )
        return CreateUserConfiguration(account=self.account).get(user_configuration=user_configuration)

    @require_id
    def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
        user_configuration = UserConfiguration(
            user_configuration_name=UserConfigurationName(name=name, folder=self),
            dictionary=dictionary,
            xml_data=xml_data,
            binary_data=binary_data,
        )
        return UpdateUserConfiguration(account=self.account).get(user_configuration=user_configuration)

    @require_id
    def delete_user_configuration(self, name):
        return DeleteUserConfiguration(account=self.account).get(
            user_configuration_name=UserConfigurationNameMNS(name=name, folder=self)
        )

    @require_id
    def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, 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
        """
        s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
            event_types=event_types, watermark=watermark, timeout=timeout,
        ))
        if len(s_ids) != 1:
            raise ValueError('Expected result length 1, but got %s' % s_ids)
        s_id = s_ids[0]
        if isinstance(s_id, Exception):
            raise s_id
        return s_id

    @require_id
    def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, 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
        """
        s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
            event_types=event_types, watermark=watermark, status_frequency=status_frequency, callback_url=callback_url,
        ))
        if len(s_ids) != 1:
            raise ValueError('Expected result length 1, but got %s' % s_ids)
        s_id = s_ids[0]
        if isinstance(s_id, Exception):
            raise s_id
        return s_id

    @require_id
    def subscribe_to_streaming(self, event_types=SubscribeToPush.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
        """
        s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(
            event_types=event_types,
        ))
        if len(s_ids) != 1:
            raise ValueError('Expected result length 1, but got %s' % s_ids)
        s_id = s_ids[0]
        if isinstance(s_id, Exception):
            raise s_id
        return s_id

    @require_id
    def pull_subscription(self, **kwargs):
        return PullSubscription(folder=self, **kwargs)

    @require_id
    def push_subscription(self, **kwargs):
        return PushSubscription(folder=self, **kwargs)

    @require_id
    def streaming_subscription(self, **kwargs):
        return StreamingSubscription(folder=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 folder instance, but it makes sense to keep the method along the other
        sync methods.
        """
        return Unsubscribe(account=self.account).get(subscription_id=subscription_id)

    def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
        """Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after
        this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.

        :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
        :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
        :param ignore: A list of Item IDs to ignore in the sync
        :param max_changes_returned: The max number of change
        :param sync_scope: Specify whether to return just items, or items and folder associated information. Possible
           values are specified in SyncFolderitems.SYNC_SCOPES
        :return: A generator of (change_type, item) tuples
        """
        if not sync_state:
            sync_state = self.item_sync_state
        try:
            yield from FolderCollection(account=self.account, folders=[self]).sync_items(
                sync_state=sync_state,
                only_fields=only_fields,
                ignore=ignore,
                max_changes_returned=max_changes_returned,
                sync_scope=sync_scope,
            )
        except SyncCompleted as e:
            # Set the new sync state on the folder instance
            self.item_sync_state = e.sync_state

    def sync_hierarchy(self, sync_state=None, only_fields=None):
        """Return all folder changes to a folder hierarchy, as a generator. If sync_state is specified, get all folder
        changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new
        sync state.

        :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
        :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
        :return:
        """
        if not sync_state:
            sync_state = self.folder_sync_state
        try:
            yield from FolderCollection(account=self.account, folders=[self]).sync_hierarchy(
                sync_state=sync_state,
                only_fields=only_fields,
            )
        except SyncCompleted as e:
            # Set the new sync state on the folder instance
            self.folder_sync_state = e.sync_state

    def get_events(self, subscription_id, watermark):
        """Get events since the given watermark. Non-blocking.

        :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|push]()
        :param watermark: Either the watermark from the subscription, or as returned in the last .get_events() call.
        :return: A Notification object containing a list of events

        This method doesn't need the current folder instance, but it makes sense to keep the method along the other
        sync methods.
        """
        svc = GetEvents(account=self.account)
        while True:
            notification = svc.get(subscription_id=subscription_id, watermark=watermark)
            yield notification
            if not notification.more_events:
                break

    def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None):
        """Get events since the subscription was created, in streaming mode. This method will block as many minutes
        as specified by 'connection_timeout'.

        :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming()
        :param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout
        is reached.
        :param max_notifications_returned: If specified, will exit after receiving this number of notifications
        :return: A generator of Notification objects, each containing a list of events

        This method doesn't need the current folder instance, but it makes sense to keep the method along the other
        sync methods.
        """
        # Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
        request_timeout = connection_timeout*60 + 60
        svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
        for i, notification in enumerate(
                svc.call(subscription_ids=[subscription_id], connection_timeout=connection_timeout),
                start=1
        ):
            yield notification
            if max_notifications_returned and i >= max_notifications_returned:
                svc.stop_streaming()
                break
        if svc.error_subscription_ids:
            raise ErrorInvalidSubscription('Invalid subscription IDs: %s' % svc.error_subscription_ids)

    def __floordiv__(self, other):
        """Support the some_folder // 'child_folder' // 'child_of_child_folder' navigation syntax.

        Works like as __truediv__ but does not touch the folder cache.

        This is useful if the folder hierarchy contains a huge number of folders and you don't want to fetch them all

        :param other:
        :return:
        """
        if other == '..':
            raise ValueError('Cannot get parent without a folder cache')

        if other == '.':
            return self

        # Assume an exact match on the folder name in a shallow search will only return at most one folder
        try:
            return SingleFolderQuerySet(account=self.account, folder=self).depth(SHALLOW_FOLDERS).get(name=other)
        except DoesNotExist:
            raise ErrorFolderNotFound("No subfolder with name '%s'" % other)

    def __truediv__(self, other):
        """Support the some_folder / 'child_folder' / 'child_of_child_folder' navigation syntax."""
        if other == '..':
            if not self.parent:
                raise ValueError('Already at top')
            return self.parent
        if other == '.':
            return self
        for c in self.children:
            if c.name == other:
                return c
        raise ErrorFolderNotFound("No subfolder with name '%s'" % other)

    def __repr__(self):
        return self.__class__.__name__ + \
               repr((self.root, self.name, self.total_count, self.unread_count, self.child_folder_count,
                     self.folder_class, self.id, self.changekey))

    def __str__(self):
        return '%s (%s)' % (self.__class__.__name__, self.name)

Ancestors

Subclasses

Class variables

var CONTAINER_CLASS
var DEFAULT_FOLDER_TRAVERSAL_DEPTH
var DEFAULT_ITEM_TRAVERSAL_DEPTH
var DISTINGUISHED_FOLDER_ID
var ELEMENT_NAME
var FIELDS
var ID_ELEMENT_CLS
var INSERT_AFTER_FIELD
var ITEM_MODEL_MAP
var LOCALIZED_NAMES
var NAMESPACE
var get_folder_allowed
var supported_from
var supported_item_models

Static methods

def allowed_item_fields(version)
Expand source code
@classmethod
def allowed_item_fields(cls, version):
    # Return non-ID fields of all item classes allowed in this folder type
    fields = set()
    for item_model in cls.supported_item_models:
        fields.update(
            set(item_model.supported_fields(version=version))
        )
    return fields
def folder_cls_from_container_class(container_class)

Return a reasonable folder class given a container class, e.g. 'IPF.Note'. Don't iterate WELLKNOWN_FOLDERS because many folder classes have the same CONTAINER_CLASS.

:param container_class: :return:

Expand source code
@staticmethod
def folder_cls_from_container_class(container_class):
    """Return a reasonable folder class given a container class, e.g. 'IPF.Note'. Don't iterate WELLKNOWN_FOLDERS
    because many folder classes have the same CONTAINER_CLASS.

    :param container_class:
    :return:
    """
    from .known_folders import Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, \
        RecipientCache, RSSFeeds
    for folder_cls in (
            Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, RecipientCache,
            RSSFeeds):
        if folder_cls.CONTAINER_CLASS == container_class:
            return folder_cls
    raise KeyError()
def get_item_field_by_fieldname(fieldname)
Expand source code
@classmethod
def get_item_field_by_fieldname(cls, fieldname):
    for item_model in cls.supported_item_models:
        try:
            return item_model.get_field_by_fieldname(fieldname)
        except InvalidField:
            pass
    raise InvalidField("%r is not a valid field name on %s" % (fieldname, cls.supported_item_models))
def item_model_from_tag(tag)
Expand source code
@classmethod
def item_model_from_tag(cls, tag):
    try:
        return cls.ITEM_MODEL_MAP[tag]
    except KeyError:
        raise ValueError('Item type %s was unexpected in a %s folder' % (tag, cls.__name__))
def localized_names(locale)
Expand source code
@classmethod
def localized_names(cls, locale):
    # Return localized names for a specific locale. If no locale-specific names exist, return the default names,
    # if any.
    return tuple(s.lower() for s in cls.LOCALIZED_NAMES.get(locale, cls.LOCALIZED_NAMES.get(None, [])))
def resolve(account, folder)
Expand source code
@classmethod
def resolve(cls, account, folder):
    # Resolve a single folder
    folders = list(FolderCollection(account=account, folders=[folder]).resolve())
    if not folders:
        raise ErrorFolderNotFound('Could not find folder %r' % folder)
    if len(folders) != 1:
        raise ValueError('Expected result length 1, but got %s' % folders)
    f = folders[0]
    if isinstance(f, Exception):
        raise f
    if f.__class__ != cls:
        raise ValueError("Expected folder %r to be a %s instance" % (f, cls))
    return f
def supports_version(version)
Expand source code
@classmethod
def supports_version(cls, version):
    # 'version' is a Version instance, for convenience by callers
    if not isinstance(version, Version):
        raise ValueError("'version' %r must be a Version instance" % version)
    if not cls.supported_from:
        return True
    return version.build >= cls.supported_from

Instance variables

var absolute
Expand source code
@property
def absolute(self):
    return ''.join('/%s' % p.name for p in self.parts)
var account
Expand source code
@property
@abc.abstractmethod
def account(self):
    pass
var child_folder_count
var children
Expand source code
@property
def children(self):
    # It's dangerous to return a generator here because we may then call methods on a child that result in the
    # cache being updated while it's iterated.
    return FolderCollection(account=self.account, folders=self.root.get_children(self))
var folder_class
var folder_sync_state

Return an attribute of instance, which is of type owner.

var has_distinguished_name
Expand source code
@property
def has_distinguished_name(self):
    return self.name and self.DISTINGUISHED_FOLDER_ID and self.name.lower() == self.DISTINGUISHED_FOLDER_ID.lower()
var is_deletable
Expand source code
@property
def is_deletable(self):
    return not self.is_distinguished
var is_distinguished

Return an attribute of instance, which is of type owner.

var item_sync_state

Return an attribute of instance, which is of type owner.

var name
var parent
Expand source code
@property
@abc.abstractmethod
def parent(self):
    pass
var parent_folder_id
var parts
Expand source code
@property
def parts(self):
    parts = [self]
    f = self.parent
    while f:
        parts.insert(0, f)
        f = f.parent
    return parts
var root
Expand source code
@property
@abc.abstractmethod
def root(self):
    pass
var total_count
var unread_count

Methods

def all(self)
Expand source code
def all(self):
    return FolderCollection(account=self.account, folders=[self]).all()
def bulk_create(self, items, *args, **kwargs)
Expand source code
def bulk_create(self, items, *args, **kwargs):
    return self.account.bulk_create(folder=self, items=items, *args, **kwargs)
def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)
    # Set a default folder class for new folders. A folder class cannot be changed after saving.
    if self.id is None and self.folder_class is None:
        self.folder_class = self.CONTAINER_CLASS
def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None)
Expand source code
@require_id
def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
    user_configuration = UserConfiguration(
        user_configuration_name=UserConfigurationName(name=name, folder=self),
        dictionary=dictionary,
        xml_data=xml_data,
        binary_data=binary_data,
    )
    return CreateUserConfiguration(account=self.account).get(user_configuration=user_configuration)
def delete(self, delete_type='HardDelete')
Expand source code
def delete(self, delete_type=HARD_DELETE):
    if delete_type not in DELETE_TYPE_CHOICES:
        raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
    DeleteFolder(account=self.account).get(folders=[self], delete_type=delete_type)
    self.root.remove_folder(self)  # Remove the updated folder from the cache
    self._id = None
def delete_user_configuration(self, name)
Expand source code
@require_id
def delete_user_configuration(self, name):
    return DeleteUserConfiguration(account=self.account).get(
        user_configuration_name=UserConfigurationNameMNS(name=name, folder=self)
    )
def empty(self, delete_type='HardDelete', delete_sub_folders=False)
Expand source code
def empty(self, delete_type=HARD_DELETE, delete_sub_folders=False):
    if delete_type not in DELETE_TYPE_CHOICES:
        raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
    EmptyFolder(account=self.account).get(
        folders=[self], delete_type=delete_type, delete_sub_folders=delete_sub_folders
    )
    if delete_sub_folders:
        # We don't know exactly what was deleted, so invalidate the entire folder cache to be safe
        self.root.clear_cache()
def exclude(self, *args, **kwargs)
Expand source code
def exclude(self, *args, **kwargs):
    return FolderCollection(account=self.account, folders=[self]).exclude(*args, **kwargs)
def filter(self, *args, **kwargs)
Expand source code
def filter(self, *args, **kwargs):
    return FolderCollection(account=self.account, folders=[self]).filter(*args, **kwargs)
def get(self, *args, **kwargs)
Expand source code
def get(self, *args, **kwargs):
    return FolderCollection(account=self.account, folders=[self]).get(*args, **kwargs)
def get_events(self, subscription_id, watermark)

Get events since the given watermark. Non-blocking.

:param subscription_id: A subscription ID as acquired by .subscribe_to_pull|push :param watermark: Either the watermark from the subscription, or as returned in the last .get_events() call. :return: A Notification object containing a list of events

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

Expand source code
def get_events(self, subscription_id, watermark):
    """Get events since the given watermark. Non-blocking.

    :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|push]()
    :param watermark: Either the watermark from the subscription, or as returned in the last .get_events() call.
    :return: A Notification object containing a list of events

    This method doesn't need the current folder instance, but it makes sense to keep the method along the other
    sync methods.
    """
    svc = GetEvents(account=self.account)
    while True:
        notification = svc.get(subscription_id=subscription_id, watermark=watermark)
        yield notification
        if not notification.more_events:
            break
def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None)

Get events since the subscription was created, in streaming mode. This method will block as many minutes as specified by 'connection_timeout'.

:param subscription_id: A subscription ID as acquired by .subscribe_to_streaming() :param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout is reached. :param max_notifications_returned: If specified, will exit after receiving this number of notifications :return: A generator of Notification objects, each containing a list of events

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

Expand source code
def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None):
    """Get events since the subscription was created, in streaming mode. This method will block as many minutes
    as specified by 'connection_timeout'.

    :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming()
    :param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout
    is reached.
    :param max_notifications_returned: If specified, will exit after receiving this number of notifications
    :return: A generator of Notification objects, each containing a list of events

    This method doesn't need the current folder instance, but it makes sense to keep the method along the other
    sync methods.
    """
    # Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
    request_timeout = connection_timeout*60 + 60
    svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
    for i, notification in enumerate(
            svc.call(subscription_ids=[subscription_id], connection_timeout=connection_timeout),
            start=1
    ):
        yield notification
        if max_notifications_returned and i >= max_notifications_returned:
            svc.stop_streaming()
            break
    if svc.error_subscription_ids:
        raise ErrorInvalidSubscription('Invalid subscription IDs: %s' % svc.error_subscription_ids)
def get_user_configuration(self, name, properties='All')
Expand source code
@require_id
def get_user_configuration(self, name, properties=ALL):
    return GetUserConfiguration(account=self.account).get(
        user_configuration_name=UserConfigurationNameMNS(name=name, folder=self),
        properties=properties,
    )
def glob(self, pattern)
Expand source code
def glob(self, pattern):
    return FolderCollection(account=self.account, folders=self._glob(pattern))
def move(self, to_folder)
Expand source code
def move(self, to_folder):
    res = MoveFolder(account=self.account).get(folders=[self], to_folder=to_folder)
    folder_id, changekey = res.id, res.changekey
    if self.id != folder_id:
        raise ValueError('ID mismatch')
    # Don't check changekey value. It may not change on no-op moves
    self.changekey = changekey
    self.parent_folder_id = ParentFolderId(id=to_folder.id, changekey=to_folder.changekey)
    self.root.update_folder(self)  # Update the folder in the cache
def none(self)
Expand source code
def none(self):
    return FolderCollection(account=self.account, folders=[self]).none()
def normalize_fields(self, fields)
Expand source code
def normalize_fields(self, fields):
    # Takes a list of fieldnames, Field or FieldPath objects pointing to item fields. Turns them into FieldPath
    # objects and adds internal timezone fields if necessary. Assume fields are already validated.
    fields = list(fields)
    has_start, has_end = False, False
    for i, field_path in enumerate(fields):
        # Allow both Field and FieldPath instances and string field paths as input
        if isinstance(field_path, str):
            field_path = FieldPath.from_string(field_path=field_path, folder=self)
            fields[i] = field_path
        elif isinstance(field_path, Field):
            field_path = FieldPath(field=field_path)
            fields[i] = field_path
        if not isinstance(field_path, FieldPath):
            raise ValueError("Field %r must be a string or FieldPath instance" % field_path)
        if field_path.field.name == 'start':
            has_start = True
        elif field_path.field.name == 'end':
            has_end = True

    # For CalendarItem items, we want to inject internal timezone fields. See also CalendarItem.clean()
    if CalendarItem in self.supported_item_models:
        meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
        if self.account.version.build < EXCHANGE_2010:
            if has_start or has_end:
                fields.append(FieldPath(field=meeting_tz_field))
        else:
            if has_start:
                fields.append(FieldPath(field=start_tz_field))
            if has_end:
                fields.append(FieldPath(field=end_tz_field))
    return fields
def people(self)
Expand source code
def people(self):
    # No point in using a FolderCollection because FindPeople only supports one folder
    return FolderCollection(account=self.account, folders=[self]).people()
def pull_subscription(self, **kwargs)
Expand source code
@require_id
def pull_subscription(self, **kwargs):
    return PullSubscription(folder=self, **kwargs)
def push_subscription(self, **kwargs)
Expand source code
@require_id
def push_subscription(self, **kwargs):
    return PushSubscription(folder=self, **kwargs)
def refresh(self)
Expand source code
@require_id
def refresh(self):
    fresh_folder = self.resolve(account=self.account, folder=self)
    if self.id != fresh_folder.id:
        raise ValueError('ID mismatch')
    # Apparently, the changekey may get updated
    for f in self.FIELDS:
        setattr(self, f.name, getattr(fresh_folder, f.name))
    return self
def save(self, update_fields=None)
Expand source code
def save(self, update_fields=None):
    if self.id is None:
        # New folder
        if update_fields:
            raise ValueError("'update_fields' is only valid for updates")
        res = CreateFolder(account=self.account).get(parent_folder=self.parent, folders=[self])
        self._id = self.ID_ELEMENT_CLS(res.id, res.changekey)
        self.root.add_folder(self)  # Add this folder to the cache
        return self

    # Update folder
    if not update_fields:
        # The fields to update was not specified explicitly. Update all fields where update is possible
        update_fields = []
        for f in self.supported_fields(version=self.account.version):
            if f.is_read_only:
                # These cannot be changed
                continue
            if (f.is_required or f.is_required_after_save) and (
                    getattr(self, f.name) is None or (f.is_list and not getattr(self, f.name))
            ):
                # These are required and cannot be deleted
                continue
            update_fields.append(f.name)
    res = UpdateFolder(account=self.account).get(folders=[(self, update_fields)])
    folder_id, changekey = res.id, res.changekey
    if self.id != folder_id:
        raise ValueError('ID mismatch')
    # Don't check changekey value. It may not change on no-op updates
    self.changekey = changekey
    self.root.update_folder(self)  # Update the folder in the cache
    return self
def streaming_subscription(self, **kwargs)
Expand source code
@require_id
def streaming_subscription(self, **kwargs):
    return StreamingSubscription(folder=self, **kwargs)
def subscribe_to_pull(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), 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

Expand source code
@require_id
def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, 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
    """
    s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
        event_types=event_types, watermark=watermark, timeout=timeout,
    ))
    if len(s_ids) != 1:
        raise ValueError('Expected result length 1, but got %s' % s_ids)
    s_id = s_ids[0]
    if isinstance(s_id, Exception):
        raise s_id
    return s_id
def subscribe_to_push(self, callback_url, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), 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

Expand source code
@require_id
def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, 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
    """
    s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
        event_types=event_types, watermark=watermark, status_frequency=status_frequency, callback_url=callback_url,
    ))
    if len(s_ids) != 1:
        raise ValueError('Expected result length 1, but got %s' % s_ids)
    s_id = s_ids[0]
    if isinstance(s_id, Exception):
        raise s_id
    return s_id
def subscribe_to_streaming(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'))

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

Expand source code
@require_id
def subscribe_to_streaming(self, event_types=SubscribeToPush.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
    """
    s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(
        event_types=event_types,
    ))
    if len(s_ids) != 1:
        raise ValueError('Expected result length 1, but got %s' % s_ids)
    s_id = s_ids[0]
    if isinstance(s_id, Exception):
        raise s_id
    return s_id
def sync_hierarchy(self, sync_state=None, only_fields=None)

Return all folder changes to a folder hierarchy, as a generator. If sync_state is specified, get all folder changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new sync state.

:param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service. :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields :return:

Expand source code
def sync_hierarchy(self, sync_state=None, only_fields=None):
    """Return all folder changes to a folder hierarchy, as a generator. If sync_state is specified, get all folder
    changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new
    sync state.

    :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
    :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
    :return:
    """
    if not sync_state:
        sync_state = self.folder_sync_state
    try:
        yield from FolderCollection(account=self.account, folders=[self]).sync_hierarchy(
            sync_state=sync_state,
            only_fields=only_fields,
        )
    except SyncCompleted as e:
        # Set the new sync state on the folder instance
        self.folder_sync_state = e.sync_state
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None)

Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.

:param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service. :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields :param ignore: A list of Item IDs to ignore in the sync :param max_changes_returned: The max number of change :param sync_scope: Specify whether to return just items, or items and folder associated information. Possible values are specified in SyncFolderitems.SYNC_SCOPES :return: A generator of (change_type, item) tuples

Expand source code
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
    """Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after
    this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.

    :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
    :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
    :param ignore: A list of Item IDs to ignore in the sync
    :param max_changes_returned: The max number of change
    :param sync_scope: Specify whether to return just items, or items and folder associated information. Possible
       values are specified in SyncFolderitems.SYNC_SCOPES
    :return: A generator of (change_type, item) tuples
    """
    if not sync_state:
        sync_state = self.item_sync_state
    try:
        yield from FolderCollection(account=self.account, folders=[self]).sync_items(
            sync_state=sync_state,
            only_fields=only_fields,
            ignore=ignore,
            max_changes_returned=max_changes_returned,
            sync_scope=sync_scope,
        )
    except SyncCompleted as e:
        # Set the new sync state on the folder instance
        self.item_sync_state = e.sync_state
def test_access(self)

Does a simple FindItem to test (read) access to the folder. Maybe the account doesn't exist, maybe the service user doesn't have access to the calendar. This will throw the most common errors.

Expand source code
def test_access(self):
    """Does a simple FindItem to test (read) access to the folder. Maybe the account doesn't exist, maybe the
    service user doesn't have access to the calendar. This will throw the most common errors.
    """
    self.all().exists()
    return True
def to_folder_id(self)
Expand source code
def to_folder_id(self):
    if self.is_distinguished:
        # Don't add the changekey here. When modifying folder content, we usually don't care if others have changed
        # the folder content since we fetched the changekey.
        if self.account:
            return DistinguishedFolderId(
                id=self.DISTINGUISHED_FOLDER_ID,
                mailbox=Mailbox(email_address=self.account.primary_smtp_address)
            )
        return DistinguishedFolderId(id=self.DISTINGUISHED_FOLDER_ID)
    if self.id:
        return FolderId(id=self.id, changekey=self.changekey)
    raise ValueError('Must be a distinguished folder or have an ID')
def to_id_xml(self, version)
Expand source code
def to_id_xml(self, version):
    # Folder(name='Foo') is a perfectly valid ID to e.g. create a folder
    return self.to_xml(version=version)
def to_xml(self, version)
Expand source code
def to_xml(self, version):
    try:
        return self.to_folder_id().to_xml(version=version)
    except ValueError:
        return super().to_xml(version=version)
def tree(self)

Return a string representation of the folder structure of this folder. Example:

root ├── inbox │ └── todos └── archive ├── Last Job ├── exchangelib issues └── Mom

Expand source code
def tree(self):
    """Return a string representation of the folder structure of this folder. Example:

    root
    ├── inbox
    │   └── todos
    └── archive
        ├── Last Job
        ├── exchangelib issues
        └── Mom
    """
    tree = '%s\n' % self.name
    children = list(self.children)
    for i, c in enumerate(sorted(children, key=attrgetter('name')), start=1):
        nodes = c.tree().split('\n')
        for j, node in enumerate(nodes, start=1):
            if i != len(children) and j == 1:
                # Not the last child, but the first node, which is the name of the child
                tree += '├── %s\n' % node
            elif i != len(children) and j > 1:
                # Not the last child, and not name of child
                tree += '│   %s\n' % node
            elif i == len(children) and j == 1:
                # Not the last child, but the first node, which is the name of the child
                tree += '└── %s\n' % node
            else:  # Last child, and not name of child
                tree += '    %s\n' % node
    return tree.strip()
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 folder instance, but it makes sense to keep the method along the other sync methods.

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 folder instance, but it makes sense to keep the method along the other
    sync methods.
    """
    return Unsubscribe(account=self.account).get(subscription_id=subscription_id)
def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None)
Expand source code
@require_id
def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
    user_configuration = UserConfiguration(
        user_configuration_name=UserConfigurationName(name=name, folder=self),
        dictionary=dictionary,
        xml_data=xml_data,
        binary_data=binary_data,
    )
    return UpdateUserConfiguration(account=self.account).get(user_configuration=user_configuration)
def validate_item_field(self, field, version)
Expand source code
def validate_item_field(self, field, version):
    # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
    # for the item types supported by this folder.

    # For each field, check if the field is valid for any of the item models supported by this folder
    for item_model in self.supported_item_models:
        try:
            item_model.validate_field(field=field, version=version)
            break
        except InvalidField:
            continue
    else:
        raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))
def walk(self)
Expand source code
def walk(self):
    return FolderCollection(account=self.account, folders=self._walk())
def wipe(self, page_size=None)
Expand source code
def wipe(self, page_size=None, _seen=None, _level=0):
    # Recursively deletes all items in this folder, and all subfolders and their content. Attempts to protect
    # distinguished folders from being deleted. Use with caution!
    _seen = _seen or set()
    if self.id in _seen:
        raise RecursionError('We already tried to wipe %s' % self)
    if _level > 16:
        raise RecursionError('Max recursion level reached: %s' % _level)
    _seen.add(self.id)
    log.warning('Wiping %s', self)
    has_distinguished_subfolders = any(f.is_distinguished for f in self.children)
    try:
        if has_distinguished_subfolders:
            self.empty(delete_sub_folders=False)
        else:
            self.empty(delete_sub_folders=True)
    except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
        try:
            if has_distinguished_subfolders:
                raise  # We already tried this
            self.empty(delete_sub_folders=False)
        except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
            log.warning('Not allowed to empty %s. Trying to delete items instead', self)
            try:
                self.all().delete(**dict(page_size=page_size) if page_size else {})
            except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound):
                log.warning('Not allowed to delete items in %s', self)
    _level += 1
    for f in self.children:
        f.wipe(page_size=page_size, _seen=_seen, _level=_level)
        # Remove non-distinguished children that are empty and have no subfolders
        if f.is_deletable and not f.children:
            log.warning('Deleting folder %s', f)
            try:
                f.delete()
            except ErrorDeleteDistinguishedFolder:
                log.warning('Tried to delete a distinguished folder (%s)', f)

Inherited members

class Calendar (**kwargs)

An interface for the Exchange calendar.

Expand source code
class Calendar(Folder):
    """An interface for the Exchange calendar."""

    DISTINGUISHED_FOLDER_ID = 'calendar'
    CONTAINER_CLASS = 'IPF.Appointment'
    supported_item_models = (CalendarItem,)

    LOCALIZED_NAMES = {
        'da_DK': ('Kalender',),
        'de_DE': ('Kalender',),
        'en_US': ('Calendar',),
        'es_ES': ('Calendario',),
        'fr_CA': ('Calendrier',),
        'nl_NL': ('Agenda',),
        'ru_RU': ('Календарь',),
        'sv_SE': ('Kalender',),
        'zh_CN': ('日历',),
    }

    def view(self, *args, **kwargs):
        return FolderCollection(account=self.account, folders=[self]).view(*args, **kwargs)

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES
var supported_item_models

Methods

def view(self, *args, **kwargs)
Expand source code
def view(self, *args, **kwargs):
    return FolderCollection(account=self.account, folders=[self]).view(*args, **kwargs)

Inherited members

class CalendarLogging (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class CalendarLogging(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Calendar Logging',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class CommonViews (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class CommonViews(NonDeletableFolderMixin, Folder):
    DEFAULT_ITEM_TRAVERSAL_DEPTH = ASSOCIATED
    LOCALIZED_NAMES = {
        None: ('Common Views',),
    }

Ancestors

Class variables

var DEFAULT_ITEM_TRAVERSAL_DEPTH
var LOCALIZED_NAMES

Inherited members

class Companies (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Companies(NonDeletableFolderMixin, Contacts):
    DISTINGUISHED_FOLDER_ID = None
    CONTAINTER_CLASS = 'IPF.Contact.Company'
    LOCALIZED_NAMES = {
        None: ('Companies',),
    }

Ancestors

Class variables

var CONTAINTER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class Conflicts (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class Conflicts(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'conflicts'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class Contacts (**kwargs)
Expand source code
class Contacts(Folder):
    DISTINGUISHED_FOLDER_ID = 'contacts'
    CONTAINER_CLASS = 'IPF.Contact'
    supported_item_models = (Contact, DistributionList)

    LOCALIZED_NAMES = {
        'da_DK': ('Kontaktpersoner',),
        'de_DE': ('Kontakte',),
        'en_US': ('Contacts',),
        'es_ES': ('Contactos',),
        'fr_CA': ('Contacts',),
        'nl_NL': ('Contactpersonen',),
        'ru_RU': ('Контакты',),
        'sv_SE': ('Kontakter',),
        'zh_CN': ('联系人',),
    }

Ancestors

Subclasses

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES
var supported_item_models

Inherited members

class ConversationHistory (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ConversationHistory(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'conversationhistory'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class ConversationSettings (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class ConversationSettings(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.Configuration'
    LOCALIZED_NAMES = {
        'da_DK': ('Indstillinger for samtalehandlinger',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class DefaultFoldersChangeHistory (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class DefaultFoldersChangeHistory(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPM.DefaultFolderHistoryItem'
    LOCALIZED_NAMES = {
        None: ('DefaultFoldersChangeHistory',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class DeferredAction (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class DeferredAction(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Deferred Action',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class DeletedItems (**kwargs)
Expand source code
class DeletedItems(Folder):
    DISTINGUISHED_FOLDER_ID = 'deleteditems'
    CONTAINER_CLASS = 'IPF.Note'
    supported_item_models = ITEM_CLASSES

    LOCALIZED_NAMES = {
        'da_DK': ('Slettet post',),
        'de_DE': ('Gelöschte Elemente',),
        'en_US': ('Deleted Items',),
        'es_ES': ('Elementos eliminados',),
        'fr_CA': ('Éléments supprimés',),
        'nl_NL': ('Verwijderde items',),
        'ru_RU': ('Удаленные',),
        'sv_SE': ('Borttaget',),
        'zh_CN': ('已删除邮件',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES
var supported_item_models

Inherited members

class Directory (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class Directory(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'directory'
    supported_from = EXCHANGE_2013_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class DistinguishedFolderId (*args, **kwargs)
Expand source code
class DistinguishedFolderId(FolderId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid"""

    ELEMENT_NAME = 'DistinguishedFolderId'

    mailbox = MailboxField()

    def clean(self, version=None):
        from .folders import PublicFoldersRoot
        super().clean(version=version)
        if self.id == PublicFoldersRoot.DISTINGUISHED_FOLDER_ID:
            # Avoid "ErrorInvalidOperation: It is not valid to specify a mailbox with the public folder root" from EWS
            self.mailbox = None

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var mailbox

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    from .folders import PublicFoldersRoot
    super().clean(version=version)
    if self.id == PublicFoldersRoot.DISTINGUISHED_FOLDER_ID:
        # Avoid "ErrorInvalidOperation: It is not valid to specify a mailbox with the public folder root" from EWS
        self.mailbox = None

Inherited members

class Drafts (**kwargs)
Expand source code
class Drafts(Messages):
    DISTINGUISHED_FOLDER_ID = 'drafts'

    LOCALIZED_NAMES = {
        'da_DK': ('Kladder',),
        'de_DE': ('Entwürfe',),
        'en_US': ('Drafts',),
        'es_ES': ('Borradores',),
        'fr_CA': ('Brouillons',),
        'nl_NL': ('Concepten',),
        'ru_RU': ('Черновики',),
        'sv_SE': ('Utkast',),
        'zh_CN': ('草稿',),
    }

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class ExchangeSyncData (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class ExchangeSyncData(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('ExchangeSyncData',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class Favorites (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class Favorites(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Note'
    DISTINGUISHED_FOLDER_ID = 'favorites'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class Files (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Files(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.Files'

    LOCALIZED_NAMES = {
        'da_DK': ('Filer',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class Folder (**kwargs)
Expand source code
class Folder(BaseFolder):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/folder"""

    permission_set = PermissionSetField(field_uri='folder:PermissionSet', supported_from=EXCHANGE_2007_SP1)
    effective_rights = EffectiveRightsField(field_uri='folder:EffectiveRights', is_read_only=True,
                                            supported_from=EXCHANGE_2007_SP1)

    __slots__ = '_root',

    def __init__(self, **kwargs):
        self._root = kwargs.pop('root', None)  # This is a pointer to the root of the folder hierarchy
        parent = kwargs.pop('parent', None)
        if parent:
            if self.root:
                if parent.root != self.root:
                    raise ValueError("'parent.root' must match 'root'")
            else:
                self.root = parent.root
            if 'parent_folder_id' in kwargs and parent.id != kwargs['parent_folder_id']:
                raise ValueError("'parent_folder_id' must match 'parent' ID")
            kwargs['parent_folder_id'] = ParentFolderId(id=parent.id, changekey=parent.changekey)
        super().__init__(**kwargs)

    @property
    def account(self):
        if self.root is None:
            return None
        return self.root.account

    @property
    def root(self):
        return self._root

    @root.setter
    def root(self, value):
        self._root = value

    @classmethod
    def register(cls, *args, **kwargs):
        if cls is not Folder:
            raise TypeError('For folders, custom fields must be registered on the Folder class')
        return super().register(*args, **kwargs)

    @classmethod
    def deregister(cls, *args, **kwargs):
        if cls is not Folder:
            raise TypeError('For folders, custom fields must be registered on the Folder class')
        return super().deregister(*args, **kwargs)

    @classmethod
    def get_distinguished(cls, root):
        """Get the distinguished folder for this folder class.

        :param root:
        :return:
        """
        try:
            return cls.resolve(
                account=root.account,
                folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
            )
        except MISSING_FOLDER_ERRORS:
            raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID)

    @property
    def parent(self):
        if not self.parent_folder_id:
            return None
        if self.parent_folder_id.id == self.id:
            # Some folders have a parent that references itself. Avoid circular references here
            return None
        return self.root.get_folder(self.parent_folder_id)

    @parent.setter
    def parent(self, value):
        if value is None:
            self.parent_folder_id = None
        else:
            if not isinstance(value, BaseFolder):
                raise ValueError("'value' %r must be a Folder instance" % value)
            self.root = value.root
            self.parent_folder_id = ParentFolderId(id=value.id, changekey=value.changekey)

    def clean(self, version=None):
        from .roots import RootOfHierarchy
        super().clean(version=version)
        if self.root and not isinstance(self.root, RootOfHierarchy):
            raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root)

    @classmethod
    def from_xml_with_root(cls, elem, root):
        folder = cls.from_xml(elem=elem, account=root.account)
        folder_cls = cls
        if cls == Folder:
            # We were called on the generic Folder class. Try to find a more specific class to return objects as.
            #
            # The "FolderClass" element value is the only indication we have in the FindFolder response of which
            # folder class we should create the folder with. And many folders share the same 'FolderClass' value, e.g.
            # Inbox and DeletedItems. We want to distinguish between these because otherwise we can't locate the right
            # folders types for e.g. Account.inbox and Account.trash.
            #
            # We should be able to just use the name, but apparently default folder names can be renamed to a set of
            # localized names using a PowerShell command:
            # https://docs.microsoft.com/en-us/powershell/module/exchange/client-access/Set-MailboxRegionalConfiguration
            #
            # Instead, search for a folder class using the localized name. If none are found, fall back to getting the
            # folder class by the "FolderClass" value.
            #
            # The returned XML may contain neither folder class nor name. In that case, we default to the generic
            # Folder class.
            if folder.name:
                try:
                    # TODO: fld_class.LOCALIZED_NAMES is most definitely neither complete nor authoritative
                    folder_cls = root.folder_cls_from_folder_name(folder_name=folder.name,
                                                                  locale=root.account.locale)
                    log.debug('Folder class %s matches localized folder name %s', folder_cls, folder.name)
                except KeyError:
                    pass
            if folder.folder_class and folder_cls == Folder:
                try:
                    folder_cls = cls.folder_cls_from_container_class(container_class=folder.folder_class)
                    log.debug('Folder class %s matches container class %s (%s)', folder_cls, folder.folder_class,
                              folder.name)
                except KeyError:
                    pass
            if folder_cls == Folder:
                log.debug('Fallback to class Folder (folder_class %s, name %s)', folder.folder_class, folder.name)
        return folder_cls(root=root, **{f.name: getattr(folder, f.name) for f in folder.FIELDS})

Ancestors

Subclasses

Class variables

var FIELDS

Static methods

def from_xml_with_root(elem, root)
Expand source code
@classmethod
def from_xml_with_root(cls, elem, root):
    folder = cls.from_xml(elem=elem, account=root.account)
    folder_cls = cls
    if cls == Folder:
        # We were called on the generic Folder class. Try to find a more specific class to return objects as.
        #
        # The "FolderClass" element value is the only indication we have in the FindFolder response of which
        # folder class we should create the folder with. And many folders share the same 'FolderClass' value, e.g.
        # Inbox and DeletedItems. We want to distinguish between these because otherwise we can't locate the right
        # folders types for e.g. Account.inbox and Account.trash.
        #
        # We should be able to just use the name, but apparently default folder names can be renamed to a set of
        # localized names using a PowerShell command:
        # https://docs.microsoft.com/en-us/powershell/module/exchange/client-access/Set-MailboxRegionalConfiguration
        #
        # Instead, search for a folder class using the localized name. If none are found, fall back to getting the
        # folder class by the "FolderClass" value.
        #
        # The returned XML may contain neither folder class nor name. In that case, we default to the generic
        # Folder class.
        if folder.name:
            try:
                # TODO: fld_class.LOCALIZED_NAMES is most definitely neither complete nor authoritative
                folder_cls = root.folder_cls_from_folder_name(folder_name=folder.name,
                                                              locale=root.account.locale)
                log.debug('Folder class %s matches localized folder name %s', folder_cls, folder.name)
            except KeyError:
                pass
        if folder.folder_class and folder_cls == Folder:
            try:
                folder_cls = cls.folder_cls_from_container_class(container_class=folder.folder_class)
                log.debug('Folder class %s matches container class %s (%s)', folder_cls, folder.folder_class,
                          folder.name)
            except KeyError:
                pass
        if folder_cls == Folder:
            log.debug('Fallback to class Folder (folder_class %s, name %s)', folder.folder_class, folder.name)
    return folder_cls(root=root, **{f.name: getattr(folder, f.name) for f in folder.FIELDS})
def get_distinguished(root)

Get the distinguished folder for this folder class.

:param root: :return:

Expand source code
@classmethod
def get_distinguished(cls, root):
    """Get the distinguished folder for this folder class.

    :param root:
    :return:
    """
    try:
        return cls.resolve(
            account=root.account,
            folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
        )
    except MISSING_FOLDER_ERRORS:
        raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID)

Instance variables

var account
Expand source code
@property
def account(self):
    if self.root is None:
        return None
    return self.root.account
var effective_rights
var parent
Expand source code
@property
def parent(self):
    if not self.parent_folder_id:
        return None
    if self.parent_folder_id.id == self.id:
        # Some folders have a parent that references itself. Avoid circular references here
        return None
    return self.root.get_folder(self.parent_folder_id)
var permission_set
var root
Expand source code
@property
def root(self):
    return self._root

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    from .roots import RootOfHierarchy
    super().clean(version=version)
    if self.root and not isinstance(self.root, RootOfHierarchy):
        raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root)

Inherited members

class FolderCollection (account, folders)

A class that implements an API for searching folders.

Implement a search API on a collection of folders.

:param account: An Account object :param folders: An iterable of folders, e.g. Folder.walk(), Folder.glob(), or [a.calendar, a.inbox]

Expand source code
class FolderCollection(SearchableMixIn):
    """A class that implements an API for searching folders."""

    # These fields are required in a FindFolder or GetFolder call to properly identify folder types
    REQUIRED_FOLDER_FIELDS = ('name', 'folder_class')

    def __init__(self, account, folders):
        """Implement a search API on a collection of folders.

        :param account: An Account object
        :param folders: An iterable of folders, e.g. Folder.walk(), Folder.glob(), or [a.calendar, a.inbox]
        """
        self.account = account
        self._folders = folders

    @threaded_cached_property
    def folders(self):
        # Resolve the list of folders, in case it's a generator
        return list(self._folders)

    def __len__(self):
        return len(self.folders)

    def __iter__(self):
        yield from self.folders

    def get(self, *args, **kwargs):
        return QuerySet(self).get(*args, **kwargs)

    def all(self):
        return QuerySet(self).all()

    def none(self):
        return QuerySet(self).none()

    def filter(self, *args, **kwargs):
        """Find items in the folder(s).

        Non-keyword args may be a list of Q instances.

        Optional extra keyword arguments follow a Django-like QuerySet filter syntax (see
           https://docs.djangoproject.com/en/1.10/ref/models/querysets/#field-lookups).

        We don't support '__year' and other date-related lookups. We also don't support '__endswith' or '__iendswith'.

        We support the additional '__not' lookup in place of Django's exclude() for simple cases. For more complicated
        cases you need to create a Q object and use ~Q().

        Examples:

            my_account.inbox.filter(datetime_received__gt=EWSDateTime(2016, 1, 1))
            my_account.calendar.filter(start__range=(EWSDateTime(2016, 1, 1), EWSDateTime(2017, 1, 1)))
            my_account.tasks.filter(subject='Hi mom')
            my_account.tasks.filter(subject__not='Hi mom')
            my_account.tasks.filter(subject__contains='Foo')
            my_account.tasks.filter(subject__icontains='foo')

        'endswith' and 'iendswith' could be emulated by searching with 'contains' or 'icontains' and then
        post-processing items. Fetch the field in question with additional_fields and remove items where the search
        string is not a postfix.
        """
        return QuerySet(self).filter(*args, **kwargs)

    def exclude(self, *args, **kwargs):
        return QuerySet(self).exclude(*args, **kwargs)

    def people(self):
        return QuerySet(self).people()

    def view(self, start, end, max_items=None, *args, **kwargs):
        """Implement the CalendarView option to FindItem. The difference between 'filter' and 'view' is that 'filter'
        only returns the master CalendarItem for recurring items, while 'view' unfolds recurring items and returns all
        CalendarItem occurrences as one would normally expect when presenting a calendar.

        Supports the same semantics as filter, except for 'start' and 'end' keyword attributes which are both required
        and behave differently than filter. Here, they denote the start and end of the timespan of the view. All items
        the overlap the timespan are returned (items that end exactly on 'start' are also returned, for some reason).

        EWS does not allow combining CalendarView with search restrictions (filter and exclude).

        'max_items' defines the maximum number of items returned in this view. Optional.

        :param start:
        :param end:
        :param max_items:  (Default value = None)
        :return:
        """
        qs = QuerySet(self).filter(*args, **kwargs)
        qs.calendar_view = CalendarView(start=start, end=end, max_items=max_items)
        return qs

    def allowed_item_fields(self):
        # Return non-ID fields of all item classes allowed in this folder type
        fields = set()
        for item_model in self.supported_item_models:
            fields.update(set(item_model.supported_fields(version=self.account.version)))
        return fields

    @property
    def supported_item_models(self):
        return tuple(item_model for folder in self.folders for item_model in folder.supported_item_models)

    def validate_item_field(self, field, version):
        # For each field, check if the field is valid for any of the item models supported by this folder
        for item_model in self.supported_item_models:
            try:
                item_model.validate_field(field=field, version=version)
                break
            except InvalidField:
                continue
        else:
            raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))

    def find_items(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
                   calendar_view=None, page_size=None, max_items=None, offset=0):
        """Private method to call the FindItem service.

        :param q: a Q instance containing any restrictions
        :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is
          non-null, we always return Item objects. (Default value = ID_ONLY)
        :param depth: controls the whether to return soft-deleted items or not. (Default value = None)
        :param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware
          that complex fields can only be fetched with fetch() (i.e. the GetItem service).
        :param order_fields: the SortOrder fields, if any (Default value = None)
        :param calendar_view: a CalendarView instance, if any (Default value = None)
        :param page_size: the requested number of items per page (Default value = None)
        :param max_items: the max number of items to return (Default value = None)
        :param offset: the offset relative to the first item in the item collection (Default value = 0)

        :return: a generator for the returned item IDs or items
        """
        if not self.folders:
            log.debug('Folder list is empty')
            return
        if q.is_never():
            log.debug('Query will never return results')
            return
        if shape not in SHAPE_CHOICES:
            raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
        if depth is None:
            depth = self._get_default_item_traversal_depth()
        if depth not in ITEM_TRAVERSAL_CHOICES:
            raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
        if additional_fields:
            for f in additional_fields:
                self.validate_item_field(field=f, version=self.account.version)
                if f.field.is_complex:
                    raise ValueError("find_items() does not support field '%s'. Use fetch() instead" % f.field.name)
        if calendar_view is not None and not isinstance(calendar_view, CalendarView):
            raise ValueError("'calendar_view' %s must be a CalendarView instance" % calendar_view)

        # Build up any restrictions
        if q.is_empty():
            restriction = None
            query_string = None
        elif q.query_string:
            restriction = None
            query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
        else:
            restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
            query_string = None
        log.debug(
            'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
            self.account,
            self.folders,
            shape,
            depth,
            additional_fields,
            restriction.q if restriction else None,
        )
        yield from FindItem(account=self.account, chunk_size=page_size).call(
            folders=self.folders,
            additional_fields=additional_fields,
            restriction=restriction,
            order_fields=order_fields,
            shape=shape,
            query_string=query_string,
            depth=depth,
            calendar_view=calendar_view,
            max_items=calendar_view.max_items if calendar_view else max_items,
            offset=offset,
        )

    def _get_single_folder(self):
        if len(self.folders) > 1:
            raise ValueError('Syncing folder hierarchy can only be done on a single folder')
        if not self.folders:
            log.debug('Folder list is empty')
            return None
        return self.folders[0]

    def find_people(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
                    page_size=None, max_items=None, offset=0):
        """Private method to call the FindPeople service.

        :param q: a Q instance containing any restrictions
        :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is
          non-null, we always return Persona objects. (Default value = ID_ONLY)
        :param depth: controls the whether to return soft-deleted items or not. (Default value = None)
        :param additional_fields: the extra properties we want on the return objects. Default is no properties.
        :param order_fields: the SortOrder fields, if any (Default value = None)
        :param page_size: the requested number of items per page (Default value = None)
        :param max_items: the max number of items to return (Default value = None)
        :param offset: the offset relative to the first item in the item collection (Default value = 0)

        :return: a generator for the returned personas
        """
        folder = self._get_single_folder()
        if not folder:
            return
        if q.is_never():
            log.debug('Query will never return results')
            return
        if shape not in SHAPE_CHOICES:
            raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
        if depth is None:
            depth = self._get_default_item_traversal_depth()
        if depth not in ITEM_TRAVERSAL_CHOICES:
            raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
        if additional_fields:
            for f in additional_fields:
                Persona.validate_field(field=f, version=self.account.version)
                if f.field.is_complex:
                    raise ValueError("find_people() does not support field '%s'" % f.field.name)

        # Build up any restrictions
        if q.is_empty():
            restriction = None
            query_string = None
        elif q.query_string:
            restriction = None
            query_string = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
        else:
            restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
            query_string = None
        yield from FindPeople(account=self.account, chunk_size=page_size).call(
                folder=folder,
                additional_fields=additional_fields,
                restriction=restriction,
                order_fields=order_fields,
                shape=shape,
                query_string=query_string,
                depth=depth,
                max_items=max_items,
                offset=offset,
        )

    def get_folder_fields(self, target_cls, is_complex=None):
        return {
            FieldPath(field=f) for f in target_cls.supported_fields(version=self.account.version)
            if is_complex is None or f.is_complex is is_complex
        }

    def _get_target_cls(self):
        # We may have root folders that don't support the same set of fields as normal folders. If there is a mix of
        # both folder types in self.folders, raise an error so we don't risk losing some fields in the query.
        from .base import Folder
        from .roots import RootOfHierarchy
        has_roots = False
        has_non_roots = False
        for f in self.folders:
            if isinstance(f, RootOfHierarchy):
                if has_non_roots:
                    raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders))
                has_roots = True
            else:
                if has_roots:
                    raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders))
                has_non_roots = True
        return RootOfHierarchy if has_roots else Folder

    def _get_default_traversal_depth(self, traversal_attr):
        unique_depths = {getattr(f, traversal_attr) for f in self.folders}
        if len(unique_depths) == 1:
            return unique_depths.pop()
        raise ValueError(
            'Folders in this collection do not have a common %s value. You need to define an explicit traversal depth'
            'with QuerySet.depth() (values: %s)' % (traversal_attr, unique_depths)
        )

    def _get_default_item_traversal_depth(self):
        # When searching folders, some folders require 'Shallow' and others 'Associated' traversal depth.
        return self._get_default_traversal_depth('DEFAULT_ITEM_TRAVERSAL_DEPTH')

    def _get_default_folder_traversal_depth(self):
        # When searching folders, some folders require 'Shallow' and others 'Deep' traversal depth.
        return self._get_default_traversal_depth('DEFAULT_FOLDER_TRAVERSAL_DEPTH')

    def resolve(self):
        # Looks up the folders or folder IDs in the collection and returns full Folder instances with all fields set.
        from .base import BaseFolder
        resolveable_folders = []
        for f in self.folders:
            if isinstance(f, BaseFolder) and not f.get_folder_allowed:
                log.debug('GetFolder not allowed on folder %s. Non-complex fields must be fetched with FindFolder', f)
                yield f
            else:
                resolveable_folders.append(f)
        # Fetch all properties for the remaining folders of folder IDs
        additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=None)
        yield from self.__class__(account=self.account, folders=resolveable_folders).get_folders(
                additional_fields=additional_fields
        )

    @require_account
    def find_folders(self, q=None, shape=ID_ONLY, depth=None, additional_fields=None, page_size=None, max_items=None,
                     offset=0):
        # 'depth' controls whether to return direct children or recurse into sub-folders
        from .base import BaseFolder, Folder
        if q is None:
            q = Q()
        if not self.folders:
            log.debug('Folder list is empty')
            return
        if q.is_never():
            log.debug('Query will never return results')
            return
        if q.is_empty():
            restriction = None
        else:
            restriction = Restriction(q, folders=self.folders, applies_to=Restriction.FOLDERS)
        if shape not in SHAPE_CHOICES:
            raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
        if depth is None:
            depth = self._get_default_folder_traversal_depth()
        if depth not in FOLDER_TRAVERSAL_CHOICES:
            raise ValueError("'depth' %s must be one of %s" % (depth, FOLDER_TRAVERSAL_CHOICES))
        if additional_fields is None:
            # Default to all non-complex properties. Subfolders will always be of class Folder
            additional_fields = self.get_folder_fields(target_cls=Folder, is_complex=False)
        else:
            for f in additional_fields:
                if f.field.is_complex:
                    raise ValueError("find_folders() does not support field '%s'. Use get_folders()." % f.field.name)

        # Add required fields
        additional_fields.update(
            (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
        )

        yield from FindFolder(account=self.account, chunk_size=page_size).call(
                folders=self.folders,
                additional_fields=additional_fields,
                restriction=restriction,
                shape=shape,
                depth=depth,
                max_items=max_items,
                offset=offset,
        )

    def get_folders(self, additional_fields=None):
        # Expand folders with their full set of properties
        from .base import BaseFolder
        if not self.folders:
            log.debug('Folder list is empty')
            return
        if additional_fields is None:
            # Default to all complex properties
            additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=True)

        # Add required fields
        additional_fields.update(
            (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
        )

        yield from GetFolder(account=self.account).call(
                folders=self.folders,
                additional_fields=additional_fields,
                shape=ID_ONLY,
        )

    def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
        if not self.folders:
            log.debug('Folder list is empty')
            return
        yield from SubscribeToPull(account=self.account).call(
            folders=self.folders, event_types=event_types, watermark=watermark, timeout=timeout,
        )

    def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
                          status_frequency=1):
        if not self.folders:
            log.debug('Folder list is empty')
            return
        yield from SubscribeToPush(account=self.account).call(
            folders=self.folders, event_types=event_types, watermark=watermark, status_frequency=status_frequency,
            url=callback_url,
        )

    def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
        if not self.folders:
            log.debug('Folder list is empty')
            return
        yield from SubscribeToStreaming(account=self.account).call(folders=self.folders, event_types=event_types)

    def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
        folder = self._get_single_folder()
        if not folder:
            return
        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 folder.allowed_item_fields(version=self.account.version)}
        else:
            for field in only_fields:
                folder.validate_item_field(field=field, version=self.account.version)
            # Remove ItemId and ChangeKey. We get them unconditionally
            additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}

        svc = SyncFolderItems(account=self.account)
        while True:
            yield from svc.call(
                folder=folder,
                shape=ID_ONLY,
                additional_fields=additional_fields,
                sync_state=sync_state,
                ignore=ignore,
                max_changes_returned=max_changes_returned,
                sync_scope=sync_scope,
            )
            if svc.sync_state == sync_state:
                # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here
                break
            sync_state = svc.sync_state  # Set the new sync state in the next call
            if svc.includes_last_item_in_range:  # Try again if there are more items
                break
        raise SyncCompleted(sync_state=svc.sync_state)

    def sync_hierarchy(self, sync_state=None, only_fields=None):
        folder = self._get_single_folder()
        if not folder:
            return
        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 folder.supported_fields(version=self.account.version)}
        else:
            for f in only_fields:
                folder.validate_field(field=f, version=self.account.version)
            # Remove ItemId and ChangeKey. We get them unconditionally
            additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}

        # Add required fields
        additional_fields.update(
            (FieldPath(field=folder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
        )

        svc = SyncFolderHierarchy(account=self.account)
        while True:
            yield from svc.call(
                folder=folder,
                shape=ID_ONLY,
                additional_fields=additional_fields,
                sync_state=sync_state,
            )
            if svc.sync_state == sync_state:
                # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here
                break
            sync_state = svc.sync_state  # Set the new sync state in the next call
            if svc.includes_last_item_in_range:  # Try again if there are more items
                break
        raise SyncCompleted(sync_state=svc.sync_state)

Ancestors

Class variables

var REQUIRED_FOLDER_FIELDS

Instance variables

var folders
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 supported_item_models
Expand source code
@property
def supported_item_models(self):
    return tuple(item_model for folder in self.folders for item_model in folder.supported_item_models)

Methods

def all(self)
Expand source code
def all(self):
    return QuerySet(self).all()
def allowed_item_fields(self)
Expand source code
def allowed_item_fields(self):
    # Return non-ID fields of all item classes allowed in this folder type
    fields = set()
    for item_model in self.supported_item_models:
        fields.update(set(item_model.supported_fields(version=self.account.version)))
    return fields
def exclude(self, *args, **kwargs)
Expand source code
def exclude(self, *args, **kwargs):
    return QuerySet(self).exclude(*args, **kwargs)
def filter(self, *args, **kwargs)

Find items in the folder(s).

Non-keyword args may be a list of Q instances.

Optional extra keyword arguments follow a Django-like QuerySet filter syntax (see https://docs.djangoproject.com/en/1.10/ref/models/querysets/#field-lookups).

We don't support '__year' and other date-related lookups. We also don't support '__endswith' or '__iendswith'.

We support the additional '__not' lookup in place of Django's exclude() for simple cases. For more complicated cases you need to create a Q object and use ~Q().

Examples

my_account.inbox.filter(datetime_received__gt=EWSDateTime(2016, 1, 1)) my_account.calendar.filter(start__range=(EWSDateTime(2016, 1, 1), EWSDateTime(2017, 1, 1))) my_account.tasks.filter(subject='Hi mom') my_account.tasks.filter(subject__not='Hi mom') my_account.tasks.filter(subject__contains='Foo') my_account.tasks.filter(subject__icontains='foo')

'endswith' and 'iendswith' could be emulated by searching with 'contains' or 'icontains' and then post-processing items. Fetch the field in question with additional_fields and remove items where the search string is not a postfix.

Expand source code
def filter(self, *args, **kwargs):
    """Find items in the folder(s).

    Non-keyword args may be a list of Q instances.

    Optional extra keyword arguments follow a Django-like QuerySet filter syntax (see
       https://docs.djangoproject.com/en/1.10/ref/models/querysets/#field-lookups).

    We don't support '__year' and other date-related lookups. We also don't support '__endswith' or '__iendswith'.

    We support the additional '__not' lookup in place of Django's exclude() for simple cases. For more complicated
    cases you need to create a Q object and use ~Q().

    Examples:

        my_account.inbox.filter(datetime_received__gt=EWSDateTime(2016, 1, 1))
        my_account.calendar.filter(start__range=(EWSDateTime(2016, 1, 1), EWSDateTime(2017, 1, 1)))
        my_account.tasks.filter(subject='Hi mom')
        my_account.tasks.filter(subject__not='Hi mom')
        my_account.tasks.filter(subject__contains='Foo')
        my_account.tasks.filter(subject__icontains='foo')

    'endswith' and 'iendswith' could be emulated by searching with 'contains' or 'icontains' and then
    post-processing items. Fetch the field in question with additional_fields and remove items where the search
    string is not a postfix.
    """
    return QuerySet(self).filter(*args, **kwargs)
def find_folders(self, q=None, shape='IdOnly', depth=None, additional_fields=None, page_size=None, max_items=None, offset=0)
Expand source code
@require_account
def find_folders(self, q=None, shape=ID_ONLY, depth=None, additional_fields=None, page_size=None, max_items=None,
                 offset=0):
    # 'depth' controls whether to return direct children or recurse into sub-folders
    from .base import BaseFolder, Folder
    if q is None:
        q = Q()
    if not self.folders:
        log.debug('Folder list is empty')
        return
    if q.is_never():
        log.debug('Query will never return results')
        return
    if q.is_empty():
        restriction = None
    else:
        restriction = Restriction(q, folders=self.folders, applies_to=Restriction.FOLDERS)
    if shape not in SHAPE_CHOICES:
        raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
    if depth is None:
        depth = self._get_default_folder_traversal_depth()
    if depth not in FOLDER_TRAVERSAL_CHOICES:
        raise ValueError("'depth' %s must be one of %s" % (depth, FOLDER_TRAVERSAL_CHOICES))
    if additional_fields is None:
        # Default to all non-complex properties. Subfolders will always be of class Folder
        additional_fields = self.get_folder_fields(target_cls=Folder, is_complex=False)
    else:
        for f in additional_fields:
            if f.field.is_complex:
                raise ValueError("find_folders() does not support field '%s'. Use get_folders()." % f.field.name)

    # Add required fields
    additional_fields.update(
        (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
    )

    yield from FindFolder(account=self.account, chunk_size=page_size).call(
            folders=self.folders,
            additional_fields=additional_fields,
            restriction=restriction,
            shape=shape,
            depth=depth,
            max_items=max_items,
            offset=offset,
    )
def find_items(self, q, shape='IdOnly', depth=None, additional_fields=None, order_fields=None, calendar_view=None, page_size=None, max_items=None, offset=0)

Private method to call the FindItem service.

:param q: a Q instance containing any restrictions :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is non-null, we always return Item objects. (Default value = ID_ONLY) :param depth: controls the whether to return soft-deleted items or not. (Default value = None) :param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware that complex fields can only be fetched with fetch() (i.e. the GetItem service). :param order_fields: the SortOrder fields, if any (Default value = None) :param calendar_view: a CalendarView instance, if any (Default value = None) :param page_size: the requested number of items per page (Default value = None) :param max_items: the max number of items to return (Default value = None) :param offset: the offset relative to the first item in the item collection (Default value = 0)

:return: a generator for the returned item IDs or items

Expand source code
def find_items(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
               calendar_view=None, page_size=None, max_items=None, offset=0):
    """Private method to call the FindItem service.

    :param q: a Q instance containing any restrictions
    :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is
      non-null, we always return Item objects. (Default value = ID_ONLY)
    :param depth: controls the whether to return soft-deleted items or not. (Default value = None)
    :param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware
      that complex fields can only be fetched with fetch() (i.e. the GetItem service).
    :param order_fields: the SortOrder fields, if any (Default value = None)
    :param calendar_view: a CalendarView instance, if any (Default value = None)
    :param page_size: the requested number of items per page (Default value = None)
    :param max_items: the max number of items to return (Default value = None)
    :param offset: the offset relative to the first item in the item collection (Default value = 0)

    :return: a generator for the returned item IDs or items
    """
    if not self.folders:
        log.debug('Folder list is empty')
        return
    if q.is_never():
        log.debug('Query will never return results')
        return
    if shape not in SHAPE_CHOICES:
        raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
    if depth is None:
        depth = self._get_default_item_traversal_depth()
    if depth not in ITEM_TRAVERSAL_CHOICES:
        raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
    if additional_fields:
        for f in additional_fields:
            self.validate_item_field(field=f, version=self.account.version)
            if f.field.is_complex:
                raise ValueError("find_items() does not support field '%s'. Use fetch() instead" % f.field.name)
    if calendar_view is not None and not isinstance(calendar_view, CalendarView):
        raise ValueError("'calendar_view' %s must be a CalendarView instance" % calendar_view)

    # Build up any restrictions
    if q.is_empty():
        restriction = None
        query_string = None
    elif q.query_string:
        restriction = None
        query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
    else:
        restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
        query_string = None
    log.debug(
        'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
        self.account,
        self.folders,
        shape,
        depth,
        additional_fields,
        restriction.q if restriction else None,
    )
    yield from FindItem(account=self.account, chunk_size=page_size).call(
        folders=self.folders,
        additional_fields=additional_fields,
        restriction=restriction,
        order_fields=order_fields,
        shape=shape,
        query_string=query_string,
        depth=depth,
        calendar_view=calendar_view,
        max_items=calendar_view.max_items if calendar_view else max_items,
        offset=offset,
    )
def find_people(self, q, shape='IdOnly', depth=None, additional_fields=None, order_fields=None, page_size=None, max_items=None, offset=0)

Private method to call the FindPeople service.

:param q: a Q instance containing any restrictions :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is non-null, we always return Persona objects. (Default value = ID_ONLY) :param depth: controls the whether to return soft-deleted items or not. (Default value = None) :param additional_fields: the extra properties we want on the return objects. Default is no properties. :param order_fields: the SortOrder fields, if any (Default value = None) :param page_size: the requested number of items per page (Default value = None) :param max_items: the max number of items to return (Default value = None) :param offset: the offset relative to the first item in the item collection (Default value = 0)

:return: a generator for the returned personas

Expand source code
def find_people(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
                page_size=None, max_items=None, offset=0):
    """Private method to call the FindPeople service.

    :param q: a Q instance containing any restrictions
    :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is
      non-null, we always return Persona objects. (Default value = ID_ONLY)
    :param depth: controls the whether to return soft-deleted items or not. (Default value = None)
    :param additional_fields: the extra properties we want on the return objects. Default is no properties.
    :param order_fields: the SortOrder fields, if any (Default value = None)
    :param page_size: the requested number of items per page (Default value = None)
    :param max_items: the max number of items to return (Default value = None)
    :param offset: the offset relative to the first item in the item collection (Default value = 0)

    :return: a generator for the returned personas
    """
    folder = self._get_single_folder()
    if not folder:
        return
    if q.is_never():
        log.debug('Query will never return results')
        return
    if shape not in SHAPE_CHOICES:
        raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
    if depth is None:
        depth = self._get_default_item_traversal_depth()
    if depth not in ITEM_TRAVERSAL_CHOICES:
        raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
    if additional_fields:
        for f in additional_fields:
            Persona.validate_field(field=f, version=self.account.version)
            if f.field.is_complex:
                raise ValueError("find_people() does not support field '%s'" % f.field.name)

    # Build up any restrictions
    if q.is_empty():
        restriction = None
        query_string = None
    elif q.query_string:
        restriction = None
        query_string = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
    else:
        restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
        query_string = None
    yield from FindPeople(account=self.account, chunk_size=page_size).call(
            folder=folder,
            additional_fields=additional_fields,
            restriction=restriction,
            order_fields=order_fields,
            shape=shape,
            query_string=query_string,
            depth=depth,
            max_items=max_items,
            offset=offset,
    )
def get(self, *args, **kwargs)
Expand source code
def get(self, *args, **kwargs):
    return QuerySet(self).get(*args, **kwargs)
def get_folder_fields(self, target_cls, is_complex=None)
Expand source code
def get_folder_fields(self, target_cls, is_complex=None):
    return {
        FieldPath(field=f) for f in target_cls.supported_fields(version=self.account.version)
        if is_complex is None or f.is_complex is is_complex
    }
def get_folders(self, additional_fields=None)
Expand source code
def get_folders(self, additional_fields=None):
    # Expand folders with their full set of properties
    from .base import BaseFolder
    if not self.folders:
        log.debug('Folder list is empty')
        return
    if additional_fields is None:
        # Default to all complex properties
        additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=True)

    # Add required fields
    additional_fields.update(
        (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
    )

    yield from GetFolder(account=self.account).call(
            folders=self.folders,
            additional_fields=additional_fields,
            shape=ID_ONLY,
    )
def none(self)
Expand source code
def none(self):
    return QuerySet(self).none()
def people(self)
Expand source code
def people(self):
    return QuerySet(self).people()
def resolve(self)
Expand source code
def resolve(self):
    # Looks up the folders or folder IDs in the collection and returns full Folder instances with all fields set.
    from .base import BaseFolder
    resolveable_folders = []
    for f in self.folders:
        if isinstance(f, BaseFolder) and not f.get_folder_allowed:
            log.debug('GetFolder not allowed on folder %s. Non-complex fields must be fetched with FindFolder', f)
            yield f
        else:
            resolveable_folders.append(f)
    # Fetch all properties for the remaining folders of folder IDs
    additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=None)
    yield from self.__class__(account=self.account, folders=resolveable_folders).get_folders(
            additional_fields=additional_fields
    )
def subscribe_to_pull(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, timeout=60)
Expand source code
def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
    if not self.folders:
        log.debug('Folder list is empty')
        return
    yield from SubscribeToPull(account=self.account).call(
        folders=self.folders, event_types=event_types, watermark=watermark, timeout=timeout,
    )
def subscribe_to_push(self, callback_url, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, status_frequency=1)
Expand source code
def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
                      status_frequency=1):
    if not self.folders:
        log.debug('Folder list is empty')
        return
    yield from SubscribeToPush(account=self.account).call(
        folders=self.folders, event_types=event_types, watermark=watermark, status_frequency=status_frequency,
        url=callback_url,
    )
def subscribe_to_streaming(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'))
Expand source code
def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
    if not self.folders:
        log.debug('Folder list is empty')
        return
    yield from SubscribeToStreaming(account=self.account).call(folders=self.folders, event_types=event_types)
def sync_hierarchy(self, sync_state=None, only_fields=None)
Expand source code
def sync_hierarchy(self, sync_state=None, only_fields=None):
    folder = self._get_single_folder()
    if not folder:
        return
    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 folder.supported_fields(version=self.account.version)}
    else:
        for f in only_fields:
            folder.validate_field(field=f, version=self.account.version)
        # Remove ItemId and ChangeKey. We get them unconditionally
        additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}

    # Add required fields
    additional_fields.update(
        (FieldPath(field=folder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
    )

    svc = SyncFolderHierarchy(account=self.account)
    while True:
        yield from svc.call(
            folder=folder,
            shape=ID_ONLY,
            additional_fields=additional_fields,
            sync_state=sync_state,
        )
        if svc.sync_state == sync_state:
            # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here
            break
        sync_state = svc.sync_state  # Set the new sync state in the next call
        if svc.includes_last_item_in_range:  # Try again if there are more items
            break
    raise SyncCompleted(sync_state=svc.sync_state)
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None)
Expand source code
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
    folder = self._get_single_folder()
    if not folder:
        return
    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 folder.allowed_item_fields(version=self.account.version)}
    else:
        for field in only_fields:
            folder.validate_item_field(field=field, version=self.account.version)
        # Remove ItemId and ChangeKey. We get them unconditionally
        additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}

    svc = SyncFolderItems(account=self.account)
    while True:
        yield from svc.call(
            folder=folder,
            shape=ID_ONLY,
            additional_fields=additional_fields,
            sync_state=sync_state,
            ignore=ignore,
            max_changes_returned=max_changes_returned,
            sync_scope=sync_scope,
        )
        if svc.sync_state == sync_state:
            # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here
            break
        sync_state = svc.sync_state  # Set the new sync state in the next call
        if svc.includes_last_item_in_range:  # Try again if there are more items
            break
    raise SyncCompleted(sync_state=svc.sync_state)
def validate_item_field(self, field, version)
Expand source code
def validate_item_field(self, field, version):
    # For each field, check if the field is valid for any of the item models supported by this folder
    for item_model in self.supported_item_models:
        try:
            item_model.validate_field(field=field, version=version)
            break
        except InvalidField:
            continue
    else:
        raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))
def view(self, start, end, max_items=None, *args, **kwargs)

Implement the CalendarView option to FindItem. The difference between 'filter' and 'view' is that 'filter' only returns the master CalendarItem for recurring items, while 'view' unfolds recurring items and returns all CalendarItem occurrences as one would normally expect when presenting a calendar.

Supports the same semantics as filter, except for 'start' and 'end' keyword attributes which are both required and behave differently than filter. Here, they denote the start and end of the timespan of the view. All items the overlap the timespan are returned (items that end exactly on 'start' are also returned, for some reason).

EWS does not allow combining CalendarView with search restrictions (filter and exclude).

'max_items' defines the maximum number of items returned in this view. Optional.

:param start: :param end: :param max_items: (Default value = None) :return:

Expand source code
def view(self, start, end, max_items=None, *args, **kwargs):
    """Implement the CalendarView option to FindItem. The difference between 'filter' and 'view' is that 'filter'
    only returns the master CalendarItem for recurring items, while 'view' unfolds recurring items and returns all
    CalendarItem occurrences as one would normally expect when presenting a calendar.

    Supports the same semantics as filter, except for 'start' and 'end' keyword attributes which are both required
    and behave differently than filter. Here, they denote the start and end of the timespan of the view. All items
    the overlap the timespan are returned (items that end exactly on 'start' are also returned, for some reason).

    EWS does not allow combining CalendarView with search restrictions (filter and exclude).

    'max_items' defines the maximum number of items returned in this view. Optional.

    :param start:
    :param end:
    :param max_items:  (Default value = None)
    :return:
    """
    qs = QuerySet(self).filter(*args, **kwargs)
    qs.calendar_view = CalendarView(start=start, end=end, max_items=max_items)
    return qs
class FolderId (*args, **kwargs)
Expand source code
class FolderId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/folderid"""

    ELEMENT_NAME = 'FolderId'

Ancestors

Subclasses

Class variables

var ELEMENT_NAME

Inherited members

class FolderQuerySet (folder_collection)

A QuerySet-like class for finding subfolders of a folder collection.

Expand source code
class FolderQuerySet:
    """A QuerySet-like class for finding subfolders of a folder collection."""

    def __init__(self, folder_collection):
        from .collections import FolderCollection
        if not isinstance(folder_collection, FolderCollection):
            raise ValueError("'folder_collection' %r must be a FolderCollection instance" % folder_collection)
        self.folder_collection = folder_collection
        self.q = Q()  # Default to no restrictions
        self.only_fields = None
        self._depth = None

    def _copy_cls(self):
        return self.__class__(folder_collection=self.folder_collection)

    def _copy_self(self):
        """Chaining operations must make a copy of self before making any modifications."""
        new_qs = self._copy_cls()
        new_qs.q = deepcopy(self.q)
        new_qs.only_fields = self.only_fields
        new_qs._depth = self._depth
        return new_qs

    def only(self, *args):
        """Restrict the fields returned. 'name' and 'folder_class' are always returned."""
        from .base import Folder
        # Subfolders will always be of class Folder
        all_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=None)
        all_fields.update(Folder.attribute_fields())
        only_fields = []
        for arg in args:
            for field_path in all_fields:
                if field_path.field.name == arg:
                    only_fields.append(field_path)
                    break
            else:
                raise InvalidField("Unknown field %r on folders %s" % (arg, self.folder_collection.folders))
        new_qs = self._copy_self()
        new_qs.only_fields = only_fields
        return new_qs

    def depth(self, depth):
        """Specify the search depth. Possible values are: SHALLOW or DEEP.

        :param depth:
        """
        new_qs = self._copy_self()
        new_qs._depth = depth
        return new_qs

    def get(self, *args, **kwargs):
        """Return the query result as exactly one item. Raises DoesNotExist if there are no results, and
        MultipleObjectsReturned if there are multiple results.
        """
        from .collections import FolderCollection
        if not args and set(kwargs) in ({'id'}, {'id', 'changekey'}):
            folders = list(FolderCollection(
                account=self.folder_collection.account, folders=[FolderId(**kwargs)]
            ).resolve())
        elif args or kwargs:
            folders = list(self.filter(*args, **kwargs))
        else:
            folders = list(self.all())
        if not folders:
            raise DoesNotExist('Could not find a child folder matching the query')
        if len(folders) != 1:
            raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders)
        f = folders[0]
        if isinstance(f, Exception):
            raise f
        return f

    def all(self):
        """ """
        new_qs = self._copy_self()
        return new_qs

    def filter(self, *args, **kwargs):
        """Add restrictions to the folder search."""
        new_qs = self._copy_self()
        q = Q(*args, **kwargs)
        new_qs.q = new_qs.q & q
        return new_qs

    def __iter__(self):
        return self._query()

    def _query(self):
        from .base import Folder
        from .collections import FolderCollection
        if self.only_fields is None:
            # Subfolders will always be of class Folder
            non_complex_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=False)
            complex_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=True)
        else:
            non_complex_fields = {f for f in self.only_fields if not f.field.is_complex}
            complex_fields = {f for f in self.only_fields if f.field.is_complex}

        # First, fetch all non-complex fields using FindFolder. We do this because some folders do not support
        # GetFolder but we still want to get as much information as possible.
        folders = self.folder_collection.find_folders(q=self.q, depth=self._depth, additional_fields=non_complex_fields)
        if not complex_fields:
            yield from folders
            return

        # Fetch all properties for the found folders
        resolveable_folders = []
        for f in folders:
            if not f.get_folder_allowed:
                log.debug('GetFolder not allowed on folder %s. Non-complex fields must be fetched with FindFolder', f)
                yield f
            else:
                resolveable_folders.append(f)

        # Get the complex fields using GetFolder, for the folders that support it, and add the extra field values
        complex_folders = FolderCollection(
            account=self.folder_collection.account, folders=resolveable_folders
        ).get_folders(additional_fields=complex_fields)
        for f, complex_f in zip(resolveable_folders, complex_folders):
            if isinstance(f, Exception):
                yield f
                continue
            if isinstance(complex_f, Exception):
                yield complex_f
                continue
            # Add the extra field values to the folders we fetched with find_folders()
            if f.__class__ != complex_f.__class__:
                raise ValueError('Type mismatch: %s vs %s' % (f, complex_f))
            for complex_field in complex_fields:
                field_name = complex_field.field.name
                setattr(f, field_name, getattr(complex_f, field_name))
            yield f

Subclasses

Methods

def all(self)
Expand source code
def all(self):
    """ """
    new_qs = self._copy_self()
    return new_qs
def depth(self, depth)

Specify the search depth. Possible values are: SHALLOW or DEEP.

:param depth:

Expand source code
def depth(self, depth):
    """Specify the search depth. Possible values are: SHALLOW or DEEP.

    :param depth:
    """
    new_qs = self._copy_self()
    new_qs._depth = depth
    return new_qs
def filter(self, *args, **kwargs)

Add restrictions to the folder search.

Expand source code
def filter(self, *args, **kwargs):
    """Add restrictions to the folder search."""
    new_qs = self._copy_self()
    q = Q(*args, **kwargs)
    new_qs.q = new_qs.q & q
    return new_qs
def get(self, *args, **kwargs)

Return the query result as exactly one item. Raises DoesNotExist if there are no results, and MultipleObjectsReturned if there are multiple results.

Expand source code
def get(self, *args, **kwargs):
    """Return the query result as exactly one item. Raises DoesNotExist if there are no results, and
    MultipleObjectsReturned if there are multiple results.
    """
    from .collections import FolderCollection
    if not args and set(kwargs) in ({'id'}, {'id', 'changekey'}):
        folders = list(FolderCollection(
            account=self.folder_collection.account, folders=[FolderId(**kwargs)]
        ).resolve())
    elif args or kwargs:
        folders = list(self.filter(*args, **kwargs))
    else:
        folders = list(self.all())
    if not folders:
        raise DoesNotExist('Could not find a child folder matching the query')
    if len(folders) != 1:
        raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders)
    f = folders[0]
    if isinstance(f, Exception):
        raise f
    return f
def only(self, *args)

Restrict the fields returned. 'name' and 'folder_class' are always returned.

Expand source code
def only(self, *args):
    """Restrict the fields returned. 'name' and 'folder_class' are always returned."""
    from .base import Folder
    # Subfolders will always be of class Folder
    all_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=None)
    all_fields.update(Folder.attribute_fields())
    only_fields = []
    for arg in args:
        for field_path in all_fields:
            if field_path.field.name == arg:
                only_fields.append(field_path)
                break
        else:
            raise InvalidField("Unknown field %r on folders %s" % (arg, self.folder_collection.folders))
    new_qs = self._copy_self()
    new_qs.only_fields = only_fields
    return new_qs
class FreebusyData (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class FreebusyData(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Freebusy Data',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class Friends (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Friends(NonDeletableFolderMixin, Contacts):
    CONTAINER_CLASS = 'IPF.Note'

    LOCALIZED_NAMES = {
        'de_DE': ('Bekannte',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class GALContacts (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class GALContacts(NonDeletableFolderMixin, Contacts):
    DISTINGUISHED_FOLDER_ID = None
    CONTAINER_CLASS = 'IPF.Contact.GalContacts'

    LOCALIZED_NAMES = {
        None: ('GAL Contacts',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class GraphAnalytics (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class GraphAnalytics(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.StoreItem.GraphAnalytics'
    LOCALIZED_NAMES = {
        None: ('GraphAnalytics',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class IMContactList (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class IMContactList(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Contact.MOC.ImContactList'
    DISTINGUISHED_FOLDER_ID = 'imcontactlist'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class Inbox (**kwargs)
Expand source code
class Inbox(Messages):
    DISTINGUISHED_FOLDER_ID = 'inbox'

    LOCALIZED_NAMES = {
        'da_DK': ('Indbakke',),
        'de_DE': ('Posteingang',),
        'en_US': ('Inbox',),
        'es_ES': ('Bandeja de entrada',),
        'fr_CA': ('Boîte de réception',),
        'nl_NL': ('Postvak IN',),
        'ru_RU': ('Входящие',),
        'sv_SE': ('Inkorgen',),
        'zh_CN': ('收件箱',),
    }

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class Journal (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class Journal(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Journal'
    DISTINGUISHED_FOLDER_ID = 'journal'

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID

Inherited members

class JunkEmail (**kwargs)
Expand source code
class JunkEmail(Messages):
    DISTINGUISHED_FOLDER_ID = 'junkemail'

    LOCALIZED_NAMES = {
        'da_DK': ('Uønsket e-mail',),
        'de_DE': ('Junk-E-Mail',),
        'en_US': ('Junk E-mail',),
        'es_ES': ('Correo no deseado',),
        'fr_CA': ('Courrier indésirables',),
        'nl_NL': ('Ongewenste e-mail',),
        'ru_RU': ('Нежелательная почта',),
        'sv_SE': ('Skräppost',),
        'zh_CN': ('垃圾邮件',),
    }

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class LocalFailures (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class LocalFailures(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'localfailures'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class Location (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Location(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Location',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class MailboxAssociations (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class MailboxAssociations(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('MailboxAssociations',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class Messages (**kwargs)
Expand source code
class Messages(Folder):
    CONTAINER_CLASS = 'IPF.Note'
    supported_item_models = (Message, MeetingRequest, MeetingResponse, MeetingCancellation)

Ancestors

Subclasses

Class variables

var CONTAINER_CLASS
var supported_item_models

Inherited members

class MsgFolderRoot (**kwargs)

Also known as the 'Top of Information Store' folder.

Expand source code
class MsgFolderRoot(WellknownFolder):
    """Also known as the 'Top of Information Store' folder."""

    DISTINGUISHED_FOLDER_ID = 'msgfolderroot'
    LOCALIZED_NAMES = {
        'zh_CN': ('信息存储顶部',),
    }

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class MyContacts (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class MyContacts(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Note'
    DISTINGUISHED_FOLDER_ID = 'mycontacts'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class MyContactsExtended (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class MyContactsExtended(NonDeletableFolderMixin, Contacts):
    CONTAINER_CLASS = 'IPF.Note'
    LOCALIZED_NAMES = {
        None: ('MyContactsExtended',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class NonDeletableFolderMixin

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class NonDeletableFolderMixin:
    """A mixin for non-wellknown folders than that are not deletable."""

    @property
    def is_deletable(self):
        return False

Subclasses

Instance variables

var is_deletable
Expand source code
@property
def is_deletable(self):
    return False
class Notes (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class Notes(WellknownFolder):
    CONTAINER_CLASS = 'IPF.StickyNote'
    DISTINGUISHED_FOLDER_ID = 'notes'
    LOCALIZED_NAMES = {
        'da_DK': ('Noter',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class OrganizationalContacts (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class OrganizationalContacts(NonDeletableFolderMixin, Contacts):
    DISTINGUISHED_FOLDER_ID = None
    CONTAINTER_CLASS = 'IPF.Contact.OrganizationalContacts'
    LOCALIZED_NAMES = {
        None: ('Organizational Contacts',),
    }

Ancestors

Class variables

var CONTAINTER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class Outbox (**kwargs)
Expand source code
class Outbox(Messages):
    DISTINGUISHED_FOLDER_ID = 'outbox'

    LOCALIZED_NAMES = {
        'da_DK': ('Udbakke',),
        'de_DE': ('Postausgang',),
        'en_US': ('Outbox',),
        'es_ES': ('Bandeja de salida',),
        'fr_CA': (u"Boîte d'envoi",),
        'nl_NL': ('Postvak UIT',),
        'ru_RU': ('Исходящие',),
        'sv_SE': ('Utkorgen',),
        'zh_CN': ('发件箱',),
    }

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class ParkedMessages (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class ParkedMessages(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = None
    LOCALIZED_NAMES = {
        None: ('ParkedMessages',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class PassThroughSearchResults (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class PassThroughSearchResults(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.StoreItem.PassThroughSearchResults'
    LOCALIZED_NAMES = {
        None: ('Pass-Through Search Results',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class PdpProfileV2Secured (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class PdpProfileV2Secured(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.StoreItem.PdpProfileSecured'
    LOCALIZED_NAMES = {
        None: ('PdpProfileV2Secured',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class PeopleCentricConversationBuddies (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class PeopleCentricConversationBuddies(NonDeletableFolderMixin, Contacts):
    DISTINGUISHED_FOLDER_ID = None
    CONTAINTER_CLASS = 'IPF.Contact.PeopleCentricConversationBuddies'
    LOCALIZED_NAMES = {
        None: ('PeopleCentricConversation Buddies',),
    }

Ancestors

Class variables

var CONTAINTER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class PeopleConnect (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class PeopleConnect(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'peopleconnect'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class PublicFoldersRoot (**kwargs)

The root of the public folders hierarchy. Not available on all mailboxes.

Expand source code
class PublicFoldersRoot(RootOfHierarchy):
    """The root of the public folders hierarchy. Not available on all mailboxes."""

    DISTINGUISHED_FOLDER_ID = 'publicfoldersroot'
    DEFAULT_FOLDER_TRAVERSAL_DEPTH = SHALLOW
    supported_from = EXCHANGE_2007_SP1

    def get_children(self, folder):
        # EWS does not allow deep traversal of public folders, so self._folders_map will only populate the top-level
        # subfolders. To traverse public folders at arbitrary depth, we need to get child folders on demand.

        # Let's check if this folder already has any cached children. If so, assume we can just return those.
        children = list(super().get_children(folder=folder))
        if children:
            # Return a generator like our parent does
            yield from children
            return

        # Also return early if the server told us that there are no child folders.
        if folder.child_folder_count == 0:
            return

        children_map = {}
        try:
            for f in SingleFolderQuerySet(account=self.account, folder=folder).depth(
                    self.DEFAULT_FOLDER_TRAVERSAL_DEPTH
            ).all():
                if isinstance(f, Exception):
                    raise f
                children_map[f.id] = f
        except ErrorAccessDenied:
            # No access to this folder
            pass

        # Let's update the cache atomically, to avoid partial reads of the cache.
        with self._subfolders_lock:
            self._subfolders.update(children_map)

        # Child folders have been cached now. Try super().get_children() again.
        yield from super().get_children(folder=folder)

Ancestors

Class variables

var DEFAULT_FOLDER_TRAVERSAL_DEPTH
var DISTINGUISHED_FOLDER_ID
var supported_from

Methods

def get_children(self, folder)
Expand source code
def get_children(self, folder):
    # EWS does not allow deep traversal of public folders, so self._folders_map will only populate the top-level
    # subfolders. To traverse public folders at arbitrary depth, we need to get child folders on demand.

    # Let's check if this folder already has any cached children. If so, assume we can just return those.
    children = list(super().get_children(folder=folder))
    if children:
        # Return a generator like our parent does
        yield from children
        return

    # Also return early if the server told us that there are no child folders.
    if folder.child_folder_count == 0:
        return

    children_map = {}
    try:
        for f in SingleFolderQuerySet(account=self.account, folder=folder).depth(
                self.DEFAULT_FOLDER_TRAVERSAL_DEPTH
        ).all():
            if isinstance(f, Exception):
                raise f
            children_map[f.id] = f
    except ErrorAccessDenied:
        # No access to this folder
        pass

    # Let's update the cache atomically, to avoid partial reads of the cache.
    with self._subfolders_lock:
        self._subfolders.update(children_map)

    # Child folders have been cached now. Try super().get_children() again.
    yield from super().get_children(folder=folder)

Inherited members

class QuickContacts (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class QuickContacts(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Contact.MOC.QuickContacts'
    DISTINGUISHED_FOLDER_ID = 'quickcontacts'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class RSSFeeds (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class RSSFeeds(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.Note.OutlookHomepage'
    LOCALIZED_NAMES = {
        None: ('RSS Feeds',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class RecipientCache (**kwargs)
Expand source code
class RecipientCache(Contacts):
    DISTINGUISHED_FOLDER_ID = 'recipientcache'
    CONTAINER_CLASS = 'IPF.Contact.RecipientCache'
    supported_from = EXCHANGE_2013

    LOCALIZED_NAMES = {}

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES
var supported_from

Inherited members

class RecoverableItemsDeletions (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class RecoverableItemsDeletions(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'recoverableitemsdeletions'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class RecoverableItemsPurges (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class RecoverableItemsPurges(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'recoverableitemspurges'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class RecoverableItemsRoot (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class RecoverableItemsRoot(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'recoverableitemsroot'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class RecoverableItemsVersions (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class RecoverableItemsVersions(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'recoverableitemsversions'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class Reminders (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Reminders(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'Outlook.Reminder'
    LOCALIZED_NAMES = {
        'da_DK': ('Påmindelser',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class Root (**kwargs)

The root of the standard folder hierarchy.

Expand source code
class Root(RootOfHierarchy):
    """The root of the standard folder hierarchy."""

    DISTINGUISHED_FOLDER_ID = 'root'
    WELLKNOWN_FOLDERS = WELLKNOWN_FOLDERS_IN_ROOT

    @property
    def tois(self):
        # 'Top of Information Store' is a folder available in some Exchange accounts. It usually contains the
        # distinguished folders belonging to the account (inbox, calendar, trash etc.).
        return self.get_default_folder(MsgFolderRoot)

    def get_default_folder(self, folder_cls):
        try:
            return super().get_default_folder(folder_cls)
        except MISSING_FOLDER_ERRORS:
            pass

        # Try to pick a suitable default folder. we do this by:
        #  1. Searching the full folder list for a folder with the distinguished folder name
        #  2. Searching TOIS for a direct child folder of the same type that is marked as distinguished
        #  3. Searching TOIS for a direct child folder of the same type that is has a localized name
        #  4. Searching root for a direct child folder of the same type that is marked as distinguished
        #  5. Searching root for a direct child folder of the same type that is has a localized name
        log.debug('Searching default %s folder in full folder list', folder_cls)

        for f in self._folders_map.values():
            # Require exact class to not match e.g. RecipientCache instead of Contacts
            if f.__class__ == folder_cls and f.has_distinguished_name:
                log.debug('Found cached %s folder with default distinguished name', folder_cls)
                return f

        # Try direct children of TOIS first, unless we're trying to get the TOIS folder. TOIS might not exist.
        if folder_cls != MsgFolderRoot:
            try:
                return self._get_candidate(folder_cls=folder_cls, folder_coll=self.tois.children)
            except MISSING_FOLDER_ERRORS:
                # No candidates, or TOIS does not exist, or we don't have access
                pass

        # No candidates in TOIS. Try direct children of root.
        return self._get_candidate(folder_cls=folder_cls, folder_coll=self.children)

    def _get_candidate(self, folder_cls, folder_coll):
        # Get a single the folder of the same type in folder_coll
        same_type = [f for f in folder_coll if f.__class__ == folder_cls]
        are_distinguished = [f for f in same_type if f.is_distinguished]
        if are_distinguished:
            candidates = are_distinguished
        else:
            candidates = [f for f in same_type if f.name.lower() in folder_cls.localized_names(self.account.locale)]
        if candidates:
            if len(candidates) > 1:
                raise ValueError(
                    'Multiple possible default %s folders: %s' % (folder_cls, [f.name for f in candidates])
                )
            if candidates[0].is_distinguished:
                log.debug('Found cached distinguished %s folder', folder_cls)
            else:
                log.debug('Found cached %s folder with localized name', folder_cls)
            return candidates[0]
        raise ErrorFolderNotFound('No usable default %s folders' % folder_cls)

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var WELLKNOWN_FOLDERS

Instance variables

var tois
Expand source code
@property
def tois(self):
    # 'Top of Information Store' is a folder available in some Exchange accounts. It usually contains the
    # distinguished folders belonging to the account (inbox, calendar, trash etc.).
    return self.get_default_folder(MsgFolderRoot)

Inherited members

class RootOfHierarchy (**kwargs)

Base class for folders that implement the root of a folder hierarchy.

Expand source code
class RootOfHierarchy(BaseFolder, metaclass=EWSMeta):
    """Base class for folders that implement the root of a folder hierarchy."""

    # A list of wellknown, or "distinguished", folders that are belong in this folder hierarchy. See
    # https://docs.microsoft.com/en-us/dotnet/api/microsoft.exchange.webservices.data.wellknownfoldername
    # and https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid
    # 'RootOfHierarchy' subclasses must not be in this list.
    WELLKNOWN_FOLDERS = []

    _subfolders_lock = Lock()

    # This folder type also has 'folder:PermissionSet' on some server versions, but requesting it sometimes causes
    # 'ErrorAccessDenied', as reported by some users. Ignore it entirely for root folders - it's usefulness is
    # deemed minimal at best.
    effective_rights = EffectiveRightsField(field_uri='folder:EffectiveRights', is_read_only=True,
                                            supported_from=EXCHANGE_2007_SP1)

    __slots__ = '_account', '_subfolders'

    # A special folder that acts as the top of a folder hierarchy. Finds and caches subfolders at arbitrary depth.
    def __init__(self, **kwargs):
        self._account = kwargs.pop('account', None)  # A pointer back to the account holding the folder hierarchy
        super().__init__(**kwargs)
        self._subfolders = None  # See self._folders_map()

    @property
    def account(self):
        return self._account

    @property
    def root(self):
        return self

    @property
    def parent(self):
        return None

    @classmethod
    def register(cls, *args, **kwargs):
        if cls is not RootOfHierarchy:
            raise TypeError('For folder roots, custom fields must be registered on the RootOfHierarchy class')
        return super().register(*args, **kwargs)

    @classmethod
    def deregister(cls, *args, **kwargs):
        if cls is not RootOfHierarchy:
            raise TypeError('For folder roots, custom fields must be registered on the RootOfHierarchy class')
        return super().deregister(*args, **kwargs)

    def get_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        return self._folders_map.get(folder.id, None)

    def add_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        self._folders_map[folder.id] = folder

    def update_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        self._folders_map[folder.id] = folder

    def remove_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        try:
            del self._folders_map[folder.id]
        except KeyError:
            pass

    def clear_cache(self):
        with self._subfolders_lock:
            self._subfolders = None

    def get_children(self, folder):
        for f in self._folders_map.values():
            if not f.parent:
                continue
            if f.parent.id == folder.id:
                yield f

    @classmethod
    def get_distinguished(cls, account):
        """Get the distinguished folder for this folder class.

        :param account:
        """
        if not cls.DISTINGUISHED_FOLDER_ID:
            raise ValueError('Class %s must have a DISTINGUISHED_FOLDER_ID value' % cls)
        try:
            return cls.resolve(
                account=account,
                folder=cls(account=account, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
            )
        except MISSING_FOLDER_ERRORS:
            raise ErrorFolderNotFound('Could not find distinguished folder %s' % cls.DISTINGUISHED_FOLDER_ID)

    def get_default_folder(self, folder_cls):
        """Return the distinguished folder instance of type folder_cls belonging to this account. If no distinguished
        folder was found, try as best we can to return the default folder of type 'folder_cls'
        """
        if not folder_cls.DISTINGUISHED_FOLDER_ID:
            raise ValueError("'folder_cls' %s must have a DISTINGUISHED_FOLDER_ID value" % folder_cls)
        # Use cached distinguished folder instance, but only if cache has already been prepped. This is an optimization
        # for accessing e.g. 'account.contacts' without fetching all folders of the account.
        if self._subfolders is not None:
            for f in self._folders_map.values():
                # Require exact class, to not match subclasses, e.g. RecipientCache instead of Contacts
                if f.__class__ == folder_cls and f.is_distinguished:
                    log.debug('Found cached distinguished %s folder', folder_cls)
                    return f
        try:
            log.debug('Requesting distinguished %s folder explicitly', folder_cls)
            return folder_cls.get_distinguished(root=self)
        except ErrorAccessDenied:
            # Maybe we just don't have GetFolder access? Try FindItems instead
            log.debug('Testing default %s folder with FindItem', folder_cls)
            fld = folder_cls(root=self, name=folder_cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
            fld.test_access()
            return self._folders_map.get(fld.id, fld)  # Use cached instance if available
        except MISSING_FOLDER_ERRORS:
            # The Exchange server does not return a distinguished folder of this type
            pass
        raise ErrorFolderNotFound('No usable default %s folders' % folder_cls)

    @property
    def _folders_map(self):
        if self._subfolders is not None:
            return self._subfolders

        with self._subfolders_lock:
            # Map root, and all subfolders of root, at arbitrary depth by folder ID. First get distinguished folders,
            # so we are sure to apply the correct Folder class, then fetch all subfolders of this root.
            folders_map = {self.id: self}
            distinguished_folders = [
                cls(root=self, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
                for cls in self.WELLKNOWN_FOLDERS
                if cls.get_folder_allowed and cls.supports_version(self.account.version)
            ]
            for f in FolderCollection(account=self.account, folders=distinguished_folders).resolve():
                if isinstance(f, MISSING_FOLDER_ERRORS):
                    # This is just a distinguished folder the server does not have
                    continue
                if isinstance(f, ErrorInvalidOperation):
                    # This is probably a distinguished folder the server does not have. We previously tested the exact
                    # error message (f.value), but some Exchange servers return localized error messages, so that's not
                    # possible to do reliably.
                    continue
                if isinstance(f, ErrorAccessDenied):
                    # We may not have GetFolder access, either to this folder or at all
                    continue
                if isinstance(f, Exception):
                    raise f
                folders_map[f.id] = f
            for f in SingleFolderQuerySet(account=self.account, folder=self).depth(
                    self.DEFAULT_FOLDER_TRAVERSAL_DEPTH
            ).all():
                if isinstance(f, ErrorAccessDenied):
                    # We may not have FindFolder access, or GetFolder access, either to this folder or at all
                    continue
                if isinstance(f, Exception):
                    raise f
                if f.id in folders_map:
                    # Already exists. Probably a distinguished folder
                    continue
                folders_map[f.id] = f
            self._subfolders = folders_map
            return folders_map

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = cls._kwargs_from_elem(elem=elem, account=account)
        cls._clear(elem)
        return cls(account=account, **kwargs)

    @classmethod
    def folder_cls_from_folder_name(cls, folder_name, locale):
        """Return the folder class that matches a localized folder name.

        :param folder_name:
        :param locale: a string, e.g. 'da_DK'
        """
        for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS:
            if folder_name.lower() in folder_cls.localized_names(locale):
                return folder_cls
        raise KeyError()

    def __repr__(self):
        # Let's not create an infinite loop when printing self.root
        return self.__class__.__name__ + \
               repr((self.account, '[self]', self.name, self.total_count, self.unread_count, self.child_folder_count,
                     self.folder_class, self.id, self.changekey))

Ancestors

Subclasses

Class variables

var FIELDS
var WELLKNOWN_FOLDERS

Static methods

def folder_cls_from_folder_name(folder_name, locale)

Return the folder class that matches a localized folder name.

:param folder_name: :param locale: a string, e.g. 'da_DK'

Expand source code
@classmethod
def folder_cls_from_folder_name(cls, folder_name, locale):
    """Return the folder class that matches a localized folder name.

    :param folder_name:
    :param locale: a string, e.g. 'da_DK'
    """
    for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS:
        if folder_name.lower() in folder_cls.localized_names(locale):
            return folder_cls
    raise KeyError()
def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    kwargs = cls._kwargs_from_elem(elem=elem, account=account)
    cls._clear(elem)
    return cls(account=account, **kwargs)
def get_distinguished(account)

Get the distinguished folder for this folder class.

:param account:

Expand source code
@classmethod
def get_distinguished(cls, account):
    """Get the distinguished folder for this folder class.

    :param account:
    """
    if not cls.DISTINGUISHED_FOLDER_ID:
        raise ValueError('Class %s must have a DISTINGUISHED_FOLDER_ID value' % cls)
    try:
        return cls.resolve(
            account=account,
            folder=cls(account=account, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
        )
    except MISSING_FOLDER_ERRORS:
        raise ErrorFolderNotFound('Could not find distinguished folder %s' % cls.DISTINGUISHED_FOLDER_ID)

Instance variables

var account
Expand source code
@property
def account(self):
    return self._account
var effective_rights
var parent
Expand source code
@property
def parent(self):
    return None
var root
Expand source code
@property
def root(self):
    return self

Methods

def add_folder(self, folder)
Expand source code
def add_folder(self, folder):
    if not folder.id:
        raise ValueError("'folder' must have an ID")
    self._folders_map[folder.id] = folder
def clear_cache(self)
Expand source code
def clear_cache(self):
    with self._subfolders_lock:
        self._subfolders = None
def get_children(self, folder)
Expand source code
def get_children(self, folder):
    for f in self._folders_map.values():
        if not f.parent:
            continue
        if f.parent.id == folder.id:
            yield f
def get_default_folder(self, folder_cls)

Return the distinguished folder instance of type folder_cls belonging to this account. If no distinguished folder was found, try as best we can to return the default folder of type 'folder_cls'

Expand source code
def get_default_folder(self, folder_cls):
    """Return the distinguished folder instance of type folder_cls belonging to this account. If no distinguished
    folder was found, try as best we can to return the default folder of type 'folder_cls'
    """
    if not folder_cls.DISTINGUISHED_FOLDER_ID:
        raise ValueError("'folder_cls' %s must have a DISTINGUISHED_FOLDER_ID value" % folder_cls)
    # Use cached distinguished folder instance, but only if cache has already been prepped. This is an optimization
    # for accessing e.g. 'account.contacts' without fetching all folders of the account.
    if self._subfolders is not None:
        for f in self._folders_map.values():
            # Require exact class, to not match subclasses, e.g. RecipientCache instead of Contacts
            if f.__class__ == folder_cls and f.is_distinguished:
                log.debug('Found cached distinguished %s folder', folder_cls)
                return f
    try:
        log.debug('Requesting distinguished %s folder explicitly', folder_cls)
        return folder_cls.get_distinguished(root=self)
    except ErrorAccessDenied:
        # Maybe we just don't have GetFolder access? Try FindItems instead
        log.debug('Testing default %s folder with FindItem', folder_cls)
        fld = folder_cls(root=self, name=folder_cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
        fld.test_access()
        return self._folders_map.get(fld.id, fld)  # Use cached instance if available
    except MISSING_FOLDER_ERRORS:
        # The Exchange server does not return a distinguished folder of this type
        pass
    raise ErrorFolderNotFound('No usable default %s folders' % folder_cls)
def get_folder(self, folder)
Expand source code
def get_folder(self, folder):
    if not folder.id:
        raise ValueError("'folder' must have an ID")
    return self._folders_map.get(folder.id, None)
def remove_folder(self, folder)
Expand source code
def remove_folder(self, folder):
    if not folder.id:
        raise ValueError("'folder' must have an ID")
    try:
        del self._folders_map[folder.id]
    except KeyError:
        pass
def update_folder(self, folder)
Expand source code
def update_folder(self, folder):
    if not folder.id:
        raise ValueError("'folder' must have an ID")
    self._folders_map[folder.id] = folder

Inherited members

class Schedule (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Schedule(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Schedule',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class SearchFolders (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class SearchFolders(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'searchfolders'

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID

Inherited members

class SentItems (**kwargs)
Expand source code
class SentItems(Messages):
    DISTINGUISHED_FOLDER_ID = 'sentitems'

    LOCALIZED_NAMES = {
        'da_DK': ('Sendt post',),
        'de_DE': ('Gesendete Elemente',),
        'en_US': ('Sent Items',),
        'es_ES': ('Elementos enviados',),
        'fr_CA': ('Éléments envoyés',),
        'nl_NL': ('Verzonden items',),
        'ru_RU': ('Отправленные',),
        'sv_SE': ('Skickat',),
        'zh_CN': ('已发送邮件',),
    }

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class ServerFailures (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ServerFailures(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'serverfailures'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class Sharing (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Sharing(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.Note'
    LOCALIZED_NAMES = {
        None: ('Sharing',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class Shortcuts (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Shortcuts(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Shortcuts',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class Signal (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Signal(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.StoreItem.Signal'
    LOCALIZED_NAMES = {
        None: ('Signal',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class SingleFolderQuerySet (account, folder)

A helper class with simpler argument types.

Expand source code
class SingleFolderQuerySet(FolderQuerySet):
    """A helper class with simpler argument types."""

    def __init__(self, account, folder):
        from .collections import FolderCollection
        folder_collection = FolderCollection(account=account, folders=[folder])
        super().__init__(folder_collection=folder_collection)

    def _copy_cls(self):
        return self.__class__(account=self.folder_collection.account, folder=self.folder_collection.folders[0])

    def resolve(self):
        folders = list(self.folder_collection.resolve())
        if not folders:
            raise DoesNotExist('Could not find a folder matching the query')
        if len(folders) != 1:
            raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders)
        f = folders[0]
        if isinstance(f, Exception):
            raise f
        return f

Ancestors

Methods

def resolve(self)
Expand source code
def resolve(self):
    folders = list(self.folder_collection.resolve())
    if not folders:
        raise DoesNotExist('Could not find a folder matching the query')
    if len(folders) != 1:
        raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders)
    f = folders[0]
    if isinstance(f, Exception):
        raise f
    return f

Inherited members

class SmsAndChatsSync (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class SmsAndChatsSync(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.SmsAndChatsSync'
    LOCALIZED_NAMES = {
        None: ('SmsAndChatsSync',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class SpoolerQueue (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class SpoolerQueue(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Spooler Queue',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class SyncIssues (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class SyncIssues(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Note'
    DISTINGUISHED_FOLDER_ID = 'syncissues'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class System (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class System(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('System',),
    }
    get_folder_allowed = False

Ancestors

Class variables

var LOCALIZED_NAMES
var get_folder_allowed

Inherited members

class Tasks (**kwargs)
Expand source code
class Tasks(Folder):
    DISTINGUISHED_FOLDER_ID = 'tasks'
    CONTAINER_CLASS = 'IPF.Task'
    supported_item_models = (Task,)

    LOCALIZED_NAMES = {
        'da_DK': ('Opgaver',),
        'de_DE': ('Aufgaben',),
        'en_US': ('Tasks',),
        'es_ES': ('Tareas',),
        'fr_CA': ('Tâches',),
        'nl_NL': ('Taken',),
        'ru_RU': ('Задачи',),
        'sv_SE': ('Uppgifter',),
        'zh_CN': ('任务',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES
var supported_item_models

Inherited members

class TemporarySaves (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class TemporarySaves(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('TemporarySaves',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class ToDoSearch (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ToDoSearch(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Task'
    DISTINGUISHED_FOLDER_ID = 'todosearch'
    supported_from = EXCHANGE_2013

    LOCALIZED_NAMES = {
        None: ('To-Do Search',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES
var supported_from

Inherited members

class Views (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Views(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Views',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class VoiceMail (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class VoiceMail(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'voicemail'
    CONTAINER_CLASS = 'IPF.Note.Microsoft.Voicemail'
    LOCALIZED_NAMES = {
        None: ('Voice Mail',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class WellknownFolder (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class WellknownFolder(Folder):
    """A base class to use until we have a more specific folder implementation for this folder."""

    supported_item_models = ITEM_CLASSES

Ancestors

Subclasses

Class variables

var supported_item_models

Inherited members

class WorkingSet (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class WorkingSet(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Working Set',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

exchangelib-4.6.1/docs/exchangelib/folders/known_folders.html000066400000000000000000021256601414601472700244140ustar00rootroot00000000000000 exchangelib.folders.known_folders API documentation

Module exchangelib.folders.known_folders

Expand source code
from .base import Folder
from .collections import FolderCollection
from ..items import CalendarItem, Contact, Message, Task, DistributionList, MeetingRequest, MeetingResponse, \
    MeetingCancellation, ITEM_CLASSES, ASSOCIATED
from ..version import EXCHANGE_2010_SP1, EXCHANGE_2013, EXCHANGE_2013_SP1


class Calendar(Folder):
    """An interface for the Exchange calendar."""

    DISTINGUISHED_FOLDER_ID = 'calendar'
    CONTAINER_CLASS = 'IPF.Appointment'
    supported_item_models = (CalendarItem,)

    LOCALIZED_NAMES = {
        'da_DK': ('Kalender',),
        'de_DE': ('Kalender',),
        'en_US': ('Calendar',),
        'es_ES': ('Calendario',),
        'fr_CA': ('Calendrier',),
        'nl_NL': ('Agenda',),
        'ru_RU': ('Календарь',),
        'sv_SE': ('Kalender',),
        'zh_CN': ('日历',),
    }

    def view(self, *args, **kwargs):
        return FolderCollection(account=self.account, folders=[self]).view(*args, **kwargs)


class DeletedItems(Folder):
    DISTINGUISHED_FOLDER_ID = 'deleteditems'
    CONTAINER_CLASS = 'IPF.Note'
    supported_item_models = ITEM_CLASSES

    LOCALIZED_NAMES = {
        'da_DK': ('Slettet post',),
        'de_DE': ('Gelöschte Elemente',),
        'en_US': ('Deleted Items',),
        'es_ES': ('Elementos eliminados',),
        'fr_CA': ('Éléments supprimés',),
        'nl_NL': ('Verwijderde items',),
        'ru_RU': ('Удаленные',),
        'sv_SE': ('Borttaget',),
        'zh_CN': ('已删除邮件',),
    }


class Messages(Folder):
    CONTAINER_CLASS = 'IPF.Note'
    supported_item_models = (Message, MeetingRequest, MeetingResponse, MeetingCancellation)


class Drafts(Messages):
    DISTINGUISHED_FOLDER_ID = 'drafts'

    LOCALIZED_NAMES = {
        'da_DK': ('Kladder',),
        'de_DE': ('Entwürfe',),
        'en_US': ('Drafts',),
        'es_ES': ('Borradores',),
        'fr_CA': ('Brouillons',),
        'nl_NL': ('Concepten',),
        'ru_RU': ('Черновики',),
        'sv_SE': ('Utkast',),
        'zh_CN': ('草稿',),
    }


class Inbox(Messages):
    DISTINGUISHED_FOLDER_ID = 'inbox'

    LOCALIZED_NAMES = {
        'da_DK': ('Indbakke',),
        'de_DE': ('Posteingang',),
        'en_US': ('Inbox',),
        'es_ES': ('Bandeja de entrada',),
        'fr_CA': ('Boîte de réception',),
        'nl_NL': ('Postvak IN',),
        'ru_RU': ('Входящие',),
        'sv_SE': ('Inkorgen',),
        'zh_CN': ('收件箱',),
    }


class Outbox(Messages):
    DISTINGUISHED_FOLDER_ID = 'outbox'

    LOCALIZED_NAMES = {
        'da_DK': ('Udbakke',),
        'de_DE': ('Postausgang',),
        'en_US': ('Outbox',),
        'es_ES': ('Bandeja de salida',),
        'fr_CA': (u"Boîte d'envoi",),
        'nl_NL': ('Postvak UIT',),
        'ru_RU': ('Исходящие',),
        'sv_SE': ('Utkorgen',),
        'zh_CN': ('发件箱',),
    }


class SentItems(Messages):
    DISTINGUISHED_FOLDER_ID = 'sentitems'

    LOCALIZED_NAMES = {
        'da_DK': ('Sendt post',),
        'de_DE': ('Gesendete Elemente',),
        'en_US': ('Sent Items',),
        'es_ES': ('Elementos enviados',),
        'fr_CA': ('Éléments envoyés',),
        'nl_NL': ('Verzonden items',),
        'ru_RU': ('Отправленные',),
        'sv_SE': ('Skickat',),
        'zh_CN': ('已发送邮件',),
    }


class JunkEmail(Messages):
    DISTINGUISHED_FOLDER_ID = 'junkemail'

    LOCALIZED_NAMES = {
        'da_DK': ('Uønsket e-mail',),
        'de_DE': ('Junk-E-Mail',),
        'en_US': ('Junk E-mail',),
        'es_ES': ('Correo no deseado',),
        'fr_CA': ('Courrier indésirables',),
        'nl_NL': ('Ongewenste e-mail',),
        'ru_RU': ('Нежелательная почта',),
        'sv_SE': ('Skräppost',),
        'zh_CN': ('垃圾邮件',),
    }


class Tasks(Folder):
    DISTINGUISHED_FOLDER_ID = 'tasks'
    CONTAINER_CLASS = 'IPF.Task'
    supported_item_models = (Task,)

    LOCALIZED_NAMES = {
        'da_DK': ('Opgaver',),
        'de_DE': ('Aufgaben',),
        'en_US': ('Tasks',),
        'es_ES': ('Tareas',),
        'fr_CA': ('Tâches',),
        'nl_NL': ('Taken',),
        'ru_RU': ('Задачи',),
        'sv_SE': ('Uppgifter',),
        'zh_CN': ('任务',),
    }


class Contacts(Folder):
    DISTINGUISHED_FOLDER_ID = 'contacts'
    CONTAINER_CLASS = 'IPF.Contact'
    supported_item_models = (Contact, DistributionList)

    LOCALIZED_NAMES = {
        'da_DK': ('Kontaktpersoner',),
        'de_DE': ('Kontakte',),
        'en_US': ('Contacts',),
        'es_ES': ('Contactos',),
        'fr_CA': ('Contacts',),
        'nl_NL': ('Contactpersonen',),
        'ru_RU': ('Контакты',),
        'sv_SE': ('Kontakter',),
        'zh_CN': ('联系人',),
    }


class WellknownFolder(Folder):
    """A base class to use until we have a more specific folder implementation for this folder."""

    supported_item_models = ITEM_CLASSES


class AdminAuditLogs(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'adminauditlogs'
    supported_from = EXCHANGE_2013
    get_folder_allowed = False


class ArchiveDeletedItems(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archivedeleteditems'
    supported_from = EXCHANGE_2010_SP1


class ArchiveInbox(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archiveinbox'
    supported_from = EXCHANGE_2013_SP1


class ArchiveMsgFolderRoot(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archivemsgfolderroot'
    supported_from = EXCHANGE_2010_SP1


class ArchiveRecoverableItemsDeletions(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemsdeletions'
    supported_from = EXCHANGE_2010_SP1


class ArchiveRecoverableItemsPurges(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemspurges'
    supported_from = EXCHANGE_2010_SP1


class ArchiveRecoverableItemsRoot(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemsroot'
    supported_from = EXCHANGE_2010_SP1


class ArchiveRecoverableItemsVersions(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemsversions'
    supported_from = EXCHANGE_2010_SP1


class Conflicts(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'conflicts'
    supported_from = EXCHANGE_2013


class ConversationHistory(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'conversationhistory'
    supported_from = EXCHANGE_2013


class Directory(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'directory'
    supported_from = EXCHANGE_2013_SP1


class Favorites(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Note'
    DISTINGUISHED_FOLDER_ID = 'favorites'
    supported_from = EXCHANGE_2013


class IMContactList(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Contact.MOC.ImContactList'
    DISTINGUISHED_FOLDER_ID = 'imcontactlist'
    supported_from = EXCHANGE_2013


class Journal(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Journal'
    DISTINGUISHED_FOLDER_ID = 'journal'


class LocalFailures(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'localfailures'
    supported_from = EXCHANGE_2013


class MsgFolderRoot(WellknownFolder):
    """Also known as the 'Top of Information Store' folder."""

    DISTINGUISHED_FOLDER_ID = 'msgfolderroot'
    LOCALIZED_NAMES = {
        'zh_CN': ('信息存储顶部',),
    }


class MyContacts(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Note'
    DISTINGUISHED_FOLDER_ID = 'mycontacts'
    supported_from = EXCHANGE_2013


class Notes(WellknownFolder):
    CONTAINER_CLASS = 'IPF.StickyNote'
    DISTINGUISHED_FOLDER_ID = 'notes'
    LOCALIZED_NAMES = {
        'da_DK': ('Noter',),
    }


class PeopleConnect(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'peopleconnect'
    supported_from = EXCHANGE_2013


class QuickContacts(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Contact.MOC.QuickContacts'
    DISTINGUISHED_FOLDER_ID = 'quickcontacts'
    supported_from = EXCHANGE_2013


class RecipientCache(Contacts):
    DISTINGUISHED_FOLDER_ID = 'recipientcache'
    CONTAINER_CLASS = 'IPF.Contact.RecipientCache'
    supported_from = EXCHANGE_2013

    LOCALIZED_NAMES = {}


class RecoverableItemsDeletions(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'recoverableitemsdeletions'
    supported_from = EXCHANGE_2010_SP1


class RecoverableItemsPurges(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'recoverableitemspurges'
    supported_from = EXCHANGE_2010_SP1


class RecoverableItemsRoot(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'recoverableitemsroot'
    supported_from = EXCHANGE_2010_SP1


class RecoverableItemsVersions(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'recoverableitemsversions'
    supported_from = EXCHANGE_2010_SP1


class SearchFolders(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'searchfolders'


class ServerFailures(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'serverfailures'
    supported_from = EXCHANGE_2013


class SyncIssues(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Note'
    DISTINGUISHED_FOLDER_ID = 'syncissues'
    supported_from = EXCHANGE_2013


class ToDoSearch(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Task'
    DISTINGUISHED_FOLDER_ID = 'todosearch'
    supported_from = EXCHANGE_2013

    LOCALIZED_NAMES = {
        None: ('To-Do Search',),
    }


class VoiceMail(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'voicemail'
    CONTAINER_CLASS = 'IPF.Note.Microsoft.Voicemail'
    LOCALIZED_NAMES = {
        None: ('Voice Mail',),
    }


class NonDeletableFolderMixin:
    """A mixin for non-wellknown folders than that are not deletable."""

    @property
    def is_deletable(self):
        return False


class AllContacts(NonDeletableFolderMixin, Contacts):
    CONTAINER_CLASS = 'IPF.Note'

    LOCALIZED_NAMES = {
        None: ('AllContacts',),
    }


class AllItems(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF'

    LOCALIZED_NAMES = {
        None: ('AllItems',),
    }


class Audits(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Audits',),
    }
    get_folder_allowed = False


class CalendarLogging(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Calendar Logging',),
    }


class CommonViews(NonDeletableFolderMixin, Folder):
    DEFAULT_ITEM_TRAVERSAL_DEPTH = ASSOCIATED
    LOCALIZED_NAMES = {
        None: ('Common Views',),
    }


class Companies(NonDeletableFolderMixin, Contacts):
    DISTINGUISHED_FOLDER_ID = None
    CONTAINTER_CLASS = 'IPF.Contact.Company'
    LOCALIZED_NAMES = {
        None: ('Companies',),
    }


class ConversationSettings(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.Configuration'
    LOCALIZED_NAMES = {
        'da_DK': ('Indstillinger for samtalehandlinger',),
    }


class DefaultFoldersChangeHistory(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPM.DefaultFolderHistoryItem'
    LOCALIZED_NAMES = {
        None: ('DefaultFoldersChangeHistory',),
    }


class DeferredAction(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Deferred Action',),
    }


class ExchangeSyncData(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('ExchangeSyncData',),
    }


class Files(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.Files'

    LOCALIZED_NAMES = {
        'da_DK': ('Filer',),
    }


class FreebusyData(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Freebusy Data',),
    }


class Friends(NonDeletableFolderMixin, Contacts):
    CONTAINER_CLASS = 'IPF.Note'

    LOCALIZED_NAMES = {
        'de_DE': ('Bekannte',),
    }


class GALContacts(NonDeletableFolderMixin, Contacts):
    DISTINGUISHED_FOLDER_ID = None
    CONTAINER_CLASS = 'IPF.Contact.GalContacts'

    LOCALIZED_NAMES = {
        None: ('GAL Contacts',),
    }


class GraphAnalytics(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.StoreItem.GraphAnalytics'
    LOCALIZED_NAMES = {
        None: ('GraphAnalytics',),
    }


class Location(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Location',),
    }


class MailboxAssociations(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('MailboxAssociations',),
    }


class MyContactsExtended(NonDeletableFolderMixin, Contacts):
    CONTAINER_CLASS = 'IPF.Note'
    LOCALIZED_NAMES = {
        None: ('MyContactsExtended',),
    }


class OrganizationalContacts(NonDeletableFolderMixin, Contacts):
    DISTINGUISHED_FOLDER_ID = None
    CONTAINTER_CLASS = 'IPF.Contact.OrganizationalContacts'
    LOCALIZED_NAMES = {
        None: ('Organizational Contacts',),
    }


class ParkedMessages(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = None
    LOCALIZED_NAMES = {
        None: ('ParkedMessages',),
    }


class PassThroughSearchResults(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.StoreItem.PassThroughSearchResults'
    LOCALIZED_NAMES = {
        None: ('Pass-Through Search Results',),
    }


class PeopleCentricConversationBuddies(NonDeletableFolderMixin, Contacts):
    DISTINGUISHED_FOLDER_ID = None
    CONTAINTER_CLASS = 'IPF.Contact.PeopleCentricConversationBuddies'
    LOCALIZED_NAMES = {
        None: ('PeopleCentricConversation Buddies',),
    }


class PdpProfileV2Secured(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.StoreItem.PdpProfileSecured'
    LOCALIZED_NAMES = {
        None: ('PdpProfileV2Secured',),
    }


class Reminders(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'Outlook.Reminder'
    LOCALIZED_NAMES = {
        'da_DK': ('Påmindelser',),
    }


class RSSFeeds(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.Note.OutlookHomepage'
    LOCALIZED_NAMES = {
        None: ('RSS Feeds',),
    }


class Schedule(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Schedule',),
    }


class Sharing(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.Note'
    LOCALIZED_NAMES = {
        None: ('Sharing',),
    }


class Shortcuts(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Shortcuts',),
    }


class Signal(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.StoreItem.Signal'
    LOCALIZED_NAMES = {
        None: ('Signal',),
    }


class SmsAndChatsSync(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.SmsAndChatsSync'
    LOCALIZED_NAMES = {
        None: ('SmsAndChatsSync',),
    }


class SpoolerQueue(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Spooler Queue',),
    }


class System(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('System',),
    }
    get_folder_allowed = False


class System1(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('System1',),
    }
    get_folder_allowed = False


class TemporarySaves(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('TemporarySaves',),
    }


class Views(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Views',),
    }


class WorkingSet(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Working Set',),
    }


# Folders that return 'ErrorDeleteDistinguishedFolder' when we try to delete them. I can't find any official docs
# listing these folders.
NON_DELETABLE_FOLDERS = [
    AllContacts,
    AllItems,
    Audits,
    CalendarLogging,
    CommonViews,
    Companies,
    ConversationSettings,
    DefaultFoldersChangeHistory,
    DeferredAction,
    ExchangeSyncData,
    FreebusyData,
    Files,
    Friends,
    GALContacts,
    GraphAnalytics,
    Location,
    MailboxAssociations,
    MyContactsExtended,
    OrganizationalContacts,
    ParkedMessages,
    PassThroughSearchResults,
    PeopleCentricConversationBuddies,
    PdpProfileV2Secured,
    Reminders,
    RSSFeeds,
    Schedule,
    Sharing,
    Shortcuts,
    Signal,
    SmsAndChatsSync,
    SpoolerQueue,
    System,
    System1,
    TemporarySaves,
    Views,
    WorkingSet,
]

WELLKNOWN_FOLDERS_IN_ROOT = [
    AdminAuditLogs,
    Calendar,
    Conflicts,
    Contacts,
    ConversationHistory,
    DeletedItems,
    Directory,
    Drafts,
    Favorites,
    IMContactList,
    Inbox,
    Journal,
    JunkEmail,
    LocalFailures,
    MsgFolderRoot,
    MyContacts,
    Notes,
    Outbox,
    PeopleConnect,
    QuickContacts,
    RecipientCache,
    RecoverableItemsDeletions,
    RecoverableItemsPurges,
    RecoverableItemsRoot,
    RecoverableItemsVersions,
    SearchFolders,
    SentItems,
    ServerFailures,
    SyncIssues,
    Tasks,
    ToDoSearch,
    VoiceMail,
]

WELLKNOWN_FOLDERS_IN_ARCHIVE_ROOT = [
    ArchiveDeletedItems,
    ArchiveInbox,
    ArchiveMsgFolderRoot,
    ArchiveRecoverableItemsDeletions,
    ArchiveRecoverableItemsPurges,
    ArchiveRecoverableItemsRoot,
    ArchiveRecoverableItemsVersions,
]

Classes

class AdminAuditLogs (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class AdminAuditLogs(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'adminauditlogs'
    supported_from = EXCHANGE_2013
    get_folder_allowed = False

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var get_folder_allowed
var supported_from

Inherited members

class AllContacts (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class AllContacts(NonDeletableFolderMixin, Contacts):
    CONTAINER_CLASS = 'IPF.Note'

    LOCALIZED_NAMES = {
        None: ('AllContacts',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class AllItems (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class AllItems(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF'

    LOCALIZED_NAMES = {
        None: ('AllItems',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class ArchiveDeletedItems (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ArchiveDeletedItems(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archivedeleteditems'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class ArchiveInbox (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ArchiveInbox(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archiveinbox'
    supported_from = EXCHANGE_2013_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class ArchiveMsgFolderRoot (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ArchiveMsgFolderRoot(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archivemsgfolderroot'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class ArchiveRecoverableItemsDeletions (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ArchiveRecoverableItemsDeletions(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemsdeletions'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class ArchiveRecoverableItemsPurges (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ArchiveRecoverableItemsPurges(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemspurges'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class ArchiveRecoverableItemsRoot (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ArchiveRecoverableItemsRoot(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemsroot'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class ArchiveRecoverableItemsVersions (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ArchiveRecoverableItemsVersions(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemsversions'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class Audits (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Audits(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Audits',),
    }
    get_folder_allowed = False

Ancestors

Class variables

var LOCALIZED_NAMES
var get_folder_allowed

Inherited members

class Calendar (**kwargs)

An interface for the Exchange calendar.

Expand source code
class Calendar(Folder):
    """An interface for the Exchange calendar."""

    DISTINGUISHED_FOLDER_ID = 'calendar'
    CONTAINER_CLASS = 'IPF.Appointment'
    supported_item_models = (CalendarItem,)

    LOCALIZED_NAMES = {
        'da_DK': ('Kalender',),
        'de_DE': ('Kalender',),
        'en_US': ('Calendar',),
        'es_ES': ('Calendario',),
        'fr_CA': ('Calendrier',),
        'nl_NL': ('Agenda',),
        'ru_RU': ('Календарь',),
        'sv_SE': ('Kalender',),
        'zh_CN': ('日历',),
    }

    def view(self, *args, **kwargs):
        return FolderCollection(account=self.account, folders=[self]).view(*args, **kwargs)

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES
var supported_item_models

Methods

def view(self, *args, **kwargs)
Expand source code
def view(self, *args, **kwargs):
    return FolderCollection(account=self.account, folders=[self]).view(*args, **kwargs)

Inherited members

class CalendarLogging (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class CalendarLogging(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Calendar Logging',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class CommonViews (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class CommonViews(NonDeletableFolderMixin, Folder):
    DEFAULT_ITEM_TRAVERSAL_DEPTH = ASSOCIATED
    LOCALIZED_NAMES = {
        None: ('Common Views',),
    }

Ancestors

Class variables

var DEFAULT_ITEM_TRAVERSAL_DEPTH
var LOCALIZED_NAMES

Inherited members

class Companies (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Companies(NonDeletableFolderMixin, Contacts):
    DISTINGUISHED_FOLDER_ID = None
    CONTAINTER_CLASS = 'IPF.Contact.Company'
    LOCALIZED_NAMES = {
        None: ('Companies',),
    }

Ancestors

Class variables

var CONTAINTER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class Conflicts (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class Conflicts(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'conflicts'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class Contacts (**kwargs)
Expand source code
class Contacts(Folder):
    DISTINGUISHED_FOLDER_ID = 'contacts'
    CONTAINER_CLASS = 'IPF.Contact'
    supported_item_models = (Contact, DistributionList)

    LOCALIZED_NAMES = {
        'da_DK': ('Kontaktpersoner',),
        'de_DE': ('Kontakte',),
        'en_US': ('Contacts',),
        'es_ES': ('Contactos',),
        'fr_CA': ('Contacts',),
        'nl_NL': ('Contactpersonen',),
        'ru_RU': ('Контакты',),
        'sv_SE': ('Kontakter',),
        'zh_CN': ('联系人',),
    }

Ancestors

Subclasses

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES
var supported_item_models

Inherited members

class ConversationHistory (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ConversationHistory(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'conversationhistory'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class ConversationSettings (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class ConversationSettings(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.Configuration'
    LOCALIZED_NAMES = {
        'da_DK': ('Indstillinger for samtalehandlinger',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class DefaultFoldersChangeHistory (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class DefaultFoldersChangeHistory(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPM.DefaultFolderHistoryItem'
    LOCALIZED_NAMES = {
        None: ('DefaultFoldersChangeHistory',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class DeferredAction (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class DeferredAction(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Deferred Action',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class DeletedItems (**kwargs)
Expand source code
class DeletedItems(Folder):
    DISTINGUISHED_FOLDER_ID = 'deleteditems'
    CONTAINER_CLASS = 'IPF.Note'
    supported_item_models = ITEM_CLASSES

    LOCALIZED_NAMES = {
        'da_DK': ('Slettet post',),
        'de_DE': ('Gelöschte Elemente',),
        'en_US': ('Deleted Items',),
        'es_ES': ('Elementos eliminados',),
        'fr_CA': ('Éléments supprimés',),
        'nl_NL': ('Verwijderde items',),
        'ru_RU': ('Удаленные',),
        'sv_SE': ('Borttaget',),
        'zh_CN': ('已删除邮件',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES
var supported_item_models

Inherited members

class Directory (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class Directory(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'directory'
    supported_from = EXCHANGE_2013_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class Drafts (**kwargs)
Expand source code
class Drafts(Messages):
    DISTINGUISHED_FOLDER_ID = 'drafts'

    LOCALIZED_NAMES = {
        'da_DK': ('Kladder',),
        'de_DE': ('Entwürfe',),
        'en_US': ('Drafts',),
        'es_ES': ('Borradores',),
        'fr_CA': ('Brouillons',),
        'nl_NL': ('Concepten',),
        'ru_RU': ('Черновики',),
        'sv_SE': ('Utkast',),
        'zh_CN': ('草稿',),
    }

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class ExchangeSyncData (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class ExchangeSyncData(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('ExchangeSyncData',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class Favorites (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class Favorites(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Note'
    DISTINGUISHED_FOLDER_ID = 'favorites'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class Files (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Files(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.Files'

    LOCALIZED_NAMES = {
        'da_DK': ('Filer',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class FreebusyData (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class FreebusyData(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Freebusy Data',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class Friends (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Friends(NonDeletableFolderMixin, Contacts):
    CONTAINER_CLASS = 'IPF.Note'

    LOCALIZED_NAMES = {
        'de_DE': ('Bekannte',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class GALContacts (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class GALContacts(NonDeletableFolderMixin, Contacts):
    DISTINGUISHED_FOLDER_ID = None
    CONTAINER_CLASS = 'IPF.Contact.GalContacts'

    LOCALIZED_NAMES = {
        None: ('GAL Contacts',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class GraphAnalytics (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class GraphAnalytics(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.StoreItem.GraphAnalytics'
    LOCALIZED_NAMES = {
        None: ('GraphAnalytics',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class IMContactList (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class IMContactList(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Contact.MOC.ImContactList'
    DISTINGUISHED_FOLDER_ID = 'imcontactlist'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class Inbox (**kwargs)
Expand source code
class Inbox(Messages):
    DISTINGUISHED_FOLDER_ID = 'inbox'

    LOCALIZED_NAMES = {
        'da_DK': ('Indbakke',),
        'de_DE': ('Posteingang',),
        'en_US': ('Inbox',),
        'es_ES': ('Bandeja de entrada',),
        'fr_CA': ('Boîte de réception',),
        'nl_NL': ('Postvak IN',),
        'ru_RU': ('Входящие',),
        'sv_SE': ('Inkorgen',),
        'zh_CN': ('收件箱',),
    }

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class Journal (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class Journal(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Journal'
    DISTINGUISHED_FOLDER_ID = 'journal'

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID

Inherited members

class JunkEmail (**kwargs)
Expand source code
class JunkEmail(Messages):
    DISTINGUISHED_FOLDER_ID = 'junkemail'

    LOCALIZED_NAMES = {
        'da_DK': ('Uønsket e-mail',),
        'de_DE': ('Junk-E-Mail',),
        'en_US': ('Junk E-mail',),
        'es_ES': ('Correo no deseado',),
        'fr_CA': ('Courrier indésirables',),
        'nl_NL': ('Ongewenste e-mail',),
        'ru_RU': ('Нежелательная почта',),
        'sv_SE': ('Skräppost',),
        'zh_CN': ('垃圾邮件',),
    }

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class LocalFailures (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class LocalFailures(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'localfailures'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class Location (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Location(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Location',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class MailboxAssociations (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class MailboxAssociations(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('MailboxAssociations',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class Messages (**kwargs)
Expand source code
class Messages(Folder):
    CONTAINER_CLASS = 'IPF.Note'
    supported_item_models = (Message, MeetingRequest, MeetingResponse, MeetingCancellation)

Ancestors

Subclasses

Class variables

var CONTAINER_CLASS
var supported_item_models

Inherited members

class MsgFolderRoot (**kwargs)

Also known as the 'Top of Information Store' folder.

Expand source code
class MsgFolderRoot(WellknownFolder):
    """Also known as the 'Top of Information Store' folder."""

    DISTINGUISHED_FOLDER_ID = 'msgfolderroot'
    LOCALIZED_NAMES = {
        'zh_CN': ('信息存储顶部',),
    }

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class MyContacts (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class MyContacts(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Note'
    DISTINGUISHED_FOLDER_ID = 'mycontacts'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class MyContactsExtended (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class MyContactsExtended(NonDeletableFolderMixin, Contacts):
    CONTAINER_CLASS = 'IPF.Note'
    LOCALIZED_NAMES = {
        None: ('MyContactsExtended',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class NonDeletableFolderMixin

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class NonDeletableFolderMixin:
    """A mixin for non-wellknown folders than that are not deletable."""

    @property
    def is_deletable(self):
        return False

Subclasses

Instance variables

var is_deletable
Expand source code
@property
def is_deletable(self):
    return False
class Notes (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class Notes(WellknownFolder):
    CONTAINER_CLASS = 'IPF.StickyNote'
    DISTINGUISHED_FOLDER_ID = 'notes'
    LOCALIZED_NAMES = {
        'da_DK': ('Noter',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class OrganizationalContacts (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class OrganizationalContacts(NonDeletableFolderMixin, Contacts):
    DISTINGUISHED_FOLDER_ID = None
    CONTAINTER_CLASS = 'IPF.Contact.OrganizationalContacts'
    LOCALIZED_NAMES = {
        None: ('Organizational Contacts',),
    }

Ancestors

Class variables

var CONTAINTER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class Outbox (**kwargs)
Expand source code
class Outbox(Messages):
    DISTINGUISHED_FOLDER_ID = 'outbox'

    LOCALIZED_NAMES = {
        'da_DK': ('Udbakke',),
        'de_DE': ('Postausgang',),
        'en_US': ('Outbox',),
        'es_ES': ('Bandeja de salida',),
        'fr_CA': (u"Boîte d'envoi",),
        'nl_NL': ('Postvak UIT',),
        'ru_RU': ('Исходящие',),
        'sv_SE': ('Utkorgen',),
        'zh_CN': ('发件箱',),
    }

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class ParkedMessages (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class ParkedMessages(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = None
    LOCALIZED_NAMES = {
        None: ('ParkedMessages',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class PassThroughSearchResults (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class PassThroughSearchResults(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.StoreItem.PassThroughSearchResults'
    LOCALIZED_NAMES = {
        None: ('Pass-Through Search Results',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class PdpProfileV2Secured (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class PdpProfileV2Secured(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.StoreItem.PdpProfileSecured'
    LOCALIZED_NAMES = {
        None: ('PdpProfileV2Secured',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class PeopleCentricConversationBuddies (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class PeopleCentricConversationBuddies(NonDeletableFolderMixin, Contacts):
    DISTINGUISHED_FOLDER_ID = None
    CONTAINTER_CLASS = 'IPF.Contact.PeopleCentricConversationBuddies'
    LOCALIZED_NAMES = {
        None: ('PeopleCentricConversation Buddies',),
    }

Ancestors

Class variables

var CONTAINTER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class PeopleConnect (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class PeopleConnect(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'peopleconnect'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class QuickContacts (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class QuickContacts(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Contact.MOC.QuickContacts'
    DISTINGUISHED_FOLDER_ID = 'quickcontacts'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class RSSFeeds (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class RSSFeeds(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.Note.OutlookHomepage'
    LOCALIZED_NAMES = {
        None: ('RSS Feeds',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class RecipientCache (**kwargs)
Expand source code
class RecipientCache(Contacts):
    DISTINGUISHED_FOLDER_ID = 'recipientcache'
    CONTAINER_CLASS = 'IPF.Contact.RecipientCache'
    supported_from = EXCHANGE_2013

    LOCALIZED_NAMES = {}

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES
var supported_from

Inherited members

class RecoverableItemsDeletions (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class RecoverableItemsDeletions(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'recoverableitemsdeletions'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class RecoverableItemsPurges (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class RecoverableItemsPurges(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'recoverableitemspurges'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class RecoverableItemsRoot (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class RecoverableItemsRoot(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'recoverableitemsroot'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class RecoverableItemsVersions (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class RecoverableItemsVersions(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'recoverableitemsversions'
    supported_from = EXCHANGE_2010_SP1

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class Reminders (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Reminders(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'Outlook.Reminder'
    LOCALIZED_NAMES = {
        'da_DK': ('Påmindelser',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class Schedule (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Schedule(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Schedule',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class SearchFolders (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class SearchFolders(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'searchfolders'

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID

Inherited members

class SentItems (**kwargs)
Expand source code
class SentItems(Messages):
    DISTINGUISHED_FOLDER_ID = 'sentitems'

    LOCALIZED_NAMES = {
        'da_DK': ('Sendt post',),
        'de_DE': ('Gesendete Elemente',),
        'en_US': ('Sent Items',),
        'es_ES': ('Elementos enviados',),
        'fr_CA': ('Éléments envoyés',),
        'nl_NL': ('Verzonden items',),
        'ru_RU': ('Отправленные',),
        'sv_SE': ('Skickat',),
        'zh_CN': ('已发送邮件',),
    }

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class ServerFailures (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ServerFailures(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'serverfailures'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class Sharing (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Sharing(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.Note'
    LOCALIZED_NAMES = {
        None: ('Sharing',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class Shortcuts (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Shortcuts(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Shortcuts',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class Signal (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Signal(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.StoreItem.Signal'
    LOCALIZED_NAMES = {
        None: ('Signal',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class SmsAndChatsSync (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class SmsAndChatsSync(NonDeletableFolderMixin, Folder):
    CONTAINER_CLASS = 'IPF.SmsAndChatsSync'
    LOCALIZED_NAMES = {
        None: ('SmsAndChatsSync',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var LOCALIZED_NAMES

Inherited members

class SpoolerQueue (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class SpoolerQueue(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Spooler Queue',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class SyncIssues (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class SyncIssues(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Note'
    DISTINGUISHED_FOLDER_ID = 'syncissues'
    supported_from = EXCHANGE_2013

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var supported_from

Inherited members

class System (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class System(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('System',),
    }
    get_folder_allowed = False

Ancestors

Class variables

var LOCALIZED_NAMES
var get_folder_allowed

Inherited members

class System1 (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class System1(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('System1',),
    }
    get_folder_allowed = False

Ancestors

Class variables

var LOCALIZED_NAMES
var get_folder_allowed

Inherited members

class Tasks (**kwargs)
Expand source code
class Tasks(Folder):
    DISTINGUISHED_FOLDER_ID = 'tasks'
    CONTAINER_CLASS = 'IPF.Task'
    supported_item_models = (Task,)

    LOCALIZED_NAMES = {
        'da_DK': ('Opgaver',),
        'de_DE': ('Aufgaben',),
        'en_US': ('Tasks',),
        'es_ES': ('Tareas',),
        'fr_CA': ('Tâches',),
        'nl_NL': ('Taken',),
        'ru_RU': ('Задачи',),
        'sv_SE': ('Uppgifter',),
        'zh_CN': ('任务',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES
var supported_item_models

Inherited members

class TemporarySaves (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class TemporarySaves(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('TemporarySaves',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class ToDoSearch (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class ToDoSearch(WellknownFolder):
    CONTAINER_CLASS = 'IPF.Task'
    DISTINGUISHED_FOLDER_ID = 'todosearch'
    supported_from = EXCHANGE_2013

    LOCALIZED_NAMES = {
        None: ('To-Do Search',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES
var supported_from

Inherited members

class Views (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class Views(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Views',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

class VoiceMail (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class VoiceMail(WellknownFolder):
    DISTINGUISHED_FOLDER_ID = 'voicemail'
    CONTAINER_CLASS = 'IPF.Note.Microsoft.Voicemail'
    LOCALIZED_NAMES = {
        None: ('Voice Mail',),
    }

Ancestors

Class variables

var CONTAINER_CLASS
var DISTINGUISHED_FOLDER_ID
var LOCALIZED_NAMES

Inherited members

class WellknownFolder (**kwargs)

A base class to use until we have a more specific folder implementation for this folder.

Expand source code
class WellknownFolder(Folder):
    """A base class to use until we have a more specific folder implementation for this folder."""

    supported_item_models = ITEM_CLASSES

Ancestors

Subclasses

Class variables

var supported_item_models

Inherited members

class WorkingSet (**kwargs)

A mixin for non-wellknown folders than that are not deletable.

Expand source code
class WorkingSet(NonDeletableFolderMixin, Folder):
    LOCALIZED_NAMES = {
        None: ('Working Set',),
    }

Ancestors

Class variables

var LOCALIZED_NAMES

Inherited members

exchangelib-4.6.1/docs/exchangelib/folders/queryset.html000066400000000000000000000701721414601472700234160ustar00rootroot00000000000000 exchangelib.folders.queryset API documentation

Module exchangelib.folders.queryset

Expand source code
import logging
from copy import deepcopy

from ..properties import InvalidField, FolderId
from ..queryset import DoesNotExist, MultipleObjectsReturned
from ..restriction import Q

# Traversal enums
SHALLOW = 'Shallow'
SOFT_DELETED = 'SoftDeleted'
DEEP = 'Deep'
FOLDER_TRAVERSAL_CHOICES = (SHALLOW, DEEP, SOFT_DELETED)


log = logging.getLogger(__name__)


class FolderQuerySet:
    """A QuerySet-like class for finding subfolders of a folder collection."""

    def __init__(self, folder_collection):
        from .collections import FolderCollection
        if not isinstance(folder_collection, FolderCollection):
            raise ValueError("'folder_collection' %r must be a FolderCollection instance" % folder_collection)
        self.folder_collection = folder_collection
        self.q = Q()  # Default to no restrictions
        self.only_fields = None
        self._depth = None

    def _copy_cls(self):
        return self.__class__(folder_collection=self.folder_collection)

    def _copy_self(self):
        """Chaining operations must make a copy of self before making any modifications."""
        new_qs = self._copy_cls()
        new_qs.q = deepcopy(self.q)
        new_qs.only_fields = self.only_fields
        new_qs._depth = self._depth
        return new_qs

    def only(self, *args):
        """Restrict the fields returned. 'name' and 'folder_class' are always returned."""
        from .base import Folder
        # Subfolders will always be of class Folder
        all_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=None)
        all_fields.update(Folder.attribute_fields())
        only_fields = []
        for arg in args:
            for field_path in all_fields:
                if field_path.field.name == arg:
                    only_fields.append(field_path)
                    break
            else:
                raise InvalidField("Unknown field %r on folders %s" % (arg, self.folder_collection.folders))
        new_qs = self._copy_self()
        new_qs.only_fields = only_fields
        return new_qs

    def depth(self, depth):
        """Specify the search depth. Possible values are: SHALLOW or DEEP.

        :param depth:
        """
        new_qs = self._copy_self()
        new_qs._depth = depth
        return new_qs

    def get(self, *args, **kwargs):
        """Return the query result as exactly one item. Raises DoesNotExist if there are no results, and
        MultipleObjectsReturned if there are multiple results.
        """
        from .collections import FolderCollection
        if not args and set(kwargs) in ({'id'}, {'id', 'changekey'}):
            folders = list(FolderCollection(
                account=self.folder_collection.account, folders=[FolderId(**kwargs)]
            ).resolve())
        elif args or kwargs:
            folders = list(self.filter(*args, **kwargs))
        else:
            folders = list(self.all())
        if not folders:
            raise DoesNotExist('Could not find a child folder matching the query')
        if len(folders) != 1:
            raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders)
        f = folders[0]
        if isinstance(f, Exception):
            raise f
        return f

    def all(self):
        """ """
        new_qs = self._copy_self()
        return new_qs

    def filter(self, *args, **kwargs):
        """Add restrictions to the folder search."""
        new_qs = self._copy_self()
        q = Q(*args, **kwargs)
        new_qs.q = new_qs.q & q
        return new_qs

    def __iter__(self):
        return self._query()

    def _query(self):
        from .base import Folder
        from .collections import FolderCollection
        if self.only_fields is None:
            # Subfolders will always be of class Folder
            non_complex_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=False)
            complex_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=True)
        else:
            non_complex_fields = {f for f in self.only_fields if not f.field.is_complex}
            complex_fields = {f for f in self.only_fields if f.field.is_complex}

        # First, fetch all non-complex fields using FindFolder. We do this because some folders do not support
        # GetFolder but we still want to get as much information as possible.
        folders = self.folder_collection.find_folders(q=self.q, depth=self._depth, additional_fields=non_complex_fields)
        if not complex_fields:
            yield from folders
            return

        # Fetch all properties for the found folders
        resolveable_folders = []
        for f in folders:
            if not f.get_folder_allowed:
                log.debug('GetFolder not allowed on folder %s. Non-complex fields must be fetched with FindFolder', f)
                yield f
            else:
                resolveable_folders.append(f)

        # Get the complex fields using GetFolder, for the folders that support it, and add the extra field values
        complex_folders = FolderCollection(
            account=self.folder_collection.account, folders=resolveable_folders
        ).get_folders(additional_fields=complex_fields)
        for f, complex_f in zip(resolveable_folders, complex_folders):
            if isinstance(f, Exception):
                yield f
                continue
            if isinstance(complex_f, Exception):
                yield complex_f
                continue
            # Add the extra field values to the folders we fetched with find_folders()
            if f.__class__ != complex_f.__class__:
                raise ValueError('Type mismatch: %s vs %s' % (f, complex_f))
            for complex_field in complex_fields:
                field_name = complex_field.field.name
                setattr(f, field_name, getattr(complex_f, field_name))
            yield f


class SingleFolderQuerySet(FolderQuerySet):
    """A helper class with simpler argument types."""

    def __init__(self, account, folder):
        from .collections import FolderCollection
        folder_collection = FolderCollection(account=account, folders=[folder])
        super().__init__(folder_collection=folder_collection)

    def _copy_cls(self):
        return self.__class__(account=self.folder_collection.account, folder=self.folder_collection.folders[0])

    def resolve(self):
        folders = list(self.folder_collection.resolve())
        if not folders:
            raise DoesNotExist('Could not find a folder matching the query')
        if len(folders) != 1:
            raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders)
        f = folders[0]
        if isinstance(f, Exception):
            raise f
        return f

Classes

class FolderQuerySet (folder_collection)

A QuerySet-like class for finding subfolders of a folder collection.

Expand source code
class FolderQuerySet:
    """A QuerySet-like class for finding subfolders of a folder collection."""

    def __init__(self, folder_collection):
        from .collections import FolderCollection
        if not isinstance(folder_collection, FolderCollection):
            raise ValueError("'folder_collection' %r must be a FolderCollection instance" % folder_collection)
        self.folder_collection = folder_collection
        self.q = Q()  # Default to no restrictions
        self.only_fields = None
        self._depth = None

    def _copy_cls(self):
        return self.__class__(folder_collection=self.folder_collection)

    def _copy_self(self):
        """Chaining operations must make a copy of self before making any modifications."""
        new_qs = self._copy_cls()
        new_qs.q = deepcopy(self.q)
        new_qs.only_fields = self.only_fields
        new_qs._depth = self._depth
        return new_qs

    def only(self, *args):
        """Restrict the fields returned. 'name' and 'folder_class' are always returned."""
        from .base import Folder
        # Subfolders will always be of class Folder
        all_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=None)
        all_fields.update(Folder.attribute_fields())
        only_fields = []
        for arg in args:
            for field_path in all_fields:
                if field_path.field.name == arg:
                    only_fields.append(field_path)
                    break
            else:
                raise InvalidField("Unknown field %r on folders %s" % (arg, self.folder_collection.folders))
        new_qs = self._copy_self()
        new_qs.only_fields = only_fields
        return new_qs

    def depth(self, depth):
        """Specify the search depth. Possible values are: SHALLOW or DEEP.

        :param depth:
        """
        new_qs = self._copy_self()
        new_qs._depth = depth
        return new_qs

    def get(self, *args, **kwargs):
        """Return the query result as exactly one item. Raises DoesNotExist if there are no results, and
        MultipleObjectsReturned if there are multiple results.
        """
        from .collections import FolderCollection
        if not args and set(kwargs) in ({'id'}, {'id', 'changekey'}):
            folders = list(FolderCollection(
                account=self.folder_collection.account, folders=[FolderId(**kwargs)]
            ).resolve())
        elif args or kwargs:
            folders = list(self.filter(*args, **kwargs))
        else:
            folders = list(self.all())
        if not folders:
            raise DoesNotExist('Could not find a child folder matching the query')
        if len(folders) != 1:
            raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders)
        f = folders[0]
        if isinstance(f, Exception):
            raise f
        return f

    def all(self):
        """ """
        new_qs = self._copy_self()
        return new_qs

    def filter(self, *args, **kwargs):
        """Add restrictions to the folder search."""
        new_qs = self._copy_self()
        q = Q(*args, **kwargs)
        new_qs.q = new_qs.q & q
        return new_qs

    def __iter__(self):
        return self._query()

    def _query(self):
        from .base import Folder
        from .collections import FolderCollection
        if self.only_fields is None:
            # Subfolders will always be of class Folder
            non_complex_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=False)
            complex_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=True)
        else:
            non_complex_fields = {f for f in self.only_fields if not f.field.is_complex}
            complex_fields = {f for f in self.only_fields if f.field.is_complex}

        # First, fetch all non-complex fields using FindFolder. We do this because some folders do not support
        # GetFolder but we still want to get as much information as possible.
        folders = self.folder_collection.find_folders(q=self.q, depth=self._depth, additional_fields=non_complex_fields)
        if not complex_fields:
            yield from folders
            return

        # Fetch all properties for the found folders
        resolveable_folders = []
        for f in folders:
            if not f.get_folder_allowed:
                log.debug('GetFolder not allowed on folder %s. Non-complex fields must be fetched with FindFolder', f)
                yield f
            else:
                resolveable_folders.append(f)

        # Get the complex fields using GetFolder, for the folders that support it, and add the extra field values
        complex_folders = FolderCollection(
            account=self.folder_collection.account, folders=resolveable_folders
        ).get_folders(additional_fields=complex_fields)
        for f, complex_f in zip(resolveable_folders, complex_folders):
            if isinstance(f, Exception):
                yield f
                continue
            if isinstance(complex_f, Exception):
                yield complex_f
                continue
            # Add the extra field values to the folders we fetched with find_folders()
            if f.__class__ != complex_f.__class__:
                raise ValueError('Type mismatch: %s vs %s' % (f, complex_f))
            for complex_field in complex_fields:
                field_name = complex_field.field.name
                setattr(f, field_name, getattr(complex_f, field_name))
            yield f

Subclasses

Methods

def all(self)
Expand source code
def all(self):
    """ """
    new_qs = self._copy_self()
    return new_qs
def depth(self, depth)

Specify the search depth. Possible values are: SHALLOW or DEEP.

:param depth:

Expand source code
def depth(self, depth):
    """Specify the search depth. Possible values are: SHALLOW or DEEP.

    :param depth:
    """
    new_qs = self._copy_self()
    new_qs._depth = depth
    return new_qs
def filter(self, *args, **kwargs)

Add restrictions to the folder search.

Expand source code
def filter(self, *args, **kwargs):
    """Add restrictions to the folder search."""
    new_qs = self._copy_self()
    q = Q(*args, **kwargs)
    new_qs.q = new_qs.q & q
    return new_qs
def get(self, *args, **kwargs)

Return the query result as exactly one item. Raises DoesNotExist if there are no results, and MultipleObjectsReturned if there are multiple results.

Expand source code
def get(self, *args, **kwargs):
    """Return the query result as exactly one item. Raises DoesNotExist if there are no results, and
    MultipleObjectsReturned if there are multiple results.
    """
    from .collections import FolderCollection
    if not args and set(kwargs) in ({'id'}, {'id', 'changekey'}):
        folders = list(FolderCollection(
            account=self.folder_collection.account, folders=[FolderId(**kwargs)]
        ).resolve())
    elif args or kwargs:
        folders = list(self.filter(*args, **kwargs))
    else:
        folders = list(self.all())
    if not folders:
        raise DoesNotExist('Could not find a child folder matching the query')
    if len(folders) != 1:
        raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders)
    f = folders[0]
    if isinstance(f, Exception):
        raise f
    return f
def only(self, *args)

Restrict the fields returned. 'name' and 'folder_class' are always returned.

Expand source code
def only(self, *args):
    """Restrict the fields returned. 'name' and 'folder_class' are always returned."""
    from .base import Folder
    # Subfolders will always be of class Folder
    all_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=None)
    all_fields.update(Folder.attribute_fields())
    only_fields = []
    for arg in args:
        for field_path in all_fields:
            if field_path.field.name == arg:
                only_fields.append(field_path)
                break
        else:
            raise InvalidField("Unknown field %r on folders %s" % (arg, self.folder_collection.folders))
    new_qs = self._copy_self()
    new_qs.only_fields = only_fields
    return new_qs
class SingleFolderQuerySet (account, folder)

A helper class with simpler argument types.

Expand source code
class SingleFolderQuerySet(FolderQuerySet):
    """A helper class with simpler argument types."""

    def __init__(self, account, folder):
        from .collections import FolderCollection
        folder_collection = FolderCollection(account=account, folders=[folder])
        super().__init__(folder_collection=folder_collection)

    def _copy_cls(self):
        return self.__class__(account=self.folder_collection.account, folder=self.folder_collection.folders[0])

    def resolve(self):
        folders = list(self.folder_collection.resolve())
        if not folders:
            raise DoesNotExist('Could not find a folder matching the query')
        if len(folders) != 1:
            raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders)
        f = folders[0]
        if isinstance(f, Exception):
            raise f
        return f

Ancestors

Methods

def resolve(self)
Expand source code
def resolve(self):
    folders = list(self.folder_collection.resolve())
    if not folders:
        raise DoesNotExist('Could not find a folder matching the query')
    if len(folders) != 1:
        raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders)
    f = folders[0]
    if isinstance(f, Exception):
        raise f
    return f

Inherited members

exchangelib-4.6.1/docs/exchangelib/folders/roots.html000066400000000000000000002274771414601472700227170ustar00rootroot00000000000000 exchangelib.folders.roots API documentation

Module exchangelib.folders.roots

Expand source code
import logging
from threading import Lock

from .base import BaseFolder, MISSING_FOLDER_ERRORS
from .collections import FolderCollection
from .known_folders import MsgFolderRoot, NON_DELETABLE_FOLDERS, WELLKNOWN_FOLDERS_IN_ROOT, \
    WELLKNOWN_FOLDERS_IN_ARCHIVE_ROOT
from .queryset import SingleFolderQuerySet, SHALLOW
from ..errors import ErrorAccessDenied, ErrorFolderNotFound, ErrorInvalidOperation
from ..fields import EffectiveRightsField
from ..properties import EWSMeta
from ..version import EXCHANGE_2007_SP1, EXCHANGE_2010_SP1

log = logging.getLogger(__name__)


class RootOfHierarchy(BaseFolder, metaclass=EWSMeta):
    """Base class for folders that implement the root of a folder hierarchy."""

    # A list of wellknown, or "distinguished", folders that are belong in this folder hierarchy. See
    # https://docs.microsoft.com/en-us/dotnet/api/microsoft.exchange.webservices.data.wellknownfoldername
    # and https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid
    # 'RootOfHierarchy' subclasses must not be in this list.
    WELLKNOWN_FOLDERS = []

    _subfolders_lock = Lock()

    # This folder type also has 'folder:PermissionSet' on some server versions, but requesting it sometimes causes
    # 'ErrorAccessDenied', as reported by some users. Ignore it entirely for root folders - it's usefulness is
    # deemed minimal at best.
    effective_rights = EffectiveRightsField(field_uri='folder:EffectiveRights', is_read_only=True,
                                            supported_from=EXCHANGE_2007_SP1)

    __slots__ = '_account', '_subfolders'

    # A special folder that acts as the top of a folder hierarchy. Finds and caches subfolders at arbitrary depth.
    def __init__(self, **kwargs):
        self._account = kwargs.pop('account', None)  # A pointer back to the account holding the folder hierarchy
        super().__init__(**kwargs)
        self._subfolders = None  # See self._folders_map()

    @property
    def account(self):
        return self._account

    @property
    def root(self):
        return self

    @property
    def parent(self):
        return None

    @classmethod
    def register(cls, *args, **kwargs):
        if cls is not RootOfHierarchy:
            raise TypeError('For folder roots, custom fields must be registered on the RootOfHierarchy class')
        return super().register(*args, **kwargs)

    @classmethod
    def deregister(cls, *args, **kwargs):
        if cls is not RootOfHierarchy:
            raise TypeError('For folder roots, custom fields must be registered on the RootOfHierarchy class')
        return super().deregister(*args, **kwargs)

    def get_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        return self._folders_map.get(folder.id, None)

    def add_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        self._folders_map[folder.id] = folder

    def update_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        self._folders_map[folder.id] = folder

    def remove_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        try:
            del self._folders_map[folder.id]
        except KeyError:
            pass

    def clear_cache(self):
        with self._subfolders_lock:
            self._subfolders = None

    def get_children(self, folder):
        for f in self._folders_map.values():
            if not f.parent:
                continue
            if f.parent.id == folder.id:
                yield f

    @classmethod
    def get_distinguished(cls, account):
        """Get the distinguished folder for this folder class.

        :param account:
        """
        if not cls.DISTINGUISHED_FOLDER_ID:
            raise ValueError('Class %s must have a DISTINGUISHED_FOLDER_ID value' % cls)
        try:
            return cls.resolve(
                account=account,
                folder=cls(account=account, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
            )
        except MISSING_FOLDER_ERRORS:
            raise ErrorFolderNotFound('Could not find distinguished folder %s' % cls.DISTINGUISHED_FOLDER_ID)

    def get_default_folder(self, folder_cls):
        """Return the distinguished folder instance of type folder_cls belonging to this account. If no distinguished
        folder was found, try as best we can to return the default folder of type 'folder_cls'
        """
        if not folder_cls.DISTINGUISHED_FOLDER_ID:
            raise ValueError("'folder_cls' %s must have a DISTINGUISHED_FOLDER_ID value" % folder_cls)
        # Use cached distinguished folder instance, but only if cache has already been prepped. This is an optimization
        # for accessing e.g. 'account.contacts' without fetching all folders of the account.
        if self._subfolders is not None:
            for f in self._folders_map.values():
                # Require exact class, to not match subclasses, e.g. RecipientCache instead of Contacts
                if f.__class__ == folder_cls and f.is_distinguished:
                    log.debug('Found cached distinguished %s folder', folder_cls)
                    return f
        try:
            log.debug('Requesting distinguished %s folder explicitly', folder_cls)
            return folder_cls.get_distinguished(root=self)
        except ErrorAccessDenied:
            # Maybe we just don't have GetFolder access? Try FindItems instead
            log.debug('Testing default %s folder with FindItem', folder_cls)
            fld = folder_cls(root=self, name=folder_cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
            fld.test_access()
            return self._folders_map.get(fld.id, fld)  # Use cached instance if available
        except MISSING_FOLDER_ERRORS:
            # The Exchange server does not return a distinguished folder of this type
            pass
        raise ErrorFolderNotFound('No usable default %s folders' % folder_cls)

    @property
    def _folders_map(self):
        if self._subfolders is not None:
            return self._subfolders

        with self._subfolders_lock:
            # Map root, and all subfolders of root, at arbitrary depth by folder ID. First get distinguished folders,
            # so we are sure to apply the correct Folder class, then fetch all subfolders of this root.
            folders_map = {self.id: self}
            distinguished_folders = [
                cls(root=self, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
                for cls in self.WELLKNOWN_FOLDERS
                if cls.get_folder_allowed and cls.supports_version(self.account.version)
            ]
            for f in FolderCollection(account=self.account, folders=distinguished_folders).resolve():
                if isinstance(f, MISSING_FOLDER_ERRORS):
                    # This is just a distinguished folder the server does not have
                    continue
                if isinstance(f, ErrorInvalidOperation):
                    # This is probably a distinguished folder the server does not have. We previously tested the exact
                    # error message (f.value), but some Exchange servers return localized error messages, so that's not
                    # possible to do reliably.
                    continue
                if isinstance(f, ErrorAccessDenied):
                    # We may not have GetFolder access, either to this folder or at all
                    continue
                if isinstance(f, Exception):
                    raise f
                folders_map[f.id] = f
            for f in SingleFolderQuerySet(account=self.account, folder=self).depth(
                    self.DEFAULT_FOLDER_TRAVERSAL_DEPTH
            ).all():
                if isinstance(f, ErrorAccessDenied):
                    # We may not have FindFolder access, or GetFolder access, either to this folder or at all
                    continue
                if isinstance(f, Exception):
                    raise f
                if f.id in folders_map:
                    # Already exists. Probably a distinguished folder
                    continue
                folders_map[f.id] = f
            self._subfolders = folders_map
            return folders_map

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = cls._kwargs_from_elem(elem=elem, account=account)
        cls._clear(elem)
        return cls(account=account, **kwargs)

    @classmethod
    def folder_cls_from_folder_name(cls, folder_name, locale):
        """Return the folder class that matches a localized folder name.

        :param folder_name:
        :param locale: a string, e.g. 'da_DK'
        """
        for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS:
            if folder_name.lower() in folder_cls.localized_names(locale):
                return folder_cls
        raise KeyError()

    def __repr__(self):
        # Let's not create an infinite loop when printing self.root
        return self.__class__.__name__ + \
               repr((self.account, '[self]', self.name, self.total_count, self.unread_count, self.child_folder_count,
                     self.folder_class, self.id, self.changekey))


class Root(RootOfHierarchy):
    """The root of the standard folder hierarchy."""

    DISTINGUISHED_FOLDER_ID = 'root'
    WELLKNOWN_FOLDERS = WELLKNOWN_FOLDERS_IN_ROOT

    @property
    def tois(self):
        # 'Top of Information Store' is a folder available in some Exchange accounts. It usually contains the
        # distinguished folders belonging to the account (inbox, calendar, trash etc.).
        return self.get_default_folder(MsgFolderRoot)

    def get_default_folder(self, folder_cls):
        try:
            return super().get_default_folder(folder_cls)
        except MISSING_FOLDER_ERRORS:
            pass

        # Try to pick a suitable default folder. we do this by:
        #  1. Searching the full folder list for a folder with the distinguished folder name
        #  2. Searching TOIS for a direct child folder of the same type that is marked as distinguished
        #  3. Searching TOIS for a direct child folder of the same type that is has a localized name
        #  4. Searching root for a direct child folder of the same type that is marked as distinguished
        #  5. Searching root for a direct child folder of the same type that is has a localized name
        log.debug('Searching default %s folder in full folder list', folder_cls)

        for f in self._folders_map.values():
            # Require exact class to not match e.g. RecipientCache instead of Contacts
            if f.__class__ == folder_cls and f.has_distinguished_name:
                log.debug('Found cached %s folder with default distinguished name', folder_cls)
                return f

        # Try direct children of TOIS first, unless we're trying to get the TOIS folder. TOIS might not exist.
        if folder_cls != MsgFolderRoot:
            try:
                return self._get_candidate(folder_cls=folder_cls, folder_coll=self.tois.children)
            except MISSING_FOLDER_ERRORS:
                # No candidates, or TOIS does not exist, or we don't have access
                pass

        # No candidates in TOIS. Try direct children of root.
        return self._get_candidate(folder_cls=folder_cls, folder_coll=self.children)

    def _get_candidate(self, folder_cls, folder_coll):
        # Get a single the folder of the same type in folder_coll
        same_type = [f for f in folder_coll if f.__class__ == folder_cls]
        are_distinguished = [f for f in same_type if f.is_distinguished]
        if are_distinguished:
            candidates = are_distinguished
        else:
            candidates = [f for f in same_type if f.name.lower() in folder_cls.localized_names(self.account.locale)]
        if candidates:
            if len(candidates) > 1:
                raise ValueError(
                    'Multiple possible default %s folders: %s' % (folder_cls, [f.name for f in candidates])
                )
            if candidates[0].is_distinguished:
                log.debug('Found cached distinguished %s folder', folder_cls)
            else:
                log.debug('Found cached %s folder with localized name', folder_cls)
            return candidates[0]
        raise ErrorFolderNotFound('No usable default %s folders' % folder_cls)


class PublicFoldersRoot(RootOfHierarchy):
    """The root of the public folders hierarchy. Not available on all mailboxes."""

    DISTINGUISHED_FOLDER_ID = 'publicfoldersroot'
    DEFAULT_FOLDER_TRAVERSAL_DEPTH = SHALLOW
    supported_from = EXCHANGE_2007_SP1

    def get_children(self, folder):
        # EWS does not allow deep traversal of public folders, so self._folders_map will only populate the top-level
        # subfolders. To traverse public folders at arbitrary depth, we need to get child folders on demand.

        # Let's check if this folder already has any cached children. If so, assume we can just return those.
        children = list(super().get_children(folder=folder))
        if children:
            # Return a generator like our parent does
            yield from children
            return

        # Also return early if the server told us that there are no child folders.
        if folder.child_folder_count == 0:
            return

        children_map = {}
        try:
            for f in SingleFolderQuerySet(account=self.account, folder=folder).depth(
                    self.DEFAULT_FOLDER_TRAVERSAL_DEPTH
            ).all():
                if isinstance(f, Exception):
                    raise f
                children_map[f.id] = f
        except ErrorAccessDenied:
            # No access to this folder
            pass

        # Let's update the cache atomically, to avoid partial reads of the cache.
        with self._subfolders_lock:
            self._subfolders.update(children_map)

        # Child folders have been cached now. Try super().get_children() again.
        yield from super().get_children(folder=folder)


class ArchiveRoot(RootOfHierarchy):
    """The root of the archive folders hierarchy. Not available on all mailboxes."""

    DISTINGUISHED_FOLDER_ID = 'archiveroot'
    supported_from = EXCHANGE_2010_SP1
    WELLKNOWN_FOLDERS = WELLKNOWN_FOLDERS_IN_ARCHIVE_ROOT

Classes

class ArchiveRoot (**kwargs)

The root of the archive folders hierarchy. Not available on all mailboxes.

Expand source code
class ArchiveRoot(RootOfHierarchy):
    """The root of the archive folders hierarchy. Not available on all mailboxes."""

    DISTINGUISHED_FOLDER_ID = 'archiveroot'
    supported_from = EXCHANGE_2010_SP1
    WELLKNOWN_FOLDERS = WELLKNOWN_FOLDERS_IN_ARCHIVE_ROOT

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var WELLKNOWN_FOLDERS
var supported_from

Inherited members

class PublicFoldersRoot (**kwargs)

The root of the public folders hierarchy. Not available on all mailboxes.

Expand source code
class PublicFoldersRoot(RootOfHierarchy):
    """The root of the public folders hierarchy. Not available on all mailboxes."""

    DISTINGUISHED_FOLDER_ID = 'publicfoldersroot'
    DEFAULT_FOLDER_TRAVERSAL_DEPTH = SHALLOW
    supported_from = EXCHANGE_2007_SP1

    def get_children(self, folder):
        # EWS does not allow deep traversal of public folders, so self._folders_map will only populate the top-level
        # subfolders. To traverse public folders at arbitrary depth, we need to get child folders on demand.

        # Let's check if this folder already has any cached children. If so, assume we can just return those.
        children = list(super().get_children(folder=folder))
        if children:
            # Return a generator like our parent does
            yield from children
            return

        # Also return early if the server told us that there are no child folders.
        if folder.child_folder_count == 0:
            return

        children_map = {}
        try:
            for f in SingleFolderQuerySet(account=self.account, folder=folder).depth(
                    self.DEFAULT_FOLDER_TRAVERSAL_DEPTH
            ).all():
                if isinstance(f, Exception):
                    raise f
                children_map[f.id] = f
        except ErrorAccessDenied:
            # No access to this folder
            pass

        # Let's update the cache atomically, to avoid partial reads of the cache.
        with self._subfolders_lock:
            self._subfolders.update(children_map)

        # Child folders have been cached now. Try super().get_children() again.
        yield from super().get_children(folder=folder)

Ancestors

Class variables

var DEFAULT_FOLDER_TRAVERSAL_DEPTH
var DISTINGUISHED_FOLDER_ID
var supported_from

Methods

def get_children(self, folder)
Expand source code
def get_children(self, folder):
    # EWS does not allow deep traversal of public folders, so self._folders_map will only populate the top-level
    # subfolders. To traverse public folders at arbitrary depth, we need to get child folders on demand.

    # Let's check if this folder already has any cached children. If so, assume we can just return those.
    children = list(super().get_children(folder=folder))
    if children:
        # Return a generator like our parent does
        yield from children
        return

    # Also return early if the server told us that there are no child folders.
    if folder.child_folder_count == 0:
        return

    children_map = {}
    try:
        for f in SingleFolderQuerySet(account=self.account, folder=folder).depth(
                self.DEFAULT_FOLDER_TRAVERSAL_DEPTH
        ).all():
            if isinstance(f, Exception):
                raise f
            children_map[f.id] = f
    except ErrorAccessDenied:
        # No access to this folder
        pass

    # Let's update the cache atomically, to avoid partial reads of the cache.
    with self._subfolders_lock:
        self._subfolders.update(children_map)

    # Child folders have been cached now. Try super().get_children() again.
    yield from super().get_children(folder=folder)

Inherited members

class Root (**kwargs)

The root of the standard folder hierarchy.

Expand source code
class Root(RootOfHierarchy):
    """The root of the standard folder hierarchy."""

    DISTINGUISHED_FOLDER_ID = 'root'
    WELLKNOWN_FOLDERS = WELLKNOWN_FOLDERS_IN_ROOT

    @property
    def tois(self):
        # 'Top of Information Store' is a folder available in some Exchange accounts. It usually contains the
        # distinguished folders belonging to the account (inbox, calendar, trash etc.).
        return self.get_default_folder(MsgFolderRoot)

    def get_default_folder(self, folder_cls):
        try:
            return super().get_default_folder(folder_cls)
        except MISSING_FOLDER_ERRORS:
            pass

        # Try to pick a suitable default folder. we do this by:
        #  1. Searching the full folder list for a folder with the distinguished folder name
        #  2. Searching TOIS for a direct child folder of the same type that is marked as distinguished
        #  3. Searching TOIS for a direct child folder of the same type that is has a localized name
        #  4. Searching root for a direct child folder of the same type that is marked as distinguished
        #  5. Searching root for a direct child folder of the same type that is has a localized name
        log.debug('Searching default %s folder in full folder list', folder_cls)

        for f in self._folders_map.values():
            # Require exact class to not match e.g. RecipientCache instead of Contacts
            if f.__class__ == folder_cls and f.has_distinguished_name:
                log.debug('Found cached %s folder with default distinguished name', folder_cls)
                return f

        # Try direct children of TOIS first, unless we're trying to get the TOIS folder. TOIS might not exist.
        if folder_cls != MsgFolderRoot:
            try:
                return self._get_candidate(folder_cls=folder_cls, folder_coll=self.tois.children)
            except MISSING_FOLDER_ERRORS:
                # No candidates, or TOIS does not exist, or we don't have access
                pass

        # No candidates in TOIS. Try direct children of root.
        return self._get_candidate(folder_cls=folder_cls, folder_coll=self.children)

    def _get_candidate(self, folder_cls, folder_coll):
        # Get a single the folder of the same type in folder_coll
        same_type = [f for f in folder_coll if f.__class__ == folder_cls]
        are_distinguished = [f for f in same_type if f.is_distinguished]
        if are_distinguished:
            candidates = are_distinguished
        else:
            candidates = [f for f in same_type if f.name.lower() in folder_cls.localized_names(self.account.locale)]
        if candidates:
            if len(candidates) > 1:
                raise ValueError(
                    'Multiple possible default %s folders: %s' % (folder_cls, [f.name for f in candidates])
                )
            if candidates[0].is_distinguished:
                log.debug('Found cached distinguished %s folder', folder_cls)
            else:
                log.debug('Found cached %s folder with localized name', folder_cls)
            return candidates[0]
        raise ErrorFolderNotFound('No usable default %s folders' % folder_cls)

Ancestors

Class variables

var DISTINGUISHED_FOLDER_ID
var WELLKNOWN_FOLDERS

Instance variables

var tois
Expand source code
@property
def tois(self):
    # 'Top of Information Store' is a folder available in some Exchange accounts. It usually contains the
    # distinguished folders belonging to the account (inbox, calendar, trash etc.).
    return self.get_default_folder(MsgFolderRoot)

Inherited members

class RootOfHierarchy (**kwargs)

Base class for folders that implement the root of a folder hierarchy.

Expand source code
class RootOfHierarchy(BaseFolder, metaclass=EWSMeta):
    """Base class for folders that implement the root of a folder hierarchy."""

    # A list of wellknown, or "distinguished", folders that are belong in this folder hierarchy. See
    # https://docs.microsoft.com/en-us/dotnet/api/microsoft.exchange.webservices.data.wellknownfoldername
    # and https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid
    # 'RootOfHierarchy' subclasses must not be in this list.
    WELLKNOWN_FOLDERS = []

    _subfolders_lock = Lock()

    # This folder type also has 'folder:PermissionSet' on some server versions, but requesting it sometimes causes
    # 'ErrorAccessDenied', as reported by some users. Ignore it entirely for root folders - it's usefulness is
    # deemed minimal at best.
    effective_rights = EffectiveRightsField(field_uri='folder:EffectiveRights', is_read_only=True,
                                            supported_from=EXCHANGE_2007_SP1)

    __slots__ = '_account', '_subfolders'

    # A special folder that acts as the top of a folder hierarchy. Finds and caches subfolders at arbitrary depth.
    def __init__(self, **kwargs):
        self._account = kwargs.pop('account', None)  # A pointer back to the account holding the folder hierarchy
        super().__init__(**kwargs)
        self._subfolders = None  # See self._folders_map()

    @property
    def account(self):
        return self._account

    @property
    def root(self):
        return self

    @property
    def parent(self):
        return None

    @classmethod
    def register(cls, *args, **kwargs):
        if cls is not RootOfHierarchy:
            raise TypeError('For folder roots, custom fields must be registered on the RootOfHierarchy class')
        return super().register(*args, **kwargs)

    @classmethod
    def deregister(cls, *args, **kwargs):
        if cls is not RootOfHierarchy:
            raise TypeError('For folder roots, custom fields must be registered on the RootOfHierarchy class')
        return super().deregister(*args, **kwargs)

    def get_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        return self._folders_map.get(folder.id, None)

    def add_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        self._folders_map[folder.id] = folder

    def update_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        self._folders_map[folder.id] = folder

    def remove_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        try:
            del self._folders_map[folder.id]
        except KeyError:
            pass

    def clear_cache(self):
        with self._subfolders_lock:
            self._subfolders = None

    def get_children(self, folder):
        for f in self._folders_map.values():
            if not f.parent:
                continue
            if f.parent.id == folder.id:
                yield f

    @classmethod
    def get_distinguished(cls, account):
        """Get the distinguished folder for this folder class.

        :param account:
        """
        if not cls.DISTINGUISHED_FOLDER_ID:
            raise ValueError('Class %s must have a DISTINGUISHED_FOLDER_ID value' % cls)
        try:
            return cls.resolve(
                account=account,
                folder=cls(account=account, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
            )
        except MISSING_FOLDER_ERRORS:
            raise ErrorFolderNotFound('Could not find distinguished folder %s' % cls.DISTINGUISHED_FOLDER_ID)

    def get_default_folder(self, folder_cls):
        """Return the distinguished folder instance of type folder_cls belonging to this account. If no distinguished
        folder was found, try as best we can to return the default folder of type 'folder_cls'
        """
        if not folder_cls.DISTINGUISHED_FOLDER_ID:
            raise ValueError("'folder_cls' %s must have a DISTINGUISHED_FOLDER_ID value" % folder_cls)
        # Use cached distinguished folder instance, but only if cache has already been prepped. This is an optimization
        # for accessing e.g. 'account.contacts' without fetching all folders of the account.
        if self._subfolders is not None:
            for f in self._folders_map.values():
                # Require exact class, to not match subclasses, e.g. RecipientCache instead of Contacts
                if f.__class__ == folder_cls and f.is_distinguished:
                    log.debug('Found cached distinguished %s folder', folder_cls)
                    return f
        try:
            log.debug('Requesting distinguished %s folder explicitly', folder_cls)
            return folder_cls.get_distinguished(root=self)
        except ErrorAccessDenied:
            # Maybe we just don't have GetFolder access? Try FindItems instead
            log.debug('Testing default %s folder with FindItem', folder_cls)
            fld = folder_cls(root=self, name=folder_cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
            fld.test_access()
            return self._folders_map.get(fld.id, fld)  # Use cached instance if available
        except MISSING_FOLDER_ERRORS:
            # The Exchange server does not return a distinguished folder of this type
            pass
        raise ErrorFolderNotFound('No usable default %s folders' % folder_cls)

    @property
    def _folders_map(self):
        if self._subfolders is not None:
            return self._subfolders

        with self._subfolders_lock:
            # Map root, and all subfolders of root, at arbitrary depth by folder ID. First get distinguished folders,
            # so we are sure to apply the correct Folder class, then fetch all subfolders of this root.
            folders_map = {self.id: self}
            distinguished_folders = [
                cls(root=self, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
                for cls in self.WELLKNOWN_FOLDERS
                if cls.get_folder_allowed and cls.supports_version(self.account.version)
            ]
            for f in FolderCollection(account=self.account, folders=distinguished_folders).resolve():
                if isinstance(f, MISSING_FOLDER_ERRORS):
                    # This is just a distinguished folder the server does not have
                    continue
                if isinstance(f, ErrorInvalidOperation):
                    # This is probably a distinguished folder the server does not have. We previously tested the exact
                    # error message (f.value), but some Exchange servers return localized error messages, so that's not
                    # possible to do reliably.
                    continue
                if isinstance(f, ErrorAccessDenied):
                    # We may not have GetFolder access, either to this folder or at all
                    continue
                if isinstance(f, Exception):
                    raise f
                folders_map[f.id] = f
            for f in SingleFolderQuerySet(account=self.account, folder=self).depth(
                    self.DEFAULT_FOLDER_TRAVERSAL_DEPTH
            ).all():
                if isinstance(f, ErrorAccessDenied):
                    # We may not have FindFolder access, or GetFolder access, either to this folder or at all
                    continue
                if isinstance(f, Exception):
                    raise f
                if f.id in folders_map:
                    # Already exists. Probably a distinguished folder
                    continue
                folders_map[f.id] = f
            self._subfolders = folders_map
            return folders_map

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = cls._kwargs_from_elem(elem=elem, account=account)
        cls._clear(elem)
        return cls(account=account, **kwargs)

    @classmethod
    def folder_cls_from_folder_name(cls, folder_name, locale):
        """Return the folder class that matches a localized folder name.

        :param folder_name:
        :param locale: a string, e.g. 'da_DK'
        """
        for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS:
            if folder_name.lower() in folder_cls.localized_names(locale):
                return folder_cls
        raise KeyError()

    def __repr__(self):
        # Let's not create an infinite loop when printing self.root
        return self.__class__.__name__ + \
               repr((self.account, '[self]', self.name, self.total_count, self.unread_count, self.child_folder_count,
                     self.folder_class, self.id, self.changekey))

Ancestors

Subclasses

Class variables

var FIELDS
var WELLKNOWN_FOLDERS

Static methods

def folder_cls_from_folder_name(folder_name, locale)

Return the folder class that matches a localized folder name.

:param folder_name: :param locale: a string, e.g. 'da_DK'

Expand source code
@classmethod
def folder_cls_from_folder_name(cls, folder_name, locale):
    """Return the folder class that matches a localized folder name.

    :param folder_name:
    :param locale: a string, e.g. 'da_DK'
    """
    for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS:
        if folder_name.lower() in folder_cls.localized_names(locale):
            return folder_cls
    raise KeyError()
def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    kwargs = cls._kwargs_from_elem(elem=elem, account=account)
    cls._clear(elem)
    return cls(account=account, **kwargs)
def get_distinguished(account)

Get the distinguished folder for this folder class.

:param account:

Expand source code
@classmethod
def get_distinguished(cls, account):
    """Get the distinguished folder for this folder class.

    :param account:
    """
    if not cls.DISTINGUISHED_FOLDER_ID:
        raise ValueError('Class %s must have a DISTINGUISHED_FOLDER_ID value' % cls)
    try:
        return cls.resolve(
            account=account,
            folder=cls(account=account, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
        )
    except MISSING_FOLDER_ERRORS:
        raise ErrorFolderNotFound('Could not find distinguished folder %s' % cls.DISTINGUISHED_FOLDER_ID)

Instance variables

var account
Expand source code
@property
def account(self):
    return self._account
var effective_rights
var parent
Expand source code
@property
def parent(self):
    return None
var root
Expand source code
@property
def root(self):
    return self

Methods

def add_folder(self, folder)
Expand source code
def add_folder(self, folder):
    if not folder.id:
        raise ValueError("'folder' must have an ID")
    self._folders_map[folder.id] = folder
def clear_cache(self)
Expand source code
def clear_cache(self):
    with self._subfolders_lock:
        self._subfolders = None
def get_children(self, folder)
Expand source code
def get_children(self, folder):
    for f in self._folders_map.values():
        if not f.parent:
            continue
        if f.parent.id == folder.id:
            yield f
def get_default_folder(self, folder_cls)

Return the distinguished folder instance of type folder_cls belonging to this account. If no distinguished folder was found, try as best we can to return the default folder of type 'folder_cls'

Expand source code
def get_default_folder(self, folder_cls):
    """Return the distinguished folder instance of type folder_cls belonging to this account. If no distinguished
    folder was found, try as best we can to return the default folder of type 'folder_cls'
    """
    if not folder_cls.DISTINGUISHED_FOLDER_ID:
        raise ValueError("'folder_cls' %s must have a DISTINGUISHED_FOLDER_ID value" % folder_cls)
    # Use cached distinguished folder instance, but only if cache has already been prepped. This is an optimization
    # for accessing e.g. 'account.contacts' without fetching all folders of the account.
    if self._subfolders is not None:
        for f in self._folders_map.values():
            # Require exact class, to not match subclasses, e.g. RecipientCache instead of Contacts
            if f.__class__ == folder_cls and f.is_distinguished:
                log.debug('Found cached distinguished %s folder', folder_cls)
                return f
    try:
        log.debug('Requesting distinguished %s folder explicitly', folder_cls)
        return folder_cls.get_distinguished(root=self)
    except ErrorAccessDenied:
        # Maybe we just don't have GetFolder access? Try FindItems instead
        log.debug('Testing default %s folder with FindItem', folder_cls)
        fld = folder_cls(root=self, name=folder_cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
        fld.test_access()
        return self._folders_map.get(fld.id, fld)  # Use cached instance if available
    except MISSING_FOLDER_ERRORS:
        # The Exchange server does not return a distinguished folder of this type
        pass
    raise ErrorFolderNotFound('No usable default %s folders' % folder_cls)
def get_folder(self, folder)
Expand source code
def get_folder(self, folder):
    if not folder.id:
        raise ValueError("'folder' must have an ID")
    return self._folders_map.get(folder.id, None)
def remove_folder(self, folder)
Expand source code
def remove_folder(self, folder):
    if not folder.id:
        raise ValueError("'folder' must have an ID")
    try:
        del self._folders_map[folder.id]
    except KeyError:
        pass
def update_folder(self, folder)
Expand source code
def update_folder(self, folder):
    if not folder.id:
        raise ValueError("'folder' must have an ID")
    self._folders_map[folder.id] = folder

Inherited members

exchangelib-4.6.1/docs/exchangelib/index.html000066400000000000000000023677021414601472700212200ustar00rootroot00000000000000 exchangelib API documentation

Package exchangelib

Expand source code
import sys

from .account import Account, Identity
from .attachments import FileAttachment, ItemAttachment
from .autodiscover import discover
from .configuration import Configuration
from .credentials import DELEGATE, IMPERSONATION, Credentials, OAuth2Credentials, \
    OAuth2AuthorizationCodeCredentials
from .ewsdatetime import EWSDate, EWSDateTime, EWSTimeZone, UTC, UTC_NOW
from .extended_properties import ExtendedProperty
from .folders import Folder, RootOfHierarchy, FolderCollection, SHALLOW, DEEP
from .items import AcceptItem, TentativelyAcceptItem, DeclineItem, CalendarItem, CancelCalendarItem, Contact, \
    DistributionList, Message, PostItem, Task, ForwardItem, ReplyToItem, ReplyAllToItem
from .properties import Body, HTMLBody, ItemId, Mailbox, Attendee, Room, RoomList, UID, DLMailbox
from .protocol import FaultTolerance, FailFast, BaseProtocol, NoVerifyHTTPAdapter, TLSClientAuth
from .restriction import Q
from .settings import OofSettings
from .transport import BASIC, DIGEST, NTLM, GSSAPI, SSPI, OAUTH2, CBA
from .version import Build, Version

__version__ = '4.6.1'

__all__ = [
    '__version__',
    'Account', 'Identity',
    'FileAttachment', 'ItemAttachment',
    'discover',
    'Configuration',
    'DELEGATE', 'IMPERSONATION', 'Credentials', 'OAuth2AuthorizationCodeCredentials', 'OAuth2Credentials',
    'EWSDate', 'EWSDateTime', 'EWSTimeZone', 'UTC', 'UTC_NOW',
    'ExtendedProperty',
    'Folder', 'RootOfHierarchy', 'FolderCollection', 'SHALLOW', 'DEEP',
    'AcceptItem', 'TentativelyAcceptItem', 'DeclineItem', 'CalendarItem', 'CancelCalendarItem', 'Contact',
    'DistributionList', 'Message', 'PostItem', 'Task', 'ForwardItem', 'ReplyToItem', 'ReplyAllToItem',
    'ItemId', 'Mailbox', 'DLMailbox', 'Attendee', 'Room', 'RoomList', 'Body', 'HTMLBody', 'UID',
    'FailFast', 'FaultTolerance', 'BaseProtocol', 'NoVerifyHTTPAdapter', 'TLSClientAuth',
    'OofSettings',
    'Q',
    'BASIC', 'DIGEST', 'NTLM', 'GSSAPI', 'SSPI', 'OAUTH2', 'CBA',
    'Build', 'Version',
]

# Set a default user agent, e.g. "exchangelib/3.1.1 (python-requests/2.22.0)"
import requests.utils
BaseProtocol.USERAGENT = "%s/%s (%s)" % (__name__, __version__, requests.utils.default_user_agent())

# Support fromisoformat() in Python < 3.7
if sys.version_info[:2] < (3, 7):
    from backports.datetime_fromisoformat import MonkeyPatch
    MonkeyPatch.patch_fromisoformat()


def close_connections():
    from .autodiscover import close_connections as close_autodiscover_connections
    from .protocol import close_connections as close_protocol_connections
    close_autodiscover_connections()
    close_protocol_connections()

Sub-modules

exchangelib.account
exchangelib.attachments
exchangelib.autodiscover
exchangelib.configuration
exchangelib.credentials

Implements an Exchange user object and access types. Exchange provides two different ways of granting access for a login to a specific account …

exchangelib.errors

Stores errors specific to this package, and mirrors all the possible errors that EWS can return.

exchangelib.ewsdatetime
exchangelib.extended_properties
exchangelib.fields
exchangelib.folders
exchangelib.indexed_properties
exchangelib.items
exchangelib.properties
exchangelib.protocol

A protocol is an endpoint for EWS service connections. It contains all necessary information to make HTTPS connections …

exchangelib.queryset
exchangelib.recurrence
exchangelib.restriction
exchangelib.services

Implement a selection of EWS services (operations) …

exchangelib.settings
exchangelib.transport
exchangelib.util
exchangelib.version
exchangelib.winzone

A dict to translate from IANA location name to Windows timezone name. Translations taken from …

Functions

def UTC_NOW()
Expand source code
UTC_NOW = lambda: EWSDateTime.now(tz=UTC)  # noqa: E731
def discover(email, credentials=None, auth_type=None, retry_policy=None)
Expand source code
def discover(email, credentials=None, auth_type=None, retry_policy=None):
    return Autodiscovery(
        email=email, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
    ).discover()

Classes

class AcceptItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/acceptitem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class AcceptItem(BaseMeetingReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/acceptitem"""

    ELEMENT_NAME = 'AcceptItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

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) :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:

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):
        """

        :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))
        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)

        # 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)

    @property
    def primary_smtp_address(self):
        return self.identity.primary_smtp_address

    @threaded_cached_property
    def admin_audit_logs(self):
        return self.root.get_default_folder(AdminAuditLogs)

    @threaded_cached_property
    def archive_deleted_items(self):
        return self.archive_root.get_default_folder(ArchiveDeletedItems)

    @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

    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_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
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_purges
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_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_versions
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_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 calendar
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 conflicts
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 contacts
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 conversation_history
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 delegates

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

Expand source code
@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
var directory
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 domain
Expand source code
@property
def domain(self):
    return get_domain(self.primary_smtp_address)
var drafts
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 favorites
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 im_contact_list
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 inbox
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 journal
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 junk
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 local_failures
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 mail_tips

See self.oof_settings about caching considerations.

Expand source code
@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',
    )
var 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 my_contacts
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 notes
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 oof_settings
Expand source code
@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),
    )
var outbox
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 people_connect
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 primary_smtp_address
Expand source code
@property
def primary_smtp_address(self):
    return self.identity.primary_smtp_address
var public_folders_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 quick_contacts
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 recipient_cache
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 recoverable_items_deletions
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 recoverable_items_purges
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 recoverable_items_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 recoverable_items_versions
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 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 search_folders
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 sent
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 server_failures
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 sync_issues
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 tasks
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))
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 trash
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 voice_mail
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))

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
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_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
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_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):
    """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_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):
    """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_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
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 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
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_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
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_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):
    """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 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
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 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
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

Expand source code
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)
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
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={})
    )
class Attendee (**kwargs)
Expand source code
class Attendee(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/attendee"""

    ELEMENT_NAME = 'Attendee'
    RESPONSE_TYPES = {'Unknown', 'Organizer', 'Tentative', 'Accept', 'Decline', 'NoResponseReceived'}

    mailbox = MailboxField(is_required=True)
    response_type = ChoiceField(field_uri='ResponseType', choices={Choice(c) for c in RESPONSE_TYPES},
                                default='Unknown')
    last_response_time = DateTimeField(field_uri='LastResponseTime')

    def __hash__(self):
        return hash(self.mailbox)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var RESPONSE_TYPES

Instance variables

var last_response_time
var mailbox
var response_type

Inherited members

class BaseProtocol (config)

Base class for Protocol which implements the bare essentials.

Expand source code
class BaseProtocol:
    """Base class for Protocol which implements the bare essentials."""

    # The maximum number of sessions (== TCP connections, see below) we will open to this service endpoint. Keep this
    # low unless you have an agreement with the Exchange admin on the receiving end to hammer the server and
    # rate-limiting policies have been disabled for the connecting user. Changing this setting only makes sense if
    # you are using a thread pool to run multiple concurrent workers in this process.
    SESSION_POOLSIZE = 1
    # We want only 1 TCP connection per Session object. We may have lots of different credentials hitting the server and
    # each credential needs its own session (NTLM auth will only send credentials once and then secure the connection,
    # so a connection can only handle requests for one credential). Having multiple connections per Session could
    # quickly exhaust the maximum number of concurrent connections the Exchange server allows from one client.
    CONNECTIONS_PER_SESSION = 1
    # The number of times a session may be reused before creating a new session object. 'None' means "infinite".
    # Discarding sessions after a certain number of usages may limit memory leaks in the Session object.
    MAX_SESSION_USAGE_COUNT = None
    # Timeout for HTTP requests
    TIMEOUT = 120

    # The adapter class to use for HTTP requests. Override this if you need e.g. proxy support or specific TLS versions
    HTTP_ADAPTER_CLS = requests.adapters.HTTPAdapter

    # The User-Agent header to use for HTTP requests. Override this to set an app-specific one
    USERAGENT = None

    def __init__(self, config):
        from .configuration import Configuration
        if not isinstance(config, Configuration):
            raise ValueError("'config' %r must be a Configuration instance" % config)
        if not config.service_endpoint:
            raise AttributeError("'config.service_endpoint' must be set")
        self.config = config
        self._session_pool_size = 0
        self._session_pool_maxsize = config.max_connections or self.SESSION_POOLSIZE

        # Try to behave nicely with the remote server. We want to keep the connection open between requests.
        # We also want to re-use sessions, to avoid the NTLM auth handshake on every request. We must know the
        # authentication method to create sessions.
        self._session_pool = LifoQueue()
        self._session_pool_lock = Lock()

    @property
    def service_endpoint(self):
        return self.config.service_endpoint

    @property
    def auth_type(self):
        return self.config.auth_type

    @property
    def credentials(self):
        return self.config.credentials

    @credentials.setter
    def credentials(self, value):
        # We are updating credentials, but that doesn't automatically propagate to the session objects. The simplest
        # solution is to just kill the sessions in the pool.
        with self._session_pool_lock:
            self.config._credentials = value
            self.close()

    @property
    def retry_policy(self):
        return self.config.retry_policy

    @property
    def server(self):
        return self.config.server

    def __getstate__(self):
        # The session pool and lock cannot be pickled
        state = self.__dict__.copy()
        del state['_session_pool']
        del state['_session_pool_lock']
        return state

    def __setstate__(self, state):
        # Restore the session pool and lock
        self.__dict__.update(state)
        self._session_pool = LifoQueue()
        self._session_pool_lock = Lock()

    def __del__(self):
        # pylint: disable=bare-except
        try:
            self.close()
        except Exception:  # nosec
            # __del__ should never fail
            pass

    def close(self):
        log.debug('Server %s: Closing sessions', self.server)
        while True:
            try:
                session = self._session_pool.get(block=False)
                self.close_session(session)
                self._session_pool_size -= 1
            except Empty:
                break

    @classmethod
    def get_adapter(cls):
        # We want just one connection per session. No retries, since we wrap all requests in our own retry handler
        return cls.HTTP_ADAPTER_CLS(
            pool_block=True,
            pool_connections=cls.CONNECTIONS_PER_SESSION,
            pool_maxsize=cls.CONNECTIONS_PER_SESSION,
            max_retries=0,
        )

    @property
    def session_pool_size(self):
        return self._session_pool_size

    def increase_poolsize(self):
        """Increases the session pool size. We increase by one session per call."""
        # Create a single session and insert it into the pool. We need to protect this with a lock while we are changing
        # the pool size variable, to avoid race conditions. We must not exceed the pool size limit.
        if self._session_pool_size == self._session_pool_maxsize:
            raise SessionPoolMaxSizeReached('Session pool size cannot be increased further')
        with self._session_pool_lock:
            if self._session_pool_size >= self._session_pool_maxsize:
                log.debug('Session pool size was increased in another thread')
                return
            log.debug('Server %s: Increasing session pool size from %s to %s', self.server, self._session_pool_size,
                      self._session_pool_size + 1)
            self._session_pool.put(self.create_session(), block=False)
            self._session_pool_size += 1

    def decrease_poolsize(self):
        """Decreases the session pool size in response to error messages from the server requesting to rate-limit
        requests. We decrease by one session per call.
        """
        # Take a single session from the pool and discard it. We need to protect this with a lock while we are changing
        # the pool size variable, to avoid race conditions. We must keep at least one session in the pool.
        if self._session_pool_size <= 1:
            raise SessionPoolMinSizeReached('Session pool size cannot be decreased further')
        with self._session_pool_lock:
            if self._session_pool_size <= 1:
                log.debug('Session pool size was decreased in another thread')
                return
            log.warning('Server %s: Decreasing session pool size from %s to %s', self.server, self._session_pool_size,
                        self._session_pool_size - 1)
            session = self.get_session()
            self.close_session(session)
            self._session_pool_size -= 1

    def get_session(self):
        # Try to get a session from the queue. If the queue is empty, try to add one more session to the queue. If the
        # queue is already at its max, wait until a session becomes available.
        _timeout = 60  # Rate-limit messages about session starvation
        try:
            session = self._session_pool.get(block=False)
            log.debug('Server %s: Got session immediately', self.server)
        except Empty:
            try:
                self.increase_poolsize()
            except SessionPoolMaxSizeReached:
                pass
            while True:
                try:
                    log.debug('Server %s: Waiting for session', self.server)
                    session = self._session_pool.get(timeout=_timeout)
                    break
                except Empty:
                    # This is normal when we have many worker threads starving for available sessions
                    log.debug('Server %s: No sessions available for %s seconds', self.server, _timeout)
        log.debug('Server %s: Got session %s', self.server, session.session_id)
        session.usage_count += 1
        return session

    def release_session(self, session):
        # This should never fail, as we don't have more sessions than the queue contains
        log.debug('Server %s: Releasing session %s', self.server, session.session_id)
        if self.MAX_SESSION_USAGE_COUNT and session.usage_count > self.MAX_SESSION_USAGE_COUNT:
            log.debug('Server %s: session %s usage exceeded limit. Discarding', self.server, session.session_id)
            session = self.renew_session(session)
        try:
            self._session_pool.put(session, block=False)
        except Full:
            log.debug('Server %s: Session pool was already full %s', self.server, session.session_id)

    @staticmethod
    def close_session(session):
        session.close()
        del session

    def retire_session(self, session):
        # The session is useless. Close it completely and place a fresh session in the pool
        log.debug('Server %s: Retiring session %s', self.server, session.session_id)
        self.close_session(session)
        self.release_session(self.create_session())

    def renew_session(self, session):
        # The session is useless. Close it completely and place a fresh session in the pool
        log.debug('Server %s: Renewing session %s', self.server, session.session_id)
        self.close_session(session)
        return self.create_session()

    def refresh_credentials(self, session):
        # Credentials need to be refreshed, probably due to an OAuth
        # access token expiring. If we've gotten here, it's because the
        # application didn't provide an OAuth client secret, so we can't
        # handle token refreshing for it.
        with self.credentials.lock:
            if self.credentials.sig() == session.credentials_sig:
                # Credentials have not been refreshed by another thread:
                # they're the same as the session was created with. If
                # this isn't the case, we can just go ahead with a new
                # session using the already-updated credentials.
                self.credentials.refresh(session=session)
        return self.renew_session(session)

    def create_session(self):
        if self.auth_type is None:
            raise ValueError('Cannot create session without knowing the auth type')
        if self.credentials is None:
            if self.auth_type in CREDENTIALS_REQUIRED:
                raise ValueError('Auth type %r requires credentials' % self.auth_type)
            session = self.raw_session(self.service_endpoint)
            session.auth = get_auth_instance(auth_type=self.auth_type)
        else:
            with self.credentials.lock:
                if isinstance(self.credentials, OAuth2Credentials):
                    session = self.create_oauth2_session()
                    # Keep track of the credentials used to create this session. If
                    # and when we need to renew credentials (for example, refreshing
                    # an OAuth access token), this lets us easily determine whether
                    # the credentials have already been refreshed in another thread
                    # by the time this session tries.
                    session.credentials_sig = self.credentials.sig()
                else:
                    if self.auth_type == NTLM and self.credentials.type == self.credentials.EMAIL:
                        username = '\\' + self.credentials.username
                    else:
                        username = self.credentials.username
                    session = self.raw_session(self.service_endpoint)
                    session.auth = get_auth_instance(auth_type=self.auth_type, username=username,
                                                     password=self.credentials.password)

        # Add some extra info
        session.session_id = sum(map(ord, str(os.urandom(100))))  # Used for debugging messages in services
        session.usage_count = 0
        session.protocol = self
        log.debug('Server %s: Created session %s', self.server, session.session_id)
        return session

    def create_oauth2_session(self):
        if self.auth_type != OAUTH2:
            raise ValueError(
                'Auth type must be %r for credentials type %s' % (OAUTH2, self.credentials.__class__.__name__)
            )

        has_token = False
        scope = ['https://outlook.office365.com/.default']
        session_params = {}
        token_params = {}

        if isinstance(self.credentials, OAuth2AuthorizationCodeCredentials):
            # Ask for a refresh token
            scope.append('offline_access')

            # We don't know (or need) the Microsoft tenant ID. Use
            # common/ to let Microsoft select the appropriate tenant
            # for the provided authorization code or refresh token.
            #
            # Suppress looks-like-password warning from Bandit.
            token_url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'  # nosec

            client_params = {}
            has_token = self.credentials.access_token is not None
            if has_token:
                session_params['token'] = self.credentials.access_token
            elif self.credentials.authorization_code is not None:
                token_params['code'] = self.credentials.authorization_code
                self.credentials.authorization_code = None

            if self.credentials.client_id is not None and self.credentials.client_secret is not None:
                # If we're given a client ID and secret, we have enough
                # to refresh access tokens ourselves. In other cases the
                # session will raise TokenExpiredError and we'll need to
                # ask the calling application to refresh the token (that
                # covers cases where the caller doesn't have access to
                # the client secret but is working with a service that
                # can provide it refreshed tokens on a limited basis).
                session_params.update({
                    'auto_refresh_kwargs': {
                        'client_id': self.credentials.client_id,
                        'client_secret': self.credentials.client_secret,
                    },
                    'auto_refresh_url': token_url,
                    'token_updater': self.credentials.on_token_auto_refreshed,
                })
            client = WebApplicationClient(self.credentials.client_id, **client_params)
        else:
            token_url = 'https://login.microsoftonline.com/%s/oauth2/v2.0/token' % self.credentials.tenant_id
            client = BackendApplicationClient(client_id=self.credentials.client_id)

        session = self.raw_session(self.service_endpoint, oauth2_client=client, oauth2_session_params=session_params)
        if not has_token:
            # Fetch the token explicitly -- it doesn't occur implicitly
            token = session.fetch_token(token_url=token_url, client_id=self.credentials.client_id,
                                        client_secret=self.credentials.client_secret, scope=scope,
                                        timeout=self.TIMEOUT, **token_params)
            # Allow the credentials object to update its copy of the new
            # token, and give the application an opportunity to cache it
            self.credentials.on_token_auto_refreshed(token)
        session.auth = get_auth_instance(auth_type=OAUTH2, client=client)

        return session

    @classmethod
    def raw_session(cls, prefix, oauth2_client=None, oauth2_session_params=None):
        if oauth2_client:
            session = OAuth2Session(client=oauth2_client, **(oauth2_session_params or {}))
        else:
            session = requests.sessions.Session()
        session.headers.update(DEFAULT_HEADERS)
        session.headers['User-Agent'] = cls.USERAGENT
        session.mount(prefix, adapter=cls.get_adapter())
        return session

    def __repr__(self):
        return self.__class__.__name__ + repr((self.service_endpoint, self.credentials, self.auth_type))

Subclasses

Class variables

var CONNECTIONS_PER_SESSION
var HTTP_ADAPTER_CLS

The built-in HTTP Adapter for urllib3.

Provides a general-case interface for Requests sessions to contact HTTP and HTTPS urls by implementing the Transport Adapter interface. This class will usually be created by the :class:Session <Session> class under the covers.

:param pool_connections: The number of urllib3 connection pools to cache. :param pool_maxsize: The maximum number of connections to save in the pool. :param max_retries: The maximum number of retries each connection should attempt. Note, this applies only to failed DNS lookups, socket connections and connection timeouts, never to requests where data has made it to the server. By default, Requests does not retry failed connections. If you need granular control over the conditions under which we retry a request, import urllib3's Retry class and pass that instead. :param pool_block: Whether the connection pool should block for connections.

Usage::

import requests s = requests.Session() a = requests.adapters.HTTPAdapter(max_retries=3) s.mount('http://', a)

var MAX_SESSION_USAGE_COUNT
var SESSION_POOLSIZE
var TIMEOUT
var USERAGENT

Static methods

def close_session(session)
Expand source code
@staticmethod
def close_session(session):
    session.close()
    del session
def get_adapter()
Expand source code
@classmethod
def get_adapter(cls):
    # We want just one connection per session. No retries, since we wrap all requests in our own retry handler
    return cls.HTTP_ADAPTER_CLS(
        pool_block=True,
        pool_connections=cls.CONNECTIONS_PER_SESSION,
        pool_maxsize=cls.CONNECTIONS_PER_SESSION,
        max_retries=0,
    )
def raw_session(prefix, oauth2_client=None, oauth2_session_params=None)
Expand source code
@classmethod
def raw_session(cls, prefix, oauth2_client=None, oauth2_session_params=None):
    if oauth2_client:
        session = OAuth2Session(client=oauth2_client, **(oauth2_session_params or {}))
    else:
        session = requests.sessions.Session()
    session.headers.update(DEFAULT_HEADERS)
    session.headers['User-Agent'] = cls.USERAGENT
    session.mount(prefix, adapter=cls.get_adapter())
    return session

Instance variables

var auth_type
Expand source code
@property
def auth_type(self):
    return self.config.auth_type
var credentials
Expand source code
@property
def credentials(self):
    return self.config.credentials
var retry_policy
Expand source code
@property
def retry_policy(self):
    return self.config.retry_policy
var server
Expand source code
@property
def server(self):
    return self.config.server
var service_endpoint
Expand source code
@property
def service_endpoint(self):
    return self.config.service_endpoint
var session_pool_size
Expand source code
@property
def session_pool_size(self):
    return self._session_pool_size

Methods

def close(self)
Expand source code
def close(self):
    log.debug('Server %s: Closing sessions', self.server)
    while True:
        try:
            session = self._session_pool.get(block=False)
            self.close_session(session)
            self._session_pool_size -= 1
        except Empty:
            break
def create_oauth2_session(self)
Expand source code
def create_oauth2_session(self):
    if self.auth_type != OAUTH2:
        raise ValueError(
            'Auth type must be %r for credentials type %s' % (OAUTH2, self.credentials.__class__.__name__)
        )

    has_token = False
    scope = ['https://outlook.office365.com/.default']
    session_params = {}
    token_params = {}

    if isinstance(self.credentials, OAuth2AuthorizationCodeCredentials):
        # Ask for a refresh token
        scope.append('offline_access')

        # We don't know (or need) the Microsoft tenant ID. Use
        # common/ to let Microsoft select the appropriate tenant
        # for the provided authorization code or refresh token.
        #
        # Suppress looks-like-password warning from Bandit.
        token_url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'  # nosec

        client_params = {}
        has_token = self.credentials.access_token is not None
        if has_token:
            session_params['token'] = self.credentials.access_token
        elif self.credentials.authorization_code is not None:
            token_params['code'] = self.credentials.authorization_code
            self.credentials.authorization_code = None

        if self.credentials.client_id is not None and self.credentials.client_secret is not None:
            # If we're given a client ID and secret, we have enough
            # to refresh access tokens ourselves. In other cases the
            # session will raise TokenExpiredError and we'll need to
            # ask the calling application to refresh the token (that
            # covers cases where the caller doesn't have access to
            # the client secret but is working with a service that
            # can provide it refreshed tokens on a limited basis).
            session_params.update({
                'auto_refresh_kwargs': {
                    'client_id': self.credentials.client_id,
                    'client_secret': self.credentials.client_secret,
                },
                'auto_refresh_url': token_url,
                'token_updater': self.credentials.on_token_auto_refreshed,
            })
        client = WebApplicationClient(self.credentials.client_id, **client_params)
    else:
        token_url = 'https://login.microsoftonline.com/%s/oauth2/v2.0/token' % self.credentials.tenant_id
        client = BackendApplicationClient(client_id=self.credentials.client_id)

    session = self.raw_session(self.service_endpoint, oauth2_client=client, oauth2_session_params=session_params)
    if not has_token:
        # Fetch the token explicitly -- it doesn't occur implicitly
        token = session.fetch_token(token_url=token_url, client_id=self.credentials.client_id,
                                    client_secret=self.credentials.client_secret, scope=scope,
                                    timeout=self.TIMEOUT, **token_params)
        # Allow the credentials object to update its copy of the new
        # token, and give the application an opportunity to cache it
        self.credentials.on_token_auto_refreshed(token)
    session.auth = get_auth_instance(auth_type=OAUTH2, client=client)

    return session
def create_session(self)
Expand source code
def create_session(self):
    if self.auth_type is None:
        raise ValueError('Cannot create session without knowing the auth type')
    if self.credentials is None:
        if self.auth_type in CREDENTIALS_REQUIRED:
            raise ValueError('Auth type %r requires credentials' % self.auth_type)
        session = self.raw_session(self.service_endpoint)
        session.auth = get_auth_instance(auth_type=self.auth_type)
    else:
        with self.credentials.lock:
            if isinstance(self.credentials, OAuth2Credentials):
                session = self.create_oauth2_session()
                # Keep track of the credentials used to create this session. If
                # and when we need to renew credentials (for example, refreshing
                # an OAuth access token), this lets us easily determine whether
                # the credentials have already been refreshed in another thread
                # by the time this session tries.
                session.credentials_sig = self.credentials.sig()
            else:
                if self.auth_type == NTLM and self.credentials.type == self.credentials.EMAIL:
                    username = '\\' + self.credentials.username
                else:
                    username = self.credentials.username
                session = self.raw_session(self.service_endpoint)
                session.auth = get_auth_instance(auth_type=self.auth_type, username=username,
                                                 password=self.credentials.password)

    # Add some extra info
    session.session_id = sum(map(ord, str(os.urandom(100))))  # Used for debugging messages in services
    session.usage_count = 0
    session.protocol = self
    log.debug('Server %s: Created session %s', self.server, session.session_id)
    return session
def decrease_poolsize(self)

Decreases the session pool size in response to error messages from the server requesting to rate-limit requests. We decrease by one session per call.

Expand source code
def decrease_poolsize(self):
    """Decreases the session pool size in response to error messages from the server requesting to rate-limit
    requests. We decrease by one session per call.
    """
    # Take a single session from the pool and discard it. We need to protect this with a lock while we are changing
    # the pool size variable, to avoid race conditions. We must keep at least one session in the pool.
    if self._session_pool_size <= 1:
        raise SessionPoolMinSizeReached('Session pool size cannot be decreased further')
    with self._session_pool_lock:
        if self._session_pool_size <= 1:
            log.debug('Session pool size was decreased in another thread')
            return
        log.warning('Server %s: Decreasing session pool size from %s to %s', self.server, self._session_pool_size,
                    self._session_pool_size - 1)
        session = self.get_session()
        self.close_session(session)
        self._session_pool_size -= 1
def get_session(self)
Expand source code
def get_session(self):
    # Try to get a session from the queue. If the queue is empty, try to add one more session to the queue. If the
    # queue is already at its max, wait until a session becomes available.
    _timeout = 60  # Rate-limit messages about session starvation
    try:
        session = self._session_pool.get(block=False)
        log.debug('Server %s: Got session immediately', self.server)
    except Empty:
        try:
            self.increase_poolsize()
        except SessionPoolMaxSizeReached:
            pass
        while True:
            try:
                log.debug('Server %s: Waiting for session', self.server)
                session = self._session_pool.get(timeout=_timeout)
                break
            except Empty:
                # This is normal when we have many worker threads starving for available sessions
                log.debug('Server %s: No sessions available for %s seconds', self.server, _timeout)
    log.debug('Server %s: Got session %s', self.server, session.session_id)
    session.usage_count += 1
    return session
def increase_poolsize(self)

Increases the session pool size. We increase by one session per call.

Expand source code
def increase_poolsize(self):
    """Increases the session pool size. We increase by one session per call."""
    # Create a single session and insert it into the pool. We need to protect this with a lock while we are changing
    # the pool size variable, to avoid race conditions. We must not exceed the pool size limit.
    if self._session_pool_size == self._session_pool_maxsize:
        raise SessionPoolMaxSizeReached('Session pool size cannot be increased further')
    with self._session_pool_lock:
        if self._session_pool_size >= self._session_pool_maxsize:
            log.debug('Session pool size was increased in another thread')
            return
        log.debug('Server %s: Increasing session pool size from %s to %s', self.server, self._session_pool_size,
                  self._session_pool_size + 1)
        self._session_pool.put(self.create_session(), block=False)
        self._session_pool_size += 1
def refresh_credentials(self, session)
Expand source code
def refresh_credentials(self, session):
    # Credentials need to be refreshed, probably due to an OAuth
    # access token expiring. If we've gotten here, it's because the
    # application didn't provide an OAuth client secret, so we can't
    # handle token refreshing for it.
    with self.credentials.lock:
        if self.credentials.sig() == session.credentials_sig:
            # Credentials have not been refreshed by another thread:
            # they're the same as the session was created with. If
            # this isn't the case, we can just go ahead with a new
            # session using the already-updated credentials.
            self.credentials.refresh(session=session)
    return self.renew_session(session)
def release_session(self, session)
Expand source code
def release_session(self, session):
    # This should never fail, as we don't have more sessions than the queue contains
    log.debug('Server %s: Releasing session %s', self.server, session.session_id)
    if self.MAX_SESSION_USAGE_COUNT and session.usage_count > self.MAX_SESSION_USAGE_COUNT:
        log.debug('Server %s: session %s usage exceeded limit. Discarding', self.server, session.session_id)
        session = self.renew_session(session)
    try:
        self._session_pool.put(session, block=False)
    except Full:
        log.debug('Server %s: Session pool was already full %s', self.server, session.session_id)
def renew_session(self, session)
Expand source code
def renew_session(self, session):
    # The session is useless. Close it completely and place a fresh session in the pool
    log.debug('Server %s: Renewing session %s', self.server, session.session_id)
    self.close_session(session)
    return self.create_session()
def retire_session(self, session)
Expand source code
def retire_session(self, session):
    # The session is useless. Close it completely and place a fresh session in the pool
    log.debug('Server %s: Retiring session %s', self.server, session.session_id)
    self.close_session(session)
    self.release_session(self.create_session())
class Body (...)

Helper to mark the 'body' field as a complex attribute.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/body

Expand source code
class Body(str):
    """Helper to mark the 'body' field as a complex attribute.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/body
    """

    body_type = 'Text'

    def __add__(self, other):
        # Make sure Body('') + 'foo' returns a Body type
        return self.__class__(super().__add__(other))

    def __mod__(self, other):
        # Make sure Body('%s') % 'foo' returns a Body type
        return self.__class__(super().__mod__(other))

    def format(self, *args, **kwargs):
        # Make sure Body('{}').format('foo') returns a Body type
        return self.__class__(super().format(*args, **kwargs))

Ancestors

  • builtins.str

Subclasses

Class variables

var body_type

Methods

def format(self, *args, **kwargs)

S.format(args, *kwargs) -> str

Return a formatted version of S, using substitutions from args and kwargs. The substitutions are identified by braces ('{' and '}').

Expand source code
def format(self, *args, **kwargs):
    # Make sure Body('{}').format('foo') returns a Body type
    return self.__class__(super().format(*args, **kwargs))
class Build (major_version, minor_version, major_build=0, minor_build=0)

Holds methods for working with build numbers.

Expand source code
class Build:
    """Holds methods for working with build numbers."""

    # List of build numbers here: https://docs.microsoft.com/en-us/exchange/new-features/build-numbers-and-release-dates
    API_VERSION_MAP = {
        8: {
            0: 'Exchange2007',
            1: 'Exchange2007_SP1',
            2: 'Exchange2007_SP1',
            3: 'Exchange2007_SP1',
        },
        14: {
            0: 'Exchange2010',
            1: 'Exchange2010_SP1',
            2: 'Exchange2010_SP2',
            3: 'Exchange2010_SP2',
        },
        15: {
            0: 'Exchange2013',  # Minor builds starting from 847 are Exchange2013_SP1, see api_version()
            1: 'Exchange2016',
            2: 'Exchange2019',
            20: 'Exchange2016',  # This is Office365. See issue #221
        },
    }

    __slots__ = 'major_version', 'minor_version', 'major_build', 'minor_build'

    def __init__(self, major_version, minor_version, major_build=0, minor_build=0):
        if not isinstance(major_version, int):
            raise ValueError("'major_version' must be an integer")
        if not isinstance(minor_version, int):
            raise ValueError("'minor_version' must be an integer")
        if not isinstance(major_build, int):
            raise ValueError("'major_build' must be an integer")
        if not isinstance(minor_build, int):
            raise ValueError("'minor_build' must be an integer")
        self.major_version = major_version
        self.minor_version = minor_version
        self.major_build = major_build
        self.minor_build = minor_build
        if major_version < 8:
            raise ValueError("Exchange major versions below 8 don't support EWS (%s)" % self)

    @classmethod
    def from_xml(cls, elem):
        xml_elems_map = {
            'major_version': 'MajorVersion',
            'minor_version': 'MinorVersion',
            'major_build': 'MajorBuildNumber',
            'minor_build': 'MinorBuildNumber',
        }
        kwargs = {}
        for k, xml_elem in xml_elems_map.items():
            v = elem.get(xml_elem)
            if v is None:
                raise ValueError()
            kwargs[k] = int(v)  # Also raises ValueError
        return cls(**kwargs)

    @classmethod
    def from_hex_string(cls, s):
        """Parse a server version string as returned in an autodiscover response. The process is described here:
        https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/serverversion-pox#example

        The string is a hex string that, converted to a 32-bit binary, encodes the server version. The rules are:
            * The first 4 bits contain the version number structure version. Can be ignored
            * The next 6 bits contain the major version number
            * The next 6 bits contain the minor version number
            * The next bit contains a flag. Can be ignored
            * The next 15 bits contain the major build number

        :param s:
        """
        bin_s = '{:032b}'.format(int(s, 16))  # Convert string to 32-bit binary string
        major_version = int(bin_s[4:10], 2)
        minor_version = int(bin_s[10:16], 2)
        build_number = int(bin_s[17:32], 2)
        return cls(major_version=major_version, minor_version=minor_version, major_build=build_number)

    def api_version(self):
        if EXCHANGE_2013_SP1 <= self < EXCHANGE_2016:
            return 'Exchange2013_SP1'
        try:
            return self.API_VERSION_MAP[self.major_version][self.minor_version]
        except KeyError:
            raise ValueError('API version for build %s is unknown' % self)

    def fullname(self):
        return VERSIONS[self.api_version()][1]

    def __cmp__(self, other):
        # __cmp__ is not a magic method in Python3. We'll just use it here to implement comparison operators
        c = (self.major_version > other.major_version) - (self.major_version < other.major_version)
        if c != 0:
            return c
        c = (self.minor_version > other.minor_version) - (self.minor_version < other.minor_version)
        if c != 0:
            return c
        c = (self.major_build > other.major_build) - (self.major_build < other.major_build)
        if c != 0:
            return c
        return (self.minor_build > other.minor_build) - (self.minor_build < other.minor_build)

    def __eq__(self, other):
        return self.__cmp__(other) == 0

    def __hash__(self):
        return hash(repr(self))

    def __ne__(self, other):
        return self.__cmp__(other) != 0

    def __lt__(self, other):
        return self.__cmp__(other) < 0

    def __le__(self, other):
        return self.__cmp__(other) <= 0

    def __gt__(self, other):
        return self.__cmp__(other) > 0

    def __ge__(self, other):
        return self.__cmp__(other) >= 0

    def __str__(self):
        return '%s.%s.%s.%s' % (self.major_version, self.minor_version, self.major_build, self.minor_build)

    def __repr__(self):
        return self.__class__.__name__ \
               + repr((self.major_version, self.minor_version, self.major_build, self.minor_build))

Class variables

var API_VERSION_MAP

Static methods

def from_hex_string(s)

Parse a server version string as returned in an autodiscover response. The process is described here: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/serverversion-pox#example

The string is a hex string that, converted to a 32-bit binary, encodes the server version. The rules are: * The first 4 bits contain the version number structure version. Can be ignored * The next 6 bits contain the major version number * The next 6 bits contain the minor version number * The next bit contains a flag. Can be ignored * The next 15 bits contain the major build number

:param s:

Expand source code
@classmethod
def from_hex_string(cls, s):
    """Parse a server version string as returned in an autodiscover response. The process is described here:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/serverversion-pox#example

    The string is a hex string that, converted to a 32-bit binary, encodes the server version. The rules are:
        * The first 4 bits contain the version number structure version. Can be ignored
        * The next 6 bits contain the major version number
        * The next 6 bits contain the minor version number
        * The next bit contains a flag. Can be ignored
        * The next 15 bits contain the major build number

    :param s:
    """
    bin_s = '{:032b}'.format(int(s, 16))  # Convert string to 32-bit binary string
    major_version = int(bin_s[4:10], 2)
    minor_version = int(bin_s[10:16], 2)
    build_number = int(bin_s[17:32], 2)
    return cls(major_version=major_version, minor_version=minor_version, major_build=build_number)
def from_xml(elem)
Expand source code
@classmethod
def from_xml(cls, elem):
    xml_elems_map = {
        'major_version': 'MajorVersion',
        'minor_version': 'MinorVersion',
        'major_build': 'MajorBuildNumber',
        'minor_build': 'MinorBuildNumber',
    }
    kwargs = {}
    for k, xml_elem in xml_elems_map.items():
        v = elem.get(xml_elem)
        if v is None:
            raise ValueError()
        kwargs[k] = int(v)  # Also raises ValueError
    return cls(**kwargs)

Instance variables

var major_build

Return an attribute of instance, which is of type owner.

var major_version

Return an attribute of instance, which is of type owner.

var minor_build

Return an attribute of instance, which is of type owner.

var minor_version

Return an attribute of instance, which is of type owner.

Methods

def api_version(self)
Expand source code
def api_version(self):
    if EXCHANGE_2013_SP1 <= self < EXCHANGE_2016:
        return 'Exchange2013_SP1'
    try:
        return self.API_VERSION_MAP[self.major_version][self.minor_version]
    except KeyError:
        raise ValueError('API version for build %s is unknown' % self)
def fullname(self)
Expand source code
def fullname(self):
    return VERSIONS[self.api_version()][1]
class CalendarItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendaritem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class CalendarItem(Item, AcceptDeclineMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendaritem"""

    ELEMENT_NAME = 'CalendarItem'

    uid = TextField(field_uri='calendar:UID', is_required_after_save=True, is_searchable=False)
    recurrence_id = DateTimeField(field_uri='calendar:RecurrenceId', is_read_only=True)
    start = DateOrDateTimeField(field_uri='calendar:Start', is_required=True)
    end = DateOrDateTimeField(field_uri='calendar:End', is_required=True)
    original_start = DateTimeField(field_uri='calendar:OriginalStart', is_read_only=True)
    is_all_day = BooleanField(field_uri='calendar:IsAllDayEvent', is_required=True, default=False)
    legacy_free_busy_status = FreeBusyStatusField(field_uri='calendar:LegacyFreeBusyStatus', is_required=True,
                                                  default='Busy')
    location = TextField(field_uri='calendar:Location')
    when = TextField(field_uri='calendar:When')
    is_meeting = BooleanField(field_uri='calendar:IsMeeting', is_read_only=True)
    is_cancelled = BooleanField(field_uri='calendar:IsCancelled', is_read_only=True)
    is_recurring = BooleanField(field_uri='calendar:IsRecurring', is_read_only=True)
    meeting_request_was_sent = BooleanField(field_uri='calendar:MeetingRequestWasSent', is_read_only=True)
    is_response_requested = BooleanField(field_uri='calendar:IsResponseRequested', default=None,
                                         is_required_after_save=True, is_searchable=False)
    type = ChoiceField(field_uri='calendar:CalendarItemType', choices={Choice(c) for c in CALENDAR_ITEM_CHOICES},
                       is_read_only=True)
    my_response_type = ChoiceField(field_uri='calendar:MyResponseType', choices={
            Choice(c) for c in Attendee.RESPONSE_TYPES
    }, is_read_only=True)
    organizer = MailboxField(field_uri='calendar:Organizer', is_read_only=True)
    required_attendees = AttendeesField(field_uri='calendar:RequiredAttendees', is_searchable=False)
    optional_attendees = AttendeesField(field_uri='calendar:OptionalAttendees', is_searchable=False)
    resources = AttendeesField(field_uri='calendar:Resources', is_searchable=False)
    conflicting_meeting_count = IntegerField(field_uri='calendar:ConflictingMeetingCount', is_read_only=True)
    adjacent_meeting_count = IntegerField(field_uri='calendar:AdjacentMeetingCount', is_read_only=True)
    conflicting_meetings = EWSElementListField(field_uri='calendar:ConflictingMeetings', value_cls='CalendarItem',
                                               namespace=Item.NAMESPACE, is_read_only=True)
    adjacent_meetings = EWSElementListField(field_uri='calendar:AdjacentMeetings', value_cls='CalendarItem',
                                            namespace=Item.NAMESPACE, is_read_only=True)
    duration = CharField(field_uri='calendar:Duration', is_read_only=True)
    appointment_reply_time = DateTimeField(field_uri='calendar:AppointmentReplyTime', is_read_only=True)
    appointment_sequence_number = IntegerField(field_uri='calendar:AppointmentSequenceNumber', is_read_only=True)
    appointment_state = AppointmentStateField(field_uri='calendar:AppointmentState', is_read_only=True)
    recurrence = RecurrenceField(field_uri='calendar:Recurrence', is_searchable=False)
    first_occurrence = OccurrenceField(field_uri='calendar:FirstOccurrence', value_cls=FirstOccurrence,
                                       is_read_only=True)
    last_occurrence = OccurrenceField(field_uri='calendar:LastOccurrence', value_cls=LastOccurrence,
                                      is_read_only=True)
    modified_occurrences = OccurrenceListField(field_uri='calendar:ModifiedOccurrences', value_cls=Occurrence,
                                               is_read_only=True)
    deleted_occurrences = OccurrenceListField(field_uri='calendar:DeletedOccurrences', value_cls=DeletedOccurrence,
                                              is_read_only=True)
    _meeting_timezone = TimeZoneField(field_uri='calendar:MeetingTimeZone', deprecated_from=EXCHANGE_2010,
                                      is_searchable=False)
    _start_timezone = TimeZoneField(field_uri='calendar:StartTimeZone', supported_from=EXCHANGE_2010,
                                    is_searchable=False)
    _end_timezone = TimeZoneField(field_uri='calendar:EndTimeZone', supported_from=EXCHANGE_2010,
                                  is_searchable=False)
    conference_type = EnumAsIntField(field_uri='calendar:ConferenceType', enum=CONFERENCE_TYPES, min=0,
                                     default=None, is_required_after_save=True)
    allow_new_time_proposal = BooleanField(field_uri='calendar:AllowNewTimeProposal', default=None,
                                           is_required_after_save=True, is_searchable=False)
    is_online_meeting = BooleanField(field_uri='calendar:IsOnlineMeeting', default=None,
                                     is_read_only=True)
    meeting_workspace_url = URIField(field_uri='calendar:MeetingWorkspaceUrl')
    net_show_url = URIField(field_uri='calendar:NetShowUrl')

    def occurrence(self, index):
        """Get an occurrence of a recurring master by index. No query is sent to the server to actually fetch the item.
        Call refresh() on the item do do so.

        Only call this method on a recurring master.

        :param index: The index, which is 1-based

        :return The occurrence
        """
        return self.__class__(
            account=self.account,
            folder=self.folder,
            _id=OccurrenceItemId(id=self.id, changekey=self.changekey, instance_index=index),
        )

    def recurring_master(self):
        """Get the recurring master of an occurrence. No query is sent to the server to actually fetch the item.
        Call refresh() on the item do do so.

        Only call this method on an occurrence of a recurring master.

        :return: The master occurrence
        """
        return self.__class__(
            account=self.account,
            folder=self.folder,
            _id=RecurringMasterItemId(id=self.id, changekey=self.changekey),
        )

    @classmethod
    def timezone_fields(cls):
        return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)]

    def clean_timezone_fields(self, version):
        # Sets proper values on the timezone fields if they are not already set
        if self.start is None:
            start_tz = None
        elif type(self.start) in (EWSDate, datetime.date):
            start_tz = self.account.default_timezone
        else:
            start_tz = self.start.tzinfo
        if self.end is None:
            end_tz = None
        elif type(self.end) in (EWSDate, datetime.date):
            end_tz = self.account.default_timezone
        else:
            end_tz = self.end.tzinfo
        if version.build < EXCHANGE_2010:
            if self._meeting_timezone is None:
                self._meeting_timezone = start_tz
            self._start_timezone = None
            self._end_timezone = None
        else:
            self._meeting_timezone = None
            if self._start_timezone is None:
                self._start_timezone = start_tz
            if self._end_timezone is None:
                self._end_timezone = end_tz

    def clean(self, version=None):
        super().clean(version=version)
        if self.start and self.end and self.end < self.start:
            raise ValueError("'end' must be greater than 'start' (%s -> %s)" % (self.start, self.end))
        if version:
            self.clean_timezone_fields(version=version)

    def cancel(self, **kwargs):
        return CancelCalendarItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            **kwargs
        ).send()

    def _update_fieldnames(self):
        update_fields = super()._update_fieldnames()
        if self.type == OCCURRENCE:
            # Some CalendarItem fields cannot be updated when the item is an occurrence. The values are empty when we
            # receive them so would have been updated because they are set to None.
            update_fields.remove('recurrence')
            update_fields.remove('uid')
        return update_fields

    @classmethod
    def from_xml(cls, elem, account):
        item = super().from_xml(elem=elem, account=account)
        # EWS returns the start and end values as a datetime regardless of the is_all_day status. Convert to date if
        # applicable.
        if not item.is_all_day:
            return item
        for field_name in ('start', 'end'):
            val = getattr(item, field_name)
            if val is None:
                continue
            # Return just the date part of the value. Subtract 1 day from the date if this is the end field. This is
            # the inverse of what we do in .to_xml(). Convert to the local timezone before getting the date.
            if field_name == 'end':
                val -= datetime.timedelta(days=1)
            tz = getattr(item, '_%s_timezone' % field_name)
            setattr(item, field_name, val.astimezone(tz).date())
        return item

    def tz_field_for_field_name(self, field_name):
        meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
        if self.account.version.build < EXCHANGE_2010:
            return meeting_tz_field
        if field_name == 'start':
            return start_tz_field
        if field_name == 'end':
            return end_tz_field
        raise ValueError('Unsupported field_name')

    def date_to_datetime(self, field_name):
        # EWS always expects a datetime. If we have a date value, then convert it to datetime in the local
        # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both
        # start and end values and let EWS apply its logic, but that seems hacky.
        value = getattr(self, field_name)
        tz = getattr(self, self.tz_field_for_field_name(field_name).name)
        value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz)
        if field_name == 'end':
            value += datetime.timedelta(days=1)
        return value

    def to_xml(self, version):
        # EWS has some special logic related to all-day start and end values. Non-midnight start values are pushed to
        # the previous midnight. Non-midnight end values are pushed to the following midnight. Midnight in this context
        # refers to midnight in the local timezone. See
        #
        # https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-create-all-day-events-by-using-ews-in-exchange
        #
        elem = super().to_xml(version=version)
        if not self.is_all_day:
            return elem
        for field_name in ('start', 'end'):
            value = getattr(self, field_name)
            if value is None:
                continue
            if type(value) in (EWSDate, datetime.date):
                # EWS always expects a datetime
                value = self.date_to_datetime(field_name=field_name)
                # We already generated an XML element for this field, but it contains a plain date at this point, which
                # is invalid. Replace the value.
                field = self.get_field_by_fieldname(field_name)
                set_xml_value(elem=elem.find(field.response_tag()), value=value, version=version)
        return elem

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    item = super().from_xml(elem=elem, account=account)
    # EWS returns the start and end values as a datetime regardless of the is_all_day status. Convert to date if
    # applicable.
    if not item.is_all_day:
        return item
    for field_name in ('start', 'end'):
        val = getattr(item, field_name)
        if val is None:
            continue
        # Return just the date part of the value. Subtract 1 day from the date if this is the end field. This is
        # the inverse of what we do in .to_xml(). Convert to the local timezone before getting the date.
        if field_name == 'end':
            val -= datetime.timedelta(days=1)
        tz = getattr(item, '_%s_timezone' % field_name)
        setattr(item, field_name, val.astimezone(tz).date())
    return item
def timezone_fields()
Expand source code
@classmethod
def timezone_fields(cls):
    return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)]

Instance variables

var adjacent_meeting_count
var adjacent_meetings
var allow_new_time_proposal
var appointment_reply_time
var appointment_sequence_number
var appointment_state
var conference_type
var conflicting_meeting_count
var conflicting_meetings
var deleted_occurrences
var duration
var end
var first_occurrence
var is_all_day
var is_cancelled
var is_meeting
var is_online_meeting
var is_recurring
var is_response_requested
var last_occurrence
var legacy_free_busy_status
var location
var meeting_request_was_sent
var meeting_workspace_url
var modified_occurrences
var my_response_type
var net_show_url
var optional_attendees
var organizer
var original_start
var recurrence
var recurrence_id
var required_attendees
var resources
var start
var type
var uid
var when

Methods

def cancel(self, **kwargs)
Expand source code
def cancel(self, **kwargs):
    return CancelCalendarItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        **kwargs
    ).send()
def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)
    if self.start and self.end and self.end < self.start:
        raise ValueError("'end' must be greater than 'start' (%s -> %s)" % (self.start, self.end))
    if version:
        self.clean_timezone_fields(version=version)
def clean_timezone_fields(self, version)
Expand source code
def clean_timezone_fields(self, version):
    # Sets proper values on the timezone fields if they are not already set
    if self.start is None:
        start_tz = None
    elif type(self.start) in (EWSDate, datetime.date):
        start_tz = self.account.default_timezone
    else:
        start_tz = self.start.tzinfo
    if self.end is None:
        end_tz = None
    elif type(self.end) in (EWSDate, datetime.date):
        end_tz = self.account.default_timezone
    else:
        end_tz = self.end.tzinfo
    if version.build < EXCHANGE_2010:
        if self._meeting_timezone is None:
            self._meeting_timezone = start_tz
        self._start_timezone = None
        self._end_timezone = None
    else:
        self._meeting_timezone = None
        if self._start_timezone is None:
            self._start_timezone = start_tz
        if self._end_timezone is None:
            self._end_timezone = end_tz
def date_to_datetime(self, field_name)
Expand source code
def date_to_datetime(self, field_name):
    # EWS always expects a datetime. If we have a date value, then convert it to datetime in the local
    # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both
    # start and end values and let EWS apply its logic, but that seems hacky.
    value = getattr(self, field_name)
    tz = getattr(self, self.tz_field_for_field_name(field_name).name)
    value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz)
    if field_name == 'end':
        value += datetime.timedelta(days=1)
    return value
def occurrence(self, index)

Get an occurrence of a recurring master by index. No query is sent to the server to actually fetch the item. Call refresh() on the item do do so.

Only call this method on a recurring master.

:param index: The index, which is 1-based

:return The occurrence

Expand source code
def occurrence(self, index):
    """Get an occurrence of a recurring master by index. No query is sent to the server to actually fetch the item.
    Call refresh() on the item do do so.

    Only call this method on a recurring master.

    :param index: The index, which is 1-based

    :return The occurrence
    """
    return self.__class__(
        account=self.account,
        folder=self.folder,
        _id=OccurrenceItemId(id=self.id, changekey=self.changekey, instance_index=index),
    )
def recurring_master(self)

Get the recurring master of an occurrence. No query is sent to the server to actually fetch the item. Call refresh() on the item do do so.

Only call this method on an occurrence of a recurring master.

:return: The master occurrence

Expand source code
def recurring_master(self):
    """Get the recurring master of an occurrence. No query is sent to the server to actually fetch the item.
    Call refresh() on the item do do so.

    Only call this method on an occurrence of a recurring master.

    :return: The master occurrence
    """
    return self.__class__(
        account=self.account,
        folder=self.folder,
        _id=RecurringMasterItemId(id=self.id, changekey=self.changekey),
    )
def to_xml(self, version)
Expand source code
def to_xml(self, version):
    # EWS has some special logic related to all-day start and end values. Non-midnight start values are pushed to
    # the previous midnight. Non-midnight end values are pushed to the following midnight. Midnight in this context
    # refers to midnight in the local timezone. See
    #
    # https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-create-all-day-events-by-using-ews-in-exchange
    #
    elem = super().to_xml(version=version)
    if not self.is_all_day:
        return elem
    for field_name in ('start', 'end'):
        value = getattr(self, field_name)
        if value is None:
            continue
        if type(value) in (EWSDate, datetime.date):
            # EWS always expects a datetime
            value = self.date_to_datetime(field_name=field_name)
            # We already generated an XML element for this field, but it contains a plain date at this point, which
            # is invalid. Replace the value.
            field = self.get_field_by_fieldname(field_name)
            set_xml_value(elem=elem.find(field.response_tag()), value=value, version=version)
    return elem
def tz_field_for_field_name(self, field_name)
Expand source code
def tz_field_for_field_name(self, field_name):
    meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
    if self.account.version.build < EXCHANGE_2010:
        return meeting_tz_field
    if field_name == 'start':
        return start_tz_field
    if field_name == 'end':
        return end_tz_field
    raise ValueError('Unsupported field_name')

Inherited members

class CancelCalendarItem (**kwargs)
Expand source code
class CancelCalendarItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/cancelcalendaritem"""

    ELEMENT_NAME = 'CancelCalendarItem'
    author_idx = BaseReplyItem.FIELDS.index_by_name('author')
    FIELDS = BaseReplyItem.FIELDS[:author_idx] + BaseReplyItem.FIELDS[author_idx + 1:]

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var author_idx

Inherited members

class Configuration (credentials=None, server=None, service_endpoint=None, auth_type=None, version=None, retry_policy=None, max_connections=None)

Contains information needed to create an authenticated connection to an EWS endpoint.

The 'credentials' argument contains the credentials needed to authenticate with the server. Multiple credentials implementations are available in 'exchangelib.credentials'.

config = Configuration(credentials=Credentials('john@example.com', 'MY_SECRET'), …)

The 'server' and 'service_endpoint' arguments are mutually exclusive. The former must contain only a domain name, the latter a full URL:

config = Configuration(server='example.com', ...)
config = Configuration(service_endpoint='https://mail.example.com/EWS/Exchange.asmx', ...)

If you know which authentication type the server uses, you add that as a hint in 'auth_type'. Likewise, you can add the server version as a hint. This allows to skip the auth type and version guessing routines:

config = Configuration(auth_type=NTLM, ...)
config = Configuration(version=Version(build=Build(15, 1, 2, 3)), ...)

You can use 'retry_policy' to define a custom retry policy for handling server connection failures:

config = Configuration(retry_policy=FaultTolerance(max_wait=3600), ...)

'max_connections' defines the max number of connections allowed for this server. This may be restricted by policies on the Exchange server.

Expand source code
class Configuration:
    """Contains information needed to create an authenticated connection to an EWS endpoint.

    The 'credentials' argument contains the credentials needed to authenticate with the server. Multiple credentials
    implementations are available in 'exchangelib.credentials'.

    config = Configuration(credentials=Credentials('john@example.com', 'MY_SECRET'), ...)

    The 'server' and 'service_endpoint' arguments are mutually exclusive. The former must contain only a domain name,
    the latter a full URL:

        config = Configuration(server='example.com', ...)
        config = Configuration(service_endpoint='https://mail.example.com/EWS/Exchange.asmx', ...)

    If you know which authentication type the server uses, you add that as a hint in 'auth_type'. Likewise, you can
    add the server version as a hint. This allows to skip the auth type and version guessing routines:

        config = Configuration(auth_type=NTLM, ...)
        config = Configuration(version=Version(build=Build(15, 1, 2, 3)), ...)

    You can use 'retry_policy' to define a custom retry policy for handling server connection failures:

        config = Configuration(retry_policy=FaultTolerance(max_wait=3600), ...)

    'max_connections' defines the max number of connections allowed for this server. This may be restricted by
    policies on the Exchange server.
    """

    def __init__(self, credentials=None, server=None, service_endpoint=None, auth_type=None, version=None,
                 retry_policy=None, max_connections=None):
        if not isinstance(credentials, (BaseCredentials, type(None))):
            raise ValueError("'credentials' %r must be a Credentials instance" % credentials)
        if isinstance(credentials, OAuth2Credentials) and auth_type is None:
            # This type of credentials *must* use the OAuth auth type
            auth_type = OAUTH2
        if server and service_endpoint:
            raise AttributeError("Only one of 'server' or 'service_endpoint' must be provided")
        if auth_type is not None and auth_type not in AUTH_TYPE_MAP:
            raise ValueError("'auth_type' %r must be one of %s"
                             % (auth_type, ', '.join("'%s'" % k for k in sorted(AUTH_TYPE_MAP))))
        if not retry_policy:
            retry_policy = FailFast()
        if not isinstance(version, (Version, type(None))):
            raise ValueError("'version' %r must be a Version instance" % version)
        if not isinstance(retry_policy, RetryPolicy):
            raise ValueError("'retry_policy' %r must be a RetryPolicy instance" % retry_policy)
        if not isinstance(max_connections, (int, type(None))):
            raise ValueError("'max_connections' must be an integer")
        self._credentials = credentials
        if server:
            self.service_endpoint = 'https://%s/EWS/Exchange.asmx' % server
        else:
            self.service_endpoint = service_endpoint
        self.auth_type = auth_type
        self.version = version
        self.retry_policy = retry_policy
        self.max_connections = max_connections

    @property
    def credentials(self):
        # Do not update credentials from this class. Instead, do it from Protocol
        return self._credentials

    @threaded_cached_property
    def server(self):
        if not self.service_endpoint:
            return None
        return split_url(self.service_endpoint)[1]

    def __repr__(self):
        return self.__class__.__name__ + '(%s)' % ', '.join('%s=%r' % (k, getattr(self, k)) for k in (
            'credentials', 'service_endpoint', 'auth_type', 'version', 'retry_policy'
        ))

Instance variables

var credentials
Expand source code
@property
def credentials(self):
    # Do not update credentials from this class. Instead, do it from Protocol
    return self._credentials
var server
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))
class Contact (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/contact

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class Contact(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/contact"""

    ELEMENT_NAME = 'Contact'

    file_as = TextField(field_uri='contacts:FileAs')
    file_as_mapping = ChoiceField(field_uri='contacts:FileAsMapping', choices={
        Choice('None'), Choice('LastCommaFirst'), Choice('FirstSpaceLast'), Choice('Company'),
        Choice('LastCommaFirstCompany'), Choice('CompanyLastFirst'), Choice('LastFirst'),
        Choice('LastFirstCompany'), Choice('CompanyLastCommaFirst'), Choice('LastFirstSuffix'),
        Choice('LastSpaceFirstCompany'), Choice('CompanyLastSpaceFirst'), Choice('LastSpaceFirst'),
        Choice('DisplayName'), Choice('FirstName'), Choice('LastFirstMiddleSuffix'), Choice('LastName'),
        Choice('Empty'),
    })
    display_name = TextField(field_uri='contacts:DisplayName', is_required=True)
    given_name = CharField(field_uri='contacts:GivenName')
    initials = TextField(field_uri='contacts:Initials')
    middle_name = CharField(field_uri='contacts:MiddleName')
    nickname = TextField(field_uri='contacts:Nickname')
    complete_name = EWSElementField(field_uri='contacts:CompleteName', value_cls=CompleteName, is_read_only=True)
    company_name = TextField(field_uri='contacts:CompanyName')
    email_addresses = EmailAddressesField(field_uri='contacts:EmailAddress')
    physical_addresses = PhysicalAddressField(field_uri='contacts:PhysicalAddress')
    phone_numbers = PhoneNumberField(field_uri='contacts:PhoneNumber')
    assistant_name = TextField(field_uri='contacts:AssistantName')
    birthday = DateTimeBackedDateField(field_uri='contacts:Birthday', default_time=datetime.time(11, 59))
    business_homepage = URIField(field_uri='contacts:BusinessHomePage')
    children = TextListField(field_uri='contacts:Children')
    companies = TextListField(field_uri='contacts:Companies', is_searchable=False)
    contact_source = ChoiceField(field_uri='contacts:ContactSource', choices={
        Choice('Store'), Choice('ActiveDirectory')
    }, is_read_only=True)
    department = TextField(field_uri='contacts:Department')
    generation = TextField(field_uri='contacts:Generation')
    im_addresses = CharField(field_uri='contacts:ImAddresses', is_read_only=True)
    job_title = TextField(field_uri='contacts:JobTitle')
    manager = TextField(field_uri='contacts:Manager')
    mileage = TextField(field_uri='contacts:Mileage')
    office = TextField(field_uri='contacts:OfficeLocation')
    postal_address_index = ChoiceField(field_uri='contacts:PostalAddressIndex', choices={
        Choice('Business'), Choice('Home'), Choice('Other'), Choice('None')
    }, default='None', is_required_after_save=True)
    profession = TextField(field_uri='contacts:Profession')
    spouse_name = TextField(field_uri='contacts:SpouseName')
    surname = CharField(field_uri='contacts:Surname')
    wedding_anniversary = DateTimeBackedDateField(field_uri='contacts:WeddingAnniversary',
                                                  default_time=datetime.time(11, 59))
    has_picture = BooleanField(field_uri='contacts:HasPicture', supported_from=EXCHANGE_2010, is_read_only=True)
    phonetic_full_name = TextField(field_uri='contacts:PhoneticFullName', supported_from=EXCHANGE_2010_SP2,
                                   is_read_only=True)
    phonetic_first_name = TextField(field_uri='contacts:PhoneticFirstName', supported_from=EXCHANGE_2010_SP2,
                                    is_read_only=True)
    phonetic_last_name = TextField(field_uri='contacts:PhoneticLastName', supported_from=EXCHANGE_2010_SP2,
                                   is_read_only=True)
    email_alias = EmailAddressField(field_uri='contacts:Alias', is_read_only=True,
                                    supported_from=EXCHANGE_2010_SP2)
    # 'notes' is documented in MSDN but apparently unused. Writing to it raises ErrorInvalidPropertyRequest. OWA
    # put entries into the 'notes' form field into the 'body' field.
    notes = CharField(field_uri='contacts:Notes', supported_from=EXCHANGE_2010_SP2, is_read_only=True)
    # 'photo' is documented in MSDN but apparently unused. Writing to it raises ErrorInvalidPropertyRequest. OWA
    # adds photos as FileAttachments on the contact item (with 'is_contact_photo=True'), which automatically flips
    # the 'has_picture' field.
    photo = Base64Field(field_uri='contacts:Photo', supported_from=EXCHANGE_2010_SP2, is_read_only=True)
    user_smime_certificate = Base64Field(field_uri='contacts:UserSMIMECertificate', supported_from=EXCHANGE_2010_SP2,
                                         is_read_only=True)
    ms_exchange_certificate = Base64Field(field_uri='contacts:MSExchangeCertificate', supported_from=EXCHANGE_2010_SP2,
                                          is_read_only=True)
    directory_id = TextField(field_uri='contacts:DirectoryId', supported_from=EXCHANGE_2010_SP2, is_read_only=True)
    manager_mailbox = MailboxField(field_uri='contacts:ManagerMailbox', supported_from=EXCHANGE_2010_SP2,
                                   is_read_only=True)
    direct_reports = MailboxListField(field_uri='contacts:DirectReports', supported_from=EXCHANGE_2010_SP2,
                                      is_read_only=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var assistant_name
var birthday
var business_homepage
var children
var companies
var company_name
var complete_name
var contact_source
var department
var direct_reports
var directory_id
var display_name
var email_addresses
var email_alias
var file_as
var file_as_mapping
var generation
var given_name
var has_picture
var im_addresses
var initials
var job_title
var manager
var manager_mailbox
var middle_name
var mileage
var ms_exchange_certificate
var nickname
var notes
var office
var phone_numbers
var phonetic_first_name
var phonetic_full_name
var phonetic_last_name
var photo
var physical_addresses
var postal_address_index
var profession
var spouse_name
var surname
var user_smime_certificate
var wedding_anniversary

Inherited members

class Credentials (username, password)

Keeps login info the way Exchange likes it.

Usernames for authentication are of one of these forms: * PrimarySMTPAddress * WINDOMAIN\username * User Principal Name (UPN) password: Clear-text password

Expand source code
class Credentials(BaseCredentials):
    r"""Keeps login info the way Exchange likes it.

    Usernames for authentication are of one of these forms:
    * PrimarySMTPAddress
    * WINDOMAIN\username
    * User Principal Name (UPN)
      password: Clear-text password
    """

    EMAIL = 'email'
    DOMAIN = 'domain'
    UPN = 'upn'

    def __init__(self, username, password):
        super().__init__()
        if username.count('@') == 1:
            self.type = self.EMAIL
        elif username.count('\\') == 1:
            self.type = self.DOMAIN
        else:
            self.type = self.UPN
        self.username = username
        self.password = password

    def refresh(self, session):
        pass

    def __repr__(self):
        return self.__class__.__name__ + repr((self.username, '********'))

    def __str__(self):
        return self.username

Ancestors

Class variables

var DOMAIN
var EMAIL
var UPN

Inherited members

class DLMailbox (**kwargs)

Like Mailbox, but creates elements in the 'messages' namespace when sending requests.

Expand source code
class DLMailbox(Mailbox):
    """Like Mailbox, but creates elements in the 'messages' namespace when sending requests."""

    NAMESPACE = MNS

Ancestors

Class variables

var NAMESPACE

Inherited members

class DeclineItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/declineitem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class DeclineItem(BaseMeetingReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/declineitem"""

    ELEMENT_NAME = 'DeclineItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class DistributionList (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distributionlist

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class DistributionList(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distributionlist"""

    ELEMENT_NAME = 'DistributionList'

    display_name = CharField(field_uri='contacts:DisplayName', is_required=True)
    file_as = CharField(field_uri='contacts:FileAs', is_read_only=True)
    contact_source = ChoiceField(field_uri='contacts:ContactSource', choices={
        Choice('Store'), Choice('ActiveDirectory')
    }, is_read_only=True)
    members = MemberListField(field_uri='distributionlist:Members')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var contact_source
var display_name
var file_as
var members

Inherited members

class EWSDate (...)

Extends the normal date implementation to satisfy EWS.

Expand source code
class EWSDate(datetime.date):
    """Extends the normal date implementation to satisfy EWS."""

    __slots__ = '_year', '_month', '_day', '_hashcode'

    def ewsformat(self):
        """ISO 8601 format to satisfy xs:date as interpreted by EWS. Example: 2009-01-15."""
        return self.isoformat()

    def __add__(self, other):
        dt = super().__add__(other)
        if isinstance(dt, self.__class__):
            return dt
        return self.from_date(dt)  # We want to return EWSDate objects

    def __iadd__(self, other):
        return self + other

    def __sub__(self, other):
        dt = super().__sub__(other)
        if isinstance(dt, datetime.timedelta):
            return dt
        if isinstance(dt, self.__class__):
            return dt
        return self.from_date(dt)  # We want to return EWSDate objects

    def __isub__(self, other):
        return self - other

    @classmethod
    def fromordinal(cls, n):
        dt = super().fromordinal(n)
        if isinstance(dt, cls):
            return dt
        return cls.from_date(dt)  # We want to return EWSDate objects

    @classmethod
    def from_date(cls, d):
        if type(d) is not datetime.date:
            raise ValueError("%r must be a date instance" % d)
        return cls(d.year, d.month, d.day)

    @classmethod
    def from_string(cls, date_string):
        # Sometimes, we'll receive a date string with timezone information. Not very useful.
        if date_string.endswith('Z'):
            date_fmt = '%Y-%m-%dZ'
        elif ':' in date_string:
            if '+' in date_string:
                date_fmt = '%Y-%m-%d+%H:%M'
            else:
                date_fmt = '%Y-%m-%d-%H:%M'
        else:
            date_fmt = '%Y-%m-%d'
        d = datetime.datetime.strptime(date_string, date_fmt).date()
        if isinstance(d, cls):
            return d
        return cls.from_date(d)  # We want to return EWSDate objects

Ancestors

  • datetime.date

Static methods

def from_date(d)
Expand source code
@classmethod
def from_date(cls, d):
    if type(d) is not datetime.date:
        raise ValueError("%r must be a date instance" % d)
    return cls(d.year, d.month, d.day)
def from_string(date_string)
Expand source code
@classmethod
def from_string(cls, date_string):
    # Sometimes, we'll receive a date string with timezone information. Not very useful.
    if date_string.endswith('Z'):
        date_fmt = '%Y-%m-%dZ'
    elif ':' in date_string:
        if '+' in date_string:
            date_fmt = '%Y-%m-%d+%H:%M'
        else:
            date_fmt = '%Y-%m-%d-%H:%M'
    else:
        date_fmt = '%Y-%m-%d'
    d = datetime.datetime.strptime(date_string, date_fmt).date()
    if isinstance(d, cls):
        return d
    return cls.from_date(d)  # We want to return EWSDate objects
def fromordinal(n)

int -> date corresponding to a proleptic Gregorian ordinal.

Expand source code
@classmethod
def fromordinal(cls, n):
    dt = super().fromordinal(n)
    if isinstance(dt, cls):
        return dt
    return cls.from_date(dt)  # We want to return EWSDate objects

Methods

def ewsformat(self)

ISO 8601 format to satisfy xs:date as interpreted by EWS. Example: 2009-01-15.

Expand source code
def ewsformat(self):
    """ISO 8601 format to satisfy xs:date as interpreted by EWS. Example: 2009-01-15."""
    return self.isoformat()
class EWSDateTime (*args, **kwargs)

Extends the normal datetime implementation to satisfy EWS.

Expand source code
class EWSDateTime(datetime.datetime):
    """Extends the normal datetime implementation to satisfy EWS."""

    __slots__ = '_year', '_month', '_day', '_hour', '_minute', '_second', '_microsecond', '_tzinfo', '_hashcode'

    def __new__(cls, *args, **kwargs):
        # pylint: disable=arguments-differ

        if len(args) == 8:
            tzinfo = args[7]
        else:
            tzinfo = kwargs.get('tzinfo')
        if isinstance(tzinfo, zoneinfo.ZoneInfo):
            # Don't allow pytz or dateutil timezones here. They are not safe to use as direct input for datetime()
            tzinfo = EWSTimeZone.from_timezone(tzinfo)
        if not isinstance(tzinfo, (EWSTimeZone, type(None))):
            raise ValueError('tzinfo %r must be an EWSTimeZone instance' % tzinfo)
        if len(args) == 8:
            args = args[:7] + (tzinfo,)
        else:
            kwargs['tzinfo'] = tzinfo
        return super().__new__(cls, *args, **kwargs)

    def ewsformat(self):
        """ISO 8601 format to satisfy xs:datetime as interpreted by EWS. Examples:
        * 2009-01-15T13:45:56Z
        * 2009-01-15T13:45:56+01:00
        """
        if not self.tzinfo:
            raise ValueError('%r must be timezone-aware' % self)
        if self.tzinfo.key == 'UTC':
            if self.microsecond:
                return self.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
            return self.strftime('%Y-%m-%dT%H:%M:%SZ')
        return self.isoformat()

    @classmethod
    def from_datetime(cls, d):
        if type(d) is not datetime.datetime:
            raise ValueError("%r must be a datetime instance" % d)
        if d.tzinfo is None:
            tz = None
        elif isinstance(d.tzinfo, EWSTimeZone):
            tz = d.tzinfo
        else:
            tz = EWSTimeZone.from_timezone(d.tzinfo)
        return cls(d.year, d.month, d.day, d.hour, d.minute, d.second, d.microsecond, tzinfo=tz)

    def astimezone(self, tz=None):
        if tz is None:
            tz = EWSTimeZone.localzone()
        t = super().astimezone(tz=tz).replace(tzinfo=tz)
        if isinstance(t, self.__class__):
            return t
        return self.from_datetime(t)  # We want to return EWSDateTime objects

    def __add__(self, other):
        t = super().__add__(other)
        if isinstance(t, self.__class__):
            return t
        return self.from_datetime(t)  # We want to return EWSDateTime objects

    def __iadd__(self, other):
        return self + other

    def __sub__(self, other):
        t = super().__sub__(other)
        if isinstance(t, datetime.timedelta):
            return t
        if isinstance(t, self.__class__):
            return t
        return self.from_datetime(t)  # We want to return EWSDateTime objects

    def __isub__(self, other):
        return self - other

    @classmethod
    def from_string(cls, date_string):
        # Parses several common datetime formats and returns timezone-aware EWSDateTime objects
        if date_string.endswith('Z'):
            # UTC datetime
            return super().strptime(date_string, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC)
        if len(date_string) == 19:
            # This is probably a naive datetime. Don't allow this, but signal caller with an appropriate error
            local_dt = super().strptime(date_string, '%Y-%m-%dT%H:%M:%S')
            raise NaiveDateTimeNotAllowed(local_dt)
        # This is probably a datetime value with timezone information. This comes in the form '+/-HH:MM'.
        aware_dt = datetime.datetime.fromisoformat(date_string).astimezone(UTC).replace(tzinfo=UTC)
        if isinstance(aware_dt, cls):
            return aware_dt
        return cls.from_datetime(aware_dt)

    @classmethod
    def fromtimestamp(cls, t, tz=None):
        dt = super().fromtimestamp(t, tz=tz)
        if isinstance(dt, cls):
            return dt
        return cls.from_datetime(dt)  # We want to return EWSDateTime objects

    @classmethod
    def utcfromtimestamp(cls, t):
        dt = super().utcfromtimestamp(t)
        if isinstance(dt, cls):
            return dt
        return cls.from_datetime(dt)  # We want to return EWSDateTime objects

    @classmethod
    def now(cls, tz=None):
        t = super().now(tz=tz)
        if isinstance(t, cls):
            return t
        return cls.from_datetime(t)  # We want to return EWSDateTime objects

    @classmethod
    def utcnow(cls):
        t = super().utcnow()
        if isinstance(t, cls):
            return t
        return cls.from_datetime(t)  # We want to return EWSDateTime objects

    def date(self):
        d = super().date()
        if isinstance(d, EWSDate):
            return d
        return EWSDate.from_date(d)  # We want to return EWSDate objects

Ancestors

  • datetime.datetime
  • datetime.date

Static methods

def from_datetime(d)
Expand source code
@classmethod
def from_datetime(cls, d):
    if type(d) is not datetime.datetime:
        raise ValueError("%r must be a datetime instance" % d)
    if d.tzinfo is None:
        tz = None
    elif isinstance(d.tzinfo, EWSTimeZone):
        tz = d.tzinfo
    else:
        tz = EWSTimeZone.from_timezone(d.tzinfo)
    return cls(d.year, d.month, d.day, d.hour, d.minute, d.second, d.microsecond, tzinfo=tz)
def from_string(date_string)
Expand source code
@classmethod
def from_string(cls, date_string):
    # Parses several common datetime formats and returns timezone-aware EWSDateTime objects
    if date_string.endswith('Z'):
        # UTC datetime
        return super().strptime(date_string, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC)
    if len(date_string) == 19:
        # This is probably a naive datetime. Don't allow this, but signal caller with an appropriate error
        local_dt = super().strptime(date_string, '%Y-%m-%dT%H:%M:%S')
        raise NaiveDateTimeNotAllowed(local_dt)
    # This is probably a datetime value with timezone information. This comes in the form '+/-HH:MM'.
    aware_dt = datetime.datetime.fromisoformat(date_string).astimezone(UTC).replace(tzinfo=UTC)
    if isinstance(aware_dt, cls):
        return aware_dt
    return cls.from_datetime(aware_dt)
def fromtimestamp(t, tz=None)

timestamp[, tz] -> tz's local time from POSIX timestamp.

Expand source code
@classmethod
def fromtimestamp(cls, t, tz=None):
    dt = super().fromtimestamp(t, tz=tz)
    if isinstance(dt, cls):
        return dt
    return cls.from_datetime(dt)  # We want to return EWSDateTime objects
def now(tz=None)

Returns new datetime object representing current time local to tz.

tz Timezone object.

If no tz is specified, uses local timezone.

Expand source code
@classmethod
def now(cls, tz=None):
    t = super().now(tz=tz)
    if isinstance(t, cls):
        return t
    return cls.from_datetime(t)  # We want to return EWSDateTime objects
def utcfromtimestamp(t)

Construct a naive UTC datetime from a POSIX timestamp.

Expand source code
@classmethod
def utcfromtimestamp(cls, t):
    dt = super().utcfromtimestamp(t)
    if isinstance(dt, cls):
        return dt
    return cls.from_datetime(dt)  # We want to return EWSDateTime objects
def utcnow()

Return a new datetime representing UTC day and time.

Expand source code
@classmethod
def utcnow(cls):
    t = super().utcnow()
    if isinstance(t, cls):
        return t
    return cls.from_datetime(t)  # We want to return EWSDateTime objects

Methods

def astimezone(self, tz=None)

tz -> convert to local time in new timezone tz

Expand source code
def astimezone(self, tz=None):
    if tz is None:
        tz = EWSTimeZone.localzone()
    t = super().astimezone(tz=tz).replace(tzinfo=tz)
    if isinstance(t, self.__class__):
        return t
    return self.from_datetime(t)  # We want to return EWSDateTime objects
def date(self)

Return date object with same year, month and day.

Expand source code
def date(self):
    d = super().date()
    if isinstance(d, EWSDate):
        return d
    return EWSDate.from_date(d)  # We want to return EWSDate objects
def ewsformat(self)

ISO 8601 format to satisfy xs:datetime as interpreted by EWS. Examples: * 2009-01-15T13:45:56Z * 2009-01-15T13:45:56+01:00

Expand source code
def ewsformat(self):
    """ISO 8601 format to satisfy xs:datetime as interpreted by EWS. Examples:
    * 2009-01-15T13:45:56Z
    * 2009-01-15T13:45:56+01:00
    """
    if not self.tzinfo:
        raise ValueError('%r must be timezone-aware' % self)
    if self.tzinfo.key == 'UTC':
        if self.microsecond:
            return self.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
        return self.strftime('%Y-%m-%dT%H:%M:%SZ')
    return self.isoformat()
class EWSTimeZone (*args, **kwargs)

Represents a timezone as expected by the EWS TimezoneContext / TimezoneDefinition XML element, and returned by services.GetServerTimeZones.

Expand source code
class EWSTimeZone(zoneinfo.ZoneInfo):
    """Represents a timezone as expected by the EWS TimezoneContext / TimezoneDefinition XML element, and returned by
    services.GetServerTimeZones.
    """

    IANA_TO_MS_MAP = IANA_TO_MS_TIMEZONE_MAP
    MS_TO_IANA_MAP = MS_TIMEZONE_TO_IANA_MAP

    def __new__(cls, *args, **kwargs):
        try:
            instance = super().__new__(cls, *args, **kwargs)
        except zoneinfo.ZoneInfoNotFoundError as e:
            raise UnknownTimeZone(e.args[0])
        try:
            instance.ms_id = cls.IANA_TO_MS_MAP[instance.key][0]
        except KeyError:
            raise UnknownTimeZone('No Windows timezone name found for timezone "%s"' % instance.key)

        # We don't need the Windows long-format timezone name in long format. It's used in timezone XML elements, but
        # EWS happily accepts empty strings. For a full list of timezones supported by the target server, including
        # long-format names, see output of services.GetServerTimeZones(account.protocol).call()
        instance.ms_name = ''
        return instance

    def __eq__(self, other):
        # Microsoft timezones are less granular than IANA, so an EWSTimeZone created from 'Europe/Copenhagen' may return
        # from the server as 'Europe/Copenhagen'. We're catering for Microsoft here, so base equality on the Microsoft
        # timezone ID.
        if not isinstance(other, self.__class__):
            return NotImplemented
        return self.ms_id == other.ms_id

    @classmethod
    def from_ms_id(cls, ms_id):
        # Create a timezone instance from a Microsoft timezone ID. This is lossy because there is not a 1:1 translation
        # from MS timezone ID to IANA timezone.
        try:
            return cls(cls.MS_TO_IANA_MAP[ms_id])
        except KeyError:
            if '/' in ms_id:
                # EWS sometimes returns an ID that has a region/location format, e.g. 'Europe/Copenhagen'. Try the
                # string unaltered.
                return cls(ms_id)
            raise UnknownTimeZone("Windows timezone ID '%s' is unknown by CLDR" % ms_id)

    @classmethod
    def from_pytz(cls, tz):
        return cls(tz.zone)

    @classmethod
    def from_datetime(cls, tz):
        """Convert from a standard library `datetime.timezone` instance."""
        return cls(tz.tzname(None))

    @classmethod
    def from_dateutil(cls, tz):
        # Objects returned by dateutil.tz.tzlocal() and dateutil.tz.gettz() are not supported. They
        # don't contain enough information to reliably match them with a CLDR timezone.
        if hasattr(tz, '_filename'):
            key = '/'.join(tz._filename.split('/')[-2:])
            return cls(key)
        return cls(tz.tzname(datetime.datetime.now()))

    @classmethod
    def from_zoneinfo(cls, tz):
        return cls(tz.key)

    @classmethod
    def from_timezone(cls, tz):
        # Support multiple tzinfo implementations. We could use isinstance(), but then we'd have to have pytz
        # and dateutil as dependencies for this package.
        tz_module = tz.__class__.__module__.split('.')[0]
        try:
            return {
                cls.__module__.split('.')[0]: lambda z: z,
                'backports': cls.from_zoneinfo,
                'datetime': cls.from_datetime,
                'dateutil': cls.from_dateutil,
                'pytz': cls.from_pytz,
                'zoneinfo': cls.from_zoneinfo,
                'pytz_deprecation_shim': lambda z: cls.from_timezone(z.unwrap_shim())
            }[tz_module](tz)
        except KeyError:
            raise TypeError('Unsupported tzinfo type: %r' % tz)

    @classmethod
    def localzone(cls):
        try:
            tz = tzlocal.get_localzone()
        except zoneinfo.ZoneInfoNotFoundError:
            # Older versions of tzlocal will raise a pytz exception. Let's not depend on pytz just for that.
            raise UnknownTimeZone("Failed to guess local timezone")
        # Handle both old and new versions of tzlocal that may return pytz or zoneinfo objects, respectively
        return cls.from_timezone(tz)

    @classmethod
    def timezone(cls, location):
        warnings.warn('replace EWSTimeZone.timezone() with just EWSTimeZone()', DeprecationWarning, stacklevel=2)
        return cls(location)

    def normalize(self, dt, is_dst=False):
        warnings.warn('normalization is now handled gracefully', DeprecationWarning, stacklevel=2)
        return dt

    def localize(self, dt, is_dst=False):
        warnings.warn('replace tz.localize() with dt.replace(tzinfo=tz)', DeprecationWarning, stacklevel=2)
        if dt.tzinfo is not None:
            raise ValueError('%r must be timezone-unaware' % dt)
        dt = dt.replace(tzinfo=self)
        if is_dst is not None:
            # DST dates are assumed to always be after non-DST dates
            dt_before = dt.replace(fold=0)
            dt_after = dt.replace(fold=1)
            dst_before = dt_before.dst()
            dst_after = dt_after.dst()
            if dst_before > dst_after:
                dt = dt_before if is_dst else dt_after
            elif dst_before < dst_after:
                dt = dt_after if is_dst else dt_before
        return dt

    def fromutc(self, dt):
        t = super().fromutc(dt)
        if isinstance(t, EWSDateTime):
            return t
        return EWSDateTime.from_datetime(t)  # We want to return EWSDateTime objects

Ancestors

  • backports.zoneinfo.ZoneInfo
  • datetime.tzinfo

Class variables

var IANA_TO_MS_MAP
var MS_TO_IANA_MAP

Static methods

def from_datetime(tz)

Convert from a standard library datetime.timezone instance.

Expand source code
@classmethod
def from_datetime(cls, tz):
    """Convert from a standard library `datetime.timezone` instance."""
    return cls(tz.tzname(None))
def from_dateutil(tz)
Expand source code
@classmethod
def from_dateutil(cls, tz):
    # Objects returned by dateutil.tz.tzlocal() and dateutil.tz.gettz() are not supported. They
    # don't contain enough information to reliably match them with a CLDR timezone.
    if hasattr(tz, '_filename'):
        key = '/'.join(tz._filename.split('/')[-2:])
        return cls(key)
    return cls(tz.tzname(datetime.datetime.now()))
def from_ms_id(ms_id)
Expand source code
@classmethod
def from_ms_id(cls, ms_id):
    # Create a timezone instance from a Microsoft timezone ID. This is lossy because there is not a 1:1 translation
    # from MS timezone ID to IANA timezone.
    try:
        return cls(cls.MS_TO_IANA_MAP[ms_id])
    except KeyError:
        if '/' in ms_id:
            # EWS sometimes returns an ID that has a region/location format, e.g. 'Europe/Copenhagen'. Try the
            # string unaltered.
            return cls(ms_id)
        raise UnknownTimeZone("Windows timezone ID '%s' is unknown by CLDR" % ms_id)
def from_pytz(tz)
Expand source code
@classmethod
def from_pytz(cls, tz):
    return cls(tz.zone)
def from_timezone(tz)
Expand source code
@classmethod
def from_timezone(cls, tz):
    # Support multiple tzinfo implementations. We could use isinstance(), but then we'd have to have pytz
    # and dateutil as dependencies for this package.
    tz_module = tz.__class__.__module__.split('.')[0]
    try:
        return {
            cls.__module__.split('.')[0]: lambda z: z,
            'backports': cls.from_zoneinfo,
            'datetime': cls.from_datetime,
            'dateutil': cls.from_dateutil,
            'pytz': cls.from_pytz,
            'zoneinfo': cls.from_zoneinfo,
            'pytz_deprecation_shim': lambda z: cls.from_timezone(z.unwrap_shim())
        }[tz_module](tz)
    except KeyError:
        raise TypeError('Unsupported tzinfo type: %r' % tz)
def from_zoneinfo(tz)
Expand source code
@classmethod
def from_zoneinfo(cls, tz):
    return cls(tz.key)
def localzone()
Expand source code
@classmethod
def localzone(cls):
    try:
        tz = tzlocal.get_localzone()
    except zoneinfo.ZoneInfoNotFoundError:
        # Older versions of tzlocal will raise a pytz exception. Let's not depend on pytz just for that.
        raise UnknownTimeZone("Failed to guess local timezone")
    # Handle both old and new versions of tzlocal that may return pytz or zoneinfo objects, respectively
    return cls.from_timezone(tz)
def timezone(location)
Expand source code
@classmethod
def timezone(cls, location):
    warnings.warn('replace EWSTimeZone.timezone() with just EWSTimeZone()', DeprecationWarning, stacklevel=2)
    return cls(location)

Methods

def fromutc(self, dt)

Given a datetime with local time in UTC, retrieve an adjusted datetime in local time.

Expand source code
def fromutc(self, dt):
    t = super().fromutc(dt)
    if isinstance(t, EWSDateTime):
        return t
    return EWSDateTime.from_datetime(t)  # We want to return EWSDateTime objects
def localize(self, dt, is_dst=False)
Expand source code
def localize(self, dt, is_dst=False):
    warnings.warn('replace tz.localize() with dt.replace(tzinfo=tz)', DeprecationWarning, stacklevel=2)
    if dt.tzinfo is not None:
        raise ValueError('%r must be timezone-unaware' % dt)
    dt = dt.replace(tzinfo=self)
    if is_dst is not None:
        # DST dates are assumed to always be after non-DST dates
        dt_before = dt.replace(fold=0)
        dt_after = dt.replace(fold=1)
        dst_before = dt_before.dst()
        dst_after = dt_after.dst()
        if dst_before > dst_after:
            dt = dt_before if is_dst else dt_after
        elif dst_before < dst_after:
            dt = dt_after if is_dst else dt_before
    return dt
def normalize(self, dt, is_dst=False)
Expand source code
def normalize(self, dt, is_dst=False):
    warnings.warn('normalization is now handled gracefully', DeprecationWarning, stacklevel=2)
    return dt
class ExtendedProperty (*args, **kwargs)
Expand source code
class ExtendedProperty(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedproperty"""

    ELEMENT_NAME = 'ExtendedProperty'

    # Enum values: https://docs.microsoft.com/en-us/dotnet/api/exchangewebservices.distinguishedpropertysettype
    DISTINGUISHED_SETS = {
        'Address',
        'Appointment',
        'CalendarAssistant',
        'Common',
        'InternetHeaders',
        'Meeting',
        'PublicStrings',
        'Sharing',
        'Task',
        'UnifiedMessaging',
    }
    # Enum values: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedfielduri
    PROPERTY_TYPES = {
        'ApplicationTime',
        'Binary',
        'BinaryArray',
        'Boolean',
        'CLSID',
        'CLSIDArray',
        'Currency',
        'CurrencyArray',
        'Double',
        'DoubleArray',
        # 'Error',
        'Float',
        'FloatArray',
        'Integer',
        'IntegerArray',
        'Long',
        'LongArray',
        # 'Null',
        # 'Object',
        # 'ObjectArray',
        'Short',
        'ShortArray',
        'SystemTime',
        'SystemTimeArray',
        'String',
        'StringArray',
    }  # The commented-out types cannot be used for setting or getting (see docs) and are thus not very useful here

    # Translation table between common distinguished_property_set_id and property_set_id values. See
    # https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/commonly-used-property-sets
    # ID values must be lowercase.
    DISTINGUISHED_SET_NAME_TO_ID_MAP = {
        'Address': '00062004-0000-0000-c000-000000000046',
        'AirSync': '71035549-0739-4dcb-9163-00f0580dbbdf',
        'Appointment': '00062002-0000-0000-c000-000000000046',
        'Common': '00062008-0000-0000-c000-000000000046',
        'InternetHeaders': '00020386-0000-0000-c000-000000000046',
        'Log': '0006200a-0000-0000-c000-000000000046',
        'Mapi': '00020328-0000-0000-c000-000000000046',
        'Meeting': '6ed8da90-450b-101b-98da-00aa003f1305',
        'Messaging': '41f28f13-83f4-4114-a584-eedb5a6b0bff',
        'Note': '0006200e-0000-0000-c000-000000000046',
        'PostRss': '00062041-0000-0000-c000-000000000046',
        'PublicStrings': '00020329-0000-0000-c000-000000000046',
        'Remote': '00062014-0000-0000-c000-000000000046',
        'Report': '00062013-0000-0000-c000-000000000046',
        'Sharing': '00062040-0000-0000-c000-000000000046',
        'Task': '00062003-0000-0000-c000-000000000046',
        'UnifiedMessaging': '4442858e-a9e3-4e80-b900-317a210cc15b',
    }
    DISTINGUISHED_SET_ID_TO_NAME_MAP = {v: k for k, v in DISTINGUISHED_SET_NAME_TO_ID_MAP.items()}

    distinguished_property_set_id = None
    property_set_id = None
    property_tag = None  # hex integer (e.g. 0x8000) or string ('0x8000')
    property_name = None
    property_id = None  # integer as hex-formatted int (e.g. 0x8000) or normal int (32768)
    property_type = ''

    __slots__ = 'value',

    def __init__(self, *args, **kwargs):
        if not kwargs:
            # Allow to set attributes without keyword
            kwargs = dict(zip(self._slots_keys, args))
        self.value = kwargs.pop('value')
        super().__init__(**kwargs)

    @classmethod
    def validate_cls(cls):
        # Validate values of class attributes and their inter-dependencies
        cls._validate_distinguished_property_set_id()
        cls._validate_property_set_id()
        cls._validate_property_tag()
        cls._validate_property_name()
        cls._validate_property_id()
        cls._validate_property_type()

    @classmethod
    def _validate_distinguished_property_set_id(cls):
        if cls.distinguished_property_set_id:
            if any([cls.property_set_id, cls.property_tag]):
                raise ValueError(
                    "When 'distinguished_property_set_id' is set, 'property_set_id' and 'property_tag' must be None"
                )
            if not any([cls.property_id, cls.property_name]):
                raise ValueError(
                    "When 'distinguished_property_set_id' is set, 'property_id' or 'property_name' must also be set"
                )
            if cls.distinguished_property_set_id not in cls.DISTINGUISHED_SETS:
                raise ValueError(
                    "'distinguished_property_set_id' %r must be one of %s"
                    % (cls.distinguished_property_set_id, sorted(cls.DISTINGUISHED_SETS))
                )

    @classmethod
    def _validate_property_set_id(cls):
        if cls.property_set_id:
            if any([cls.distinguished_property_set_id, cls.property_tag]):
                raise ValueError(
                    "When 'property_set_id' is set, 'distinguished_property_set_id' and 'property_tag' must be None"
                )
            if not any([cls.property_id, cls.property_name]):
                raise ValueError(
                    "When 'property_set_id' is set, 'property_id' or 'property_name' must also be set"
                )

    @classmethod
    def _validate_property_tag(cls):
        if cls.property_tag:
            if any([
                cls.distinguished_property_set_id, cls.property_set_id, cls.property_name, cls.property_id
            ]):
                raise ValueError("When 'property_tag' is set, only 'property_type' must be set")
            if 0x8000 <= cls.property_tag_as_int() <= 0xFFFE:
                raise ValueError(
                    "'property_tag' value '%s' is reserved for custom properties" % cls.property_tag_as_hex()
                )

    @classmethod
    def _validate_property_name(cls):
        if cls.property_name:
            if any([cls.property_id, cls.property_tag]):
                raise ValueError("When 'property_name' is set, 'property_id' and 'property_tag' must be None")
            if not any([cls.distinguished_property_set_id, cls.property_set_id]):
                raise ValueError(
                    "When 'property_name' is set, 'distinguished_property_set_id' or 'property_set_id' must also be set"
                )

    @classmethod
    def _validate_property_id(cls):
        if cls.property_id:
            if any([cls.property_name, cls.property_tag]):
                raise ValueError("When 'property_id' is set, 'property_name' and 'property_tag' must be None")
            if not any([cls.distinguished_property_set_id, cls.property_set_id]):
                raise ValueError(
                    "When 'property_id' is set, 'distinguished_property_set_id' or 'property_set_id' must also be set"
                )

    @classmethod
    def _validate_property_type(cls):
        if cls.property_type not in cls.PROPERTY_TYPES:
            raise ValueError(
                "'property_type' %r must be one of %s" % (cls.property_type, sorted(cls.PROPERTY_TYPES))
            )

    def clean(self, version=None):
        self.validate_cls()
        python_type = self.python_type()
        if self.is_array_type():
            if not is_iterable(self.value):
                raise ValueError("'%s' value %r must be a list" % (self.__class__.__name__, self.value))
            for v in self.value:
                if not isinstance(v, python_type):
                    raise TypeError(
                        "'%s' value element %r must be an instance of %s" % (self.__class__.__name__, v, python_type))
        else:
            if not isinstance(self.value, python_type):
                raise TypeError(
                    "'%s' value %r must be an instance of %s" % (self.__class__.__name__, self.value, python_type))

    @classmethod
    def _normalize_obj(cls, obj):
        # Sometimes, EWS will helpfully translate a 'distinguished_property_set_id' value to a 'property_set_id' value
        # and vice versa. Align these values on an ExtendedFieldURI instance.
        try:
            obj.property_set_id = cls.DISTINGUISHED_SET_NAME_TO_ID_MAP[obj.distinguished_property_set_id]
        except KeyError:
            try:
                obj.distinguished_property_set_id = cls.DISTINGUISHED_SET_ID_TO_NAME_MAP[obj.property_set_id]
            except KeyError:
                pass
        return obj

    @classmethod
    def is_property_instance(cls, elem):
        """Return whether an 'ExtendedProperty' element matches the definition for this class. Extended property fields
        do not have a name, so we must match on the cls.property_* attributes to match a field in the request with a
        field in the response.
        """
        # We can't use ExtendedFieldURI.from_xml(). It clears the XML element but we may not want to consume it here.
        kwargs = {
            f.name: f.from_xml(elem=elem.find(ExtendedFieldURI.response_tag()), account=None)
            for f in ExtendedFieldURI.FIELDS
        }
        xml_obj = ExtendedFieldURI(**kwargs)
        cls_obj = cls.as_object()
        return cls._normalize_obj(cls_obj) == cls._normalize_obj(xml_obj)

    @classmethod
    def from_xml(cls, elem, account):
        # Gets value of this specific ExtendedProperty from a list of 'ExtendedProperty' XML elements
        python_type = cls.python_type()
        if cls.is_array_type():
            values = elem.find('{%s}Values' % TNS)
            return [
                xml_text_to_value(value=val, value_type=python_type)
                for val in get_xml_attrs(values, '{%s}Value' % TNS)
            ]
        extended_field_value = xml_text_to_value(value=get_xml_attr(elem, '{%s}Value' % TNS), value_type=python_type)
        if python_type == str and not extended_field_value:
            # For string types, we want to return the empty string instead of None if the element was
            # actually found, but there was no XML value. For other types, it would be more problematic
            # to make that distinction, e.g. return False for bool, 0 for int, etc.
            return ''
        return extended_field_value

    def to_xml(self, version):
        if self.is_array_type():
            values = create_element('t:Values')
            for v in self.value:
                add_xml_child(values, 't:Value', v)
            return values
        return set_xml_value(create_element('t:Value'), self.value, version=version)

    @classmethod
    def is_array_type(cls):
        return cls.property_type.endswith('Array')

    @classmethod
    def property_tag_as_int(cls):
        if isinstance(cls.property_tag, str):
            return int(cls.property_tag, base=16)
        return cls.property_tag

    @classmethod
    def property_tag_as_hex(cls):
        return hex(cls.property_tag) if isinstance(cls.property_tag, int) else cls.property_tag

    @classmethod
    def python_type(cls):
        # Return the best equivalent for a Python type for the property type of this class
        base_type = cls.property_type[:-5] if cls.is_array_type() else cls.property_type
        return {
            'ApplicationTime': Decimal,
            'Binary': bytes,
            'Boolean': bool,
            'CLSID': str,
            'Currency': int,
            'Double': Decimal,
            'Float': Decimal,
            'Integer': int,
            'Long': int,
            'Short': int,
            'SystemTime': EWSDateTime,
            'String': str,
        }[base_type]

    @classmethod
    def as_object(cls):
        # Return an object we can use to match with the incoming object from XML
        return ExtendedFieldURI(
            distinguished_property_set_id=cls.distinguished_property_set_id,
            property_set_id=cls.property_set_id.lower() if cls.property_set_id else None,
            property_tag=cls.property_tag_as_hex(),
            property_name=cls.property_name,
            property_id=value_to_xml_text(cls.property_id) if cls.property_id else None,
            property_type=cls.property_type,
        )

Ancestors

Subclasses

Class variables

var DISTINGUISHED_SETS
var DISTINGUISHED_SET_ID_TO_NAME_MAP
var DISTINGUISHED_SET_NAME_TO_ID_MAP
var ELEMENT_NAME
var PROPERTY_TYPES
var distinguished_property_set_id
var property_id
var property_name
var property_set_id
var property_tag
var property_type

Static methods

def as_object()
Expand source code
@classmethod
def as_object(cls):
    # Return an object we can use to match with the incoming object from XML
    return ExtendedFieldURI(
        distinguished_property_set_id=cls.distinguished_property_set_id,
        property_set_id=cls.property_set_id.lower() if cls.property_set_id else None,
        property_tag=cls.property_tag_as_hex(),
        property_name=cls.property_name,
        property_id=value_to_xml_text(cls.property_id) if cls.property_id else None,
        property_type=cls.property_type,
    )
def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    # Gets value of this specific ExtendedProperty from a list of 'ExtendedProperty' XML elements
    python_type = cls.python_type()
    if cls.is_array_type():
        values = elem.find('{%s}Values' % TNS)
        return [
            xml_text_to_value(value=val, value_type=python_type)
            for val in get_xml_attrs(values, '{%s}Value' % TNS)
        ]
    extended_field_value = xml_text_to_value(value=get_xml_attr(elem, '{%s}Value' % TNS), value_type=python_type)
    if python_type == str and not extended_field_value:
        # For string types, we want to return the empty string instead of None if the element was
        # actually found, but there was no XML value. For other types, it would be more problematic
        # to make that distinction, e.g. return False for bool, 0 for int, etc.
        return ''
    return extended_field_value
def is_array_type()
Expand source code
@classmethod
def is_array_type(cls):
    return cls.property_type.endswith('Array')
def is_property_instance(elem)

Return whether an 'ExtendedProperty' element matches the definition for this class. Extended property fields do not have a name, so we must match on the cls.property_* attributes to match a field in the request with a field in the response.

Expand source code
@classmethod
def is_property_instance(cls, elem):
    """Return whether an 'ExtendedProperty' element matches the definition for this class. Extended property fields
    do not have a name, so we must match on the cls.property_* attributes to match a field in the request with a
    field in the response.
    """
    # We can't use ExtendedFieldURI.from_xml(). It clears the XML element but we may not want to consume it here.
    kwargs = {
        f.name: f.from_xml(elem=elem.find(ExtendedFieldURI.response_tag()), account=None)
        for f in ExtendedFieldURI.FIELDS
    }
    xml_obj = ExtendedFieldURI(**kwargs)
    cls_obj = cls.as_object()
    return cls._normalize_obj(cls_obj) == cls._normalize_obj(xml_obj)
def property_tag_as_hex()
Expand source code
@classmethod
def property_tag_as_hex(cls):
    return hex(cls.property_tag) if isinstance(cls.property_tag, int) else cls.property_tag
def property_tag_as_int()
Expand source code
@classmethod
def property_tag_as_int(cls):
    if isinstance(cls.property_tag, str):
        return int(cls.property_tag, base=16)
    return cls.property_tag
def python_type()
Expand source code
@classmethod
def python_type(cls):
    # Return the best equivalent for a Python type for the property type of this class
    base_type = cls.property_type[:-5] if cls.is_array_type() else cls.property_type
    return {
        'ApplicationTime': Decimal,
        'Binary': bytes,
        'Boolean': bool,
        'CLSID': str,
        'Currency': int,
        'Double': Decimal,
        'Float': Decimal,
        'Integer': int,
        'Long': int,
        'Short': int,
        'SystemTime': EWSDateTime,
        'String': str,
    }[base_type]
def validate_cls()
Expand source code
@classmethod
def validate_cls(cls):
    # Validate values of class attributes and their inter-dependencies
    cls._validate_distinguished_property_set_id()
    cls._validate_property_set_id()
    cls._validate_property_tag()
    cls._validate_property_name()
    cls._validate_property_id()
    cls._validate_property_type()

Instance variables

var value

Return an attribute of instance, which is of type owner.

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    self.validate_cls()
    python_type = self.python_type()
    if self.is_array_type():
        if not is_iterable(self.value):
            raise ValueError("'%s' value %r must be a list" % (self.__class__.__name__, self.value))
        for v in self.value:
            if not isinstance(v, python_type):
                raise TypeError(
                    "'%s' value element %r must be an instance of %s" % (self.__class__.__name__, v, python_type))
    else:
        if not isinstance(self.value, python_type):
            raise TypeError(
                "'%s' value %r must be an instance of %s" % (self.__class__.__name__, self.value, python_type))
def to_xml(self, version)
Expand source code
def to_xml(self, version):
    if self.is_array_type():
        values = create_element('t:Values')
        for v in self.value:
            add_xml_child(values, 't:Value', v)
        return values
    return set_xml_value(create_element('t:Value'), self.value, version=version)

Inherited members

class FailFast

Fail immediately on server errors.

Expand source code
class FailFast(RetryPolicy):
    """Fail immediately on server errors."""

    @property
    def fail_fast(self):
        return True

    @property
    def back_off_until(self):
        return None

    def back_off(self, seconds):
        raise ValueError('Cannot back off with fail-fast policy')

    def may_retry_on_error(self, response, wait):
        log.debug('No retry: no fail-fast policy')
        return False

Ancestors

Instance variables

var back_off_until
Expand source code
@property
def back_off_until(self):
    return None
var fail_fast
Expand source code
@property
def fail_fast(self):
    return True

Methods

def back_off(self, seconds)
Expand source code
def back_off(self, seconds):
    raise ValueError('Cannot back off with fail-fast policy')
def may_retry_on_error(self, response, wait)
Expand source code
def may_retry_on_error(self, response, wait):
    log.debug('No retry: no fail-fast policy')
    return False
class FaultTolerance (max_wait=3600)

Enables fault-tolerant error handling. Tells internal methods to do an exponential back off when requests start failing, and wait up to max_wait seconds before failing.

Expand source code
class FaultTolerance(RetryPolicy):
    """Enables fault-tolerant error handling. Tells internal methods to do an exponential back off when requests start
    failing, and wait up to max_wait seconds before failing.
    """

    # Back off 60 seconds if we didn't get an explicit suggested value
    DEFAULT_BACKOFF = 60

    def __init__(self, max_wait=3600):
        self.max_wait = max_wait
        self._back_off_until = None
        self._back_off_lock = Lock()

    def __getstate__(self):
        # Locks cannot be pickled
        state = self.__dict__.copy()
        del state['_back_off_lock']
        return state

    def __setstate__(self, state):
        # Restore the lock
        self.__dict__.update(state)
        self._back_off_lock = Lock()

    @property
    def fail_fast(self):
        return False

    @property
    def back_off_until(self):
        """Return the back off value as a datetime. Reset the current back off value if it has expired."""
        if self._back_off_until is None:
            return None
        with self._back_off_lock:
            if self._back_off_until is None:
                return None
            if self._back_off_until < datetime.datetime.now():
                self._back_off_until = None  # The back off value has expired. Reset
                return None
            return self._back_off_until

    @back_off_until.setter
    def back_off_until(self, value):
        with self._back_off_lock:
            self._back_off_until = value

    def back_off(self, seconds):
        if seconds is None:
            seconds = self.DEFAULT_BACKOFF
        value = datetime.datetime.now() + datetime.timedelta(seconds=seconds)
        with self._back_off_lock:
            self._back_off_until = value

    def may_retry_on_error(self, response, wait):
        if response.status_code not in (301, 302, 401, 500, 503):
            # Don't retry if we didn't get a status code that we can hope to recover from
            log.debug('No retry: wrong status code %s', response.status_code)
            return False
        if wait > self.max_wait:
            # We lost patience. Session is cleaned up in outer loop
            raise RateLimitError(
                'Max timeout reached', url=response.url, status_code=response.status_code, total_wait=wait)
        if response.status_code == 401:
            # EWS sometimes throws 401's when it wants us to throttle connections. OK to retry.
            return True
        if response.headers.get('connection') == 'close':
            # Connection closed. OK to retry.
            return True
        if response.status_code == 302 and response.headers.get('location', '').lower() \
                == '/ews/genericerrorpage.htm?aspxerrorpath=/ews/exchange.asmx':
            # The genericerrorpage.htm/internalerror.asp is ridiculous behaviour for random outages. OK to retry.
            #
            # Redirect to '/internalsite/internalerror.asp' or '/internalsite/initparams.aspx' is caused by e.g. TLS
            # certificate f*ckups on the Exchange server. We should not retry those.
            return True
        if response.status_code == 503:
            # Internal server error. OK to retry.
            return True
        if response.status_code == 500 and b"Server Error in '/EWS' Application" in response.content:
            # "Server Error in '/EWS' Application" has been seen in highly concurrent settings. OK to retry.
            log.debug('Retry allowed: conditions met')
            return True
        return False

Ancestors

Class variables

var DEFAULT_BACKOFF

Instance variables

var back_off_until

Return the back off value as a datetime. Reset the current back off value if it has expired.

Expand source code
@property
def back_off_until(self):
    """Return the back off value as a datetime. Reset the current back off value if it has expired."""
    if self._back_off_until is None:
        return None
    with self._back_off_lock:
        if self._back_off_until is None:
            return None
        if self._back_off_until < datetime.datetime.now():
            self._back_off_until = None  # The back off value has expired. Reset
            return None
        return self._back_off_until
var fail_fast
Expand source code
@property
def fail_fast(self):
    return False

Methods

def back_off(self, seconds)
Expand source code
def back_off(self, seconds):
    if seconds is None:
        seconds = self.DEFAULT_BACKOFF
    value = datetime.datetime.now() + datetime.timedelta(seconds=seconds)
    with self._back_off_lock:
        self._back_off_until = value
def may_retry_on_error(self, response, wait)
Expand source code
def may_retry_on_error(self, response, wait):
    if response.status_code not in (301, 302, 401, 500, 503):
        # Don't retry if we didn't get a status code that we can hope to recover from
        log.debug('No retry: wrong status code %s', response.status_code)
        return False
    if wait > self.max_wait:
        # We lost patience. Session is cleaned up in outer loop
        raise RateLimitError(
            'Max timeout reached', url=response.url, status_code=response.status_code, total_wait=wait)
    if response.status_code == 401:
        # EWS sometimes throws 401's when it wants us to throttle connections. OK to retry.
        return True
    if response.headers.get('connection') == 'close':
        # Connection closed. OK to retry.
        return True
    if response.status_code == 302 and response.headers.get('location', '').lower() \
            == '/ews/genericerrorpage.htm?aspxerrorpath=/ews/exchange.asmx':
        # The genericerrorpage.htm/internalerror.asp is ridiculous behaviour for random outages. OK to retry.
        #
        # Redirect to '/internalsite/internalerror.asp' or '/internalsite/initparams.aspx' is caused by e.g. TLS
        # certificate f*ckups on the Exchange server. We should not retry those.
        return True
    if response.status_code == 503:
        # Internal server error. OK to retry.
        return True
    if response.status_code == 500 and b"Server Error in '/EWS' Application" in response.content:
        # "Server Error in '/EWS' Application" has been seen in highly concurrent settings. OK to retry.
        log.debug('Retry allowed: conditions met')
        return True
    return False
class FileAttachment (**kwargs)
Expand source code
class FileAttachment(Attachment):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/fileattachment"""

    ELEMENT_NAME = 'FileAttachment'

    is_contact_photo = BooleanField(field_uri='IsContactPhoto')
    _content = Base64Field(field_uri='Content')

    __slots__ = '_fp',

    def __init__(self, **kwargs):
        kwargs['_content'] = kwargs.pop('content', None)
        super().__init__(**kwargs)
        self._fp = None

    @property
    def fp(self):
        # Return a file-like object for the content. This avoids creating multiple in-memory copies of the content.
        if self._fp is None:
            self._init_fp()
        return self._fp

    def _init_fp(self):
        # Create a file-like object for the attachment content. We try hard to reduce memory consumption so we never
        # store the full attachment content in-memory.
        if not self.parent_item or not self.parent_item.account:
            raise ValueError('%s must have an account' % self.__class__.__name__)
        self._fp = FileAttachmentIO(attachment=self)

    @property
    def content(self):
        """Return the attachment content. Stores a local copy of the content in case you want to upload the attachment
        again later.
        """
        if self.attachment_id is None:
            return self._content
        if self._content is not None:
            return self._content
        # We have an ID to the data but still haven't called GetAttachment to get the actual data. Do that now.
        with self.fp as fp:
            self._content = fp.read()
        return self._content

    @content.setter
    def content(self, value):
        """Replace the attachment content."""
        if not isinstance(value, bytes):
            raise ValueError("'value' %r must be a bytes object" % value)
        self._content = value

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
        kwargs['content'] = kwargs.pop('_content')
        cls._clear(elem)
        return cls(**kwargs)

    def to_xml(self, version):
        self._content = self.content  # Make sure content is available, to avoid ErrorRequiredPropertyMissing
        return super().to_xml(version=version)

    def __getstate__(self):
        # The fp does not need to be pickled
        state = {k: getattr(self, k) for k in self._slots_keys}
        del state['_fp']
        return state

    def __setstate__(self, state):
        # Restore the fp
        for k in self._slots_keys:
            setattr(self, k, state.get(k))
        self._fp = None

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
    kwargs['content'] = kwargs.pop('_content')
    cls._clear(elem)
    return cls(**kwargs)

Instance variables

var content

Return the attachment content. Stores a local copy of the content in case you want to upload the attachment again later.

Expand source code
@property
def content(self):
    """Return the attachment content. Stores a local copy of the content in case you want to upload the attachment
    again later.
    """
    if self.attachment_id is None:
        return self._content
    if self._content is not None:
        return self._content
    # We have an ID to the data but still haven't called GetAttachment to get the actual data. Do that now.
    with self.fp as fp:
        self._content = fp.read()
    return self._content
var fp
Expand source code
@property
def fp(self):
    # Return a file-like object for the content. This avoids creating multiple in-memory copies of the content.
    if self._fp is None:
        self._init_fp()
    return self._fp
var is_contact_photo

Methods

def to_xml(self, version)
Expand source code
def to_xml(self, version):
    self._content = self.content  # Make sure content is available, to avoid ErrorRequiredPropertyMissing
    return super().to_xml(version=version)

Inherited members

class Folder (**kwargs)
Expand source code
class Folder(BaseFolder):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/folder"""

    permission_set = PermissionSetField(field_uri='folder:PermissionSet', supported_from=EXCHANGE_2007_SP1)
    effective_rights = EffectiveRightsField(field_uri='folder:EffectiveRights', is_read_only=True,
                                            supported_from=EXCHANGE_2007_SP1)

    __slots__ = '_root',

    def __init__(self, **kwargs):
        self._root = kwargs.pop('root', None)  # This is a pointer to the root of the folder hierarchy
        parent = kwargs.pop('parent', None)
        if parent:
            if self.root:
                if parent.root != self.root:
                    raise ValueError("'parent.root' must match 'root'")
            else:
                self.root = parent.root
            if 'parent_folder_id' in kwargs and parent.id != kwargs['parent_folder_id']:
                raise ValueError("'parent_folder_id' must match 'parent' ID")
            kwargs['parent_folder_id'] = ParentFolderId(id=parent.id, changekey=parent.changekey)
        super().__init__(**kwargs)

    @property
    def account(self):
        if self.root is None:
            return None
        return self.root.account

    @property
    def root(self):
        return self._root

    @root.setter
    def root(self, value):
        self._root = value

    @classmethod
    def register(cls, *args, **kwargs):
        if cls is not Folder:
            raise TypeError('For folders, custom fields must be registered on the Folder class')
        return super().register(*args, **kwargs)

    @classmethod
    def deregister(cls, *args, **kwargs):
        if cls is not Folder:
            raise TypeError('For folders, custom fields must be registered on the Folder class')
        return super().deregister(*args, **kwargs)

    @classmethod
    def get_distinguished(cls, root):
        """Get the distinguished folder for this folder class.

        :param root:
        :return:
        """
        try:
            return cls.resolve(
                account=root.account,
                folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
            )
        except MISSING_FOLDER_ERRORS:
            raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID)

    @property
    def parent(self):
        if not self.parent_folder_id:
            return None
        if self.parent_folder_id.id == self.id:
            # Some folders have a parent that references itself. Avoid circular references here
            return None
        return self.root.get_folder(self.parent_folder_id)

    @parent.setter
    def parent(self, value):
        if value is None:
            self.parent_folder_id = None
        else:
            if not isinstance(value, BaseFolder):
                raise ValueError("'value' %r must be a Folder instance" % value)
            self.root = value.root
            self.parent_folder_id = ParentFolderId(id=value.id, changekey=value.changekey)

    def clean(self, version=None):
        from .roots import RootOfHierarchy
        super().clean(version=version)
        if self.root and not isinstance(self.root, RootOfHierarchy):
            raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root)

    @classmethod
    def from_xml_with_root(cls, elem, root):
        folder = cls.from_xml(elem=elem, account=root.account)
        folder_cls = cls
        if cls == Folder:
            # We were called on the generic Folder class. Try to find a more specific class to return objects as.
            #
            # The "FolderClass" element value is the only indication we have in the FindFolder response of which
            # folder class we should create the folder with. And many folders share the same 'FolderClass' value, e.g.
            # Inbox and DeletedItems. We want to distinguish between these because otherwise we can't locate the right
            # folders types for e.g. Account.inbox and Account.trash.
            #
            # We should be able to just use the name, but apparently default folder names can be renamed to a set of
            # localized names using a PowerShell command:
            # https://docs.microsoft.com/en-us/powershell/module/exchange/client-access/Set-MailboxRegionalConfiguration
            #
            # Instead, search for a folder class using the localized name. If none are found, fall back to getting the
            # folder class by the "FolderClass" value.
            #
            # The returned XML may contain neither folder class nor name. In that case, we default to the generic
            # Folder class.
            if folder.name:
                try:
                    # TODO: fld_class.LOCALIZED_NAMES is most definitely neither complete nor authoritative
                    folder_cls = root.folder_cls_from_folder_name(folder_name=folder.name,
                                                                  locale=root.account.locale)
                    log.debug('Folder class %s matches localized folder name %s', folder_cls, folder.name)
                except KeyError:
                    pass
            if folder.folder_class and folder_cls == Folder:
                try:
                    folder_cls = cls.folder_cls_from_container_class(container_class=folder.folder_class)
                    log.debug('Folder class %s matches container class %s (%s)', folder_cls, folder.folder_class,
                              folder.name)
                except KeyError:
                    pass
            if folder_cls == Folder:
                log.debug('Fallback to class Folder (folder_class %s, name %s)', folder.folder_class, folder.name)
        return folder_cls(root=root, **{f.name: getattr(folder, f.name) for f in folder.FIELDS})

Ancestors

Subclasses

Class variables

var FIELDS

Static methods

def from_xml_with_root(elem, root)
Expand source code
@classmethod
def from_xml_with_root(cls, elem, root):
    folder = cls.from_xml(elem=elem, account=root.account)
    folder_cls = cls
    if cls == Folder:
        # We were called on the generic Folder class. Try to find a more specific class to return objects as.
        #
        # The "FolderClass" element value is the only indication we have in the FindFolder response of which
        # folder class we should create the folder with. And many folders share the same 'FolderClass' value, e.g.
        # Inbox and DeletedItems. We want to distinguish between these because otherwise we can't locate the right
        # folders types for e.g. Account.inbox and Account.trash.
        #
        # We should be able to just use the name, but apparently default folder names can be renamed to a set of
        # localized names using a PowerShell command:
        # https://docs.microsoft.com/en-us/powershell/module/exchange/client-access/Set-MailboxRegionalConfiguration
        #
        # Instead, search for a folder class using the localized name. If none are found, fall back to getting the
        # folder class by the "FolderClass" value.
        #
        # The returned XML may contain neither folder class nor name. In that case, we default to the generic
        # Folder class.
        if folder.name:
            try:
                # TODO: fld_class.LOCALIZED_NAMES is most definitely neither complete nor authoritative
                folder_cls = root.folder_cls_from_folder_name(folder_name=folder.name,
                                                              locale=root.account.locale)
                log.debug('Folder class %s matches localized folder name %s', folder_cls, folder.name)
            except KeyError:
                pass
        if folder.folder_class and folder_cls == Folder:
            try:
                folder_cls = cls.folder_cls_from_container_class(container_class=folder.folder_class)
                log.debug('Folder class %s matches container class %s (%s)', folder_cls, folder.folder_class,
                          folder.name)
            except KeyError:
                pass
        if folder_cls == Folder:
            log.debug('Fallback to class Folder (folder_class %s, name %s)', folder.folder_class, folder.name)
    return folder_cls(root=root, **{f.name: getattr(folder, f.name) for f in folder.FIELDS})
def get_distinguished(root)

Get the distinguished folder for this folder class.

:param root: :return:

Expand source code
@classmethod
def get_distinguished(cls, root):
    """Get the distinguished folder for this folder class.

    :param root:
    :return:
    """
    try:
        return cls.resolve(
            account=root.account,
            folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
        )
    except MISSING_FOLDER_ERRORS:
        raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID)

Instance variables

var account
Expand source code
@property
def account(self):
    if self.root is None:
        return None
    return self.root.account
var effective_rights
var parent
Expand source code
@property
def parent(self):
    if not self.parent_folder_id:
        return None
    if self.parent_folder_id.id == self.id:
        # Some folders have a parent that references itself. Avoid circular references here
        return None
    return self.root.get_folder(self.parent_folder_id)
var permission_set
var root
Expand source code
@property
def root(self):
    return self._root

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    from .roots import RootOfHierarchy
    super().clean(version=version)
    if self.root and not isinstance(self.root, RootOfHierarchy):
        raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root)

Inherited members

class FolderCollection (account, folders)

A class that implements an API for searching folders.

Implement a search API on a collection of folders.

:param account: An Account object :param folders: An iterable of folders, e.g. Folder.walk(), Folder.glob(), or [a.calendar, a.inbox]

Expand source code
class FolderCollection(SearchableMixIn):
    """A class that implements an API for searching folders."""

    # These fields are required in a FindFolder or GetFolder call to properly identify folder types
    REQUIRED_FOLDER_FIELDS = ('name', 'folder_class')

    def __init__(self, account, folders):
        """Implement a search API on a collection of folders.

        :param account: An Account object
        :param folders: An iterable of folders, e.g. Folder.walk(), Folder.glob(), or [a.calendar, a.inbox]
        """
        self.account = account
        self._folders = folders

    @threaded_cached_property
    def folders(self):
        # Resolve the list of folders, in case it's a generator
        return list(self._folders)

    def __len__(self):
        return len(self.folders)

    def __iter__(self):
        yield from self.folders

    def get(self, *args, **kwargs):
        return QuerySet(self).get(*args, **kwargs)

    def all(self):
        return QuerySet(self).all()

    def none(self):
        return QuerySet(self).none()

    def filter(self, *args, **kwargs):
        """Find items in the folder(s).

        Non-keyword args may be a list of Q instances.

        Optional extra keyword arguments follow a Django-like QuerySet filter syntax (see
           https://docs.djangoproject.com/en/1.10/ref/models/querysets/#field-lookups).

        We don't support '__year' and other date-related lookups. We also don't support '__endswith' or '__iendswith'.

        We support the additional '__not' lookup in place of Django's exclude() for simple cases. For more complicated
        cases you need to create a Q object and use ~Q().

        Examples:

            my_account.inbox.filter(datetime_received__gt=EWSDateTime(2016, 1, 1))
            my_account.calendar.filter(start__range=(EWSDateTime(2016, 1, 1), EWSDateTime(2017, 1, 1)))
            my_account.tasks.filter(subject='Hi mom')
            my_account.tasks.filter(subject__not='Hi mom')
            my_account.tasks.filter(subject__contains='Foo')
            my_account.tasks.filter(subject__icontains='foo')

        'endswith' and 'iendswith' could be emulated by searching with 'contains' or 'icontains' and then
        post-processing items. Fetch the field in question with additional_fields and remove items where the search
        string is not a postfix.
        """
        return QuerySet(self).filter(*args, **kwargs)

    def exclude(self, *args, **kwargs):
        return QuerySet(self).exclude(*args, **kwargs)

    def people(self):
        return QuerySet(self).people()

    def view(self, start, end, max_items=None, *args, **kwargs):
        """Implement the CalendarView option to FindItem. The difference between 'filter' and 'view' is that 'filter'
        only returns the master CalendarItem for recurring items, while 'view' unfolds recurring items and returns all
        CalendarItem occurrences as one would normally expect when presenting a calendar.

        Supports the same semantics as filter, except for 'start' and 'end' keyword attributes which are both required
        and behave differently than filter. Here, they denote the start and end of the timespan of the view. All items
        the overlap the timespan are returned (items that end exactly on 'start' are also returned, for some reason).

        EWS does not allow combining CalendarView with search restrictions (filter and exclude).

        'max_items' defines the maximum number of items returned in this view. Optional.

        :param start:
        :param end:
        :param max_items:  (Default value = None)
        :return:
        """
        qs = QuerySet(self).filter(*args, **kwargs)
        qs.calendar_view = CalendarView(start=start, end=end, max_items=max_items)
        return qs

    def allowed_item_fields(self):
        # Return non-ID fields of all item classes allowed in this folder type
        fields = set()
        for item_model in self.supported_item_models:
            fields.update(set(item_model.supported_fields(version=self.account.version)))
        return fields

    @property
    def supported_item_models(self):
        return tuple(item_model for folder in self.folders for item_model in folder.supported_item_models)

    def validate_item_field(self, field, version):
        # For each field, check if the field is valid for any of the item models supported by this folder
        for item_model in self.supported_item_models:
            try:
                item_model.validate_field(field=field, version=version)
                break
            except InvalidField:
                continue
        else:
            raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))

    def find_items(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
                   calendar_view=None, page_size=None, max_items=None, offset=0):
        """Private method to call the FindItem service.

        :param q: a Q instance containing any restrictions
        :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is
          non-null, we always return Item objects. (Default value = ID_ONLY)
        :param depth: controls the whether to return soft-deleted items or not. (Default value = None)
        :param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware
          that complex fields can only be fetched with fetch() (i.e. the GetItem service).
        :param order_fields: the SortOrder fields, if any (Default value = None)
        :param calendar_view: a CalendarView instance, if any (Default value = None)
        :param page_size: the requested number of items per page (Default value = None)
        :param max_items: the max number of items to return (Default value = None)
        :param offset: the offset relative to the first item in the item collection (Default value = 0)

        :return: a generator for the returned item IDs or items
        """
        if not self.folders:
            log.debug('Folder list is empty')
            return
        if q.is_never():
            log.debug('Query will never return results')
            return
        if shape not in SHAPE_CHOICES:
            raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
        if depth is None:
            depth = self._get_default_item_traversal_depth()
        if depth not in ITEM_TRAVERSAL_CHOICES:
            raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
        if additional_fields:
            for f in additional_fields:
                self.validate_item_field(field=f, version=self.account.version)
                if f.field.is_complex:
                    raise ValueError("find_items() does not support field '%s'. Use fetch() instead" % f.field.name)
        if calendar_view is not None and not isinstance(calendar_view, CalendarView):
            raise ValueError("'calendar_view' %s must be a CalendarView instance" % calendar_view)

        # Build up any restrictions
        if q.is_empty():
            restriction = None
            query_string = None
        elif q.query_string:
            restriction = None
            query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
        else:
            restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
            query_string = None
        log.debug(
            'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
            self.account,
            self.folders,
            shape,
            depth,
            additional_fields,
            restriction.q if restriction else None,
        )
        yield from FindItem(account=self.account, chunk_size=page_size).call(
            folders=self.folders,
            additional_fields=additional_fields,
            restriction=restriction,
            order_fields=order_fields,
            shape=shape,
            query_string=query_string,
            depth=depth,
            calendar_view=calendar_view,
            max_items=calendar_view.max_items if calendar_view else max_items,
            offset=offset,
        )

    def _get_single_folder(self):
        if len(self.folders) > 1:
            raise ValueError('Syncing folder hierarchy can only be done on a single folder')
        if not self.folders:
            log.debug('Folder list is empty')
            return None
        return self.folders[0]

    def find_people(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
                    page_size=None, max_items=None, offset=0):
        """Private method to call the FindPeople service.

        :param q: a Q instance containing any restrictions
        :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is
          non-null, we always return Persona objects. (Default value = ID_ONLY)
        :param depth: controls the whether to return soft-deleted items or not. (Default value = None)
        :param additional_fields: the extra properties we want on the return objects. Default is no properties.
        :param order_fields: the SortOrder fields, if any (Default value = None)
        :param page_size: the requested number of items per page (Default value = None)
        :param max_items: the max number of items to return (Default value = None)
        :param offset: the offset relative to the first item in the item collection (Default value = 0)

        :return: a generator for the returned personas
        """
        folder = self._get_single_folder()
        if not folder:
            return
        if q.is_never():
            log.debug('Query will never return results')
            return
        if shape not in SHAPE_CHOICES:
            raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
        if depth is None:
            depth = self._get_default_item_traversal_depth()
        if depth not in ITEM_TRAVERSAL_CHOICES:
            raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
        if additional_fields:
            for f in additional_fields:
                Persona.validate_field(field=f, version=self.account.version)
                if f.field.is_complex:
                    raise ValueError("find_people() does not support field '%s'" % f.field.name)

        # Build up any restrictions
        if q.is_empty():
            restriction = None
            query_string = None
        elif q.query_string:
            restriction = None
            query_string = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
        else:
            restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
            query_string = None
        yield from FindPeople(account=self.account, chunk_size=page_size).call(
                folder=folder,
                additional_fields=additional_fields,
                restriction=restriction,
                order_fields=order_fields,
                shape=shape,
                query_string=query_string,
                depth=depth,
                max_items=max_items,
                offset=offset,
        )

    def get_folder_fields(self, target_cls, is_complex=None):
        return {
            FieldPath(field=f) for f in target_cls.supported_fields(version=self.account.version)
            if is_complex is None or f.is_complex is is_complex
        }

    def _get_target_cls(self):
        # We may have root folders that don't support the same set of fields as normal folders. If there is a mix of
        # both folder types in self.folders, raise an error so we don't risk losing some fields in the query.
        from .base import Folder
        from .roots import RootOfHierarchy
        has_roots = False
        has_non_roots = False
        for f in self.folders:
            if isinstance(f, RootOfHierarchy):
                if has_non_roots:
                    raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders))
                has_roots = True
            else:
                if has_roots:
                    raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders))
                has_non_roots = True
        return RootOfHierarchy if has_roots else Folder

    def _get_default_traversal_depth(self, traversal_attr):
        unique_depths = {getattr(f, traversal_attr) for f in self.folders}
        if len(unique_depths) == 1:
            return unique_depths.pop()
        raise ValueError(
            'Folders in this collection do not have a common %s value. You need to define an explicit traversal depth'
            'with QuerySet.depth() (values: %s)' % (traversal_attr, unique_depths)
        )

    def _get_default_item_traversal_depth(self):
        # When searching folders, some folders require 'Shallow' and others 'Associated' traversal depth.
        return self._get_default_traversal_depth('DEFAULT_ITEM_TRAVERSAL_DEPTH')

    def _get_default_folder_traversal_depth(self):
        # When searching folders, some folders require 'Shallow' and others 'Deep' traversal depth.
        return self._get_default_traversal_depth('DEFAULT_FOLDER_TRAVERSAL_DEPTH')

    def resolve(self):
        # Looks up the folders or folder IDs in the collection and returns full Folder instances with all fields set.
        from .base import BaseFolder
        resolveable_folders = []
        for f in self.folders:
            if isinstance(f, BaseFolder) and not f.get_folder_allowed:
                log.debug('GetFolder not allowed on folder %s. Non-complex fields must be fetched with FindFolder', f)
                yield f
            else:
                resolveable_folders.append(f)
        # Fetch all properties for the remaining folders of folder IDs
        additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=None)
        yield from self.__class__(account=self.account, folders=resolveable_folders).get_folders(
                additional_fields=additional_fields
        )

    @require_account
    def find_folders(self, q=None, shape=ID_ONLY, depth=None, additional_fields=None, page_size=None, max_items=None,
                     offset=0):
        # 'depth' controls whether to return direct children or recurse into sub-folders
        from .base import BaseFolder, Folder
        if q is None:
            q = Q()
        if not self.folders:
            log.debug('Folder list is empty')
            return
        if q.is_never():
            log.debug('Query will never return results')
            return
        if q.is_empty():
            restriction = None
        else:
            restriction = Restriction(q, folders=self.folders, applies_to=Restriction.FOLDERS)
        if shape not in SHAPE_CHOICES:
            raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
        if depth is None:
            depth = self._get_default_folder_traversal_depth()
        if depth not in FOLDER_TRAVERSAL_CHOICES:
            raise ValueError("'depth' %s must be one of %s" % (depth, FOLDER_TRAVERSAL_CHOICES))
        if additional_fields is None:
            # Default to all non-complex properties. Subfolders will always be of class Folder
            additional_fields = self.get_folder_fields(target_cls=Folder, is_complex=False)
        else:
            for f in additional_fields:
                if f.field.is_complex:
                    raise ValueError("find_folders() does not support field '%s'. Use get_folders()." % f.field.name)

        # Add required fields
        additional_fields.update(
            (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
        )

        yield from FindFolder(account=self.account, chunk_size=page_size).call(
                folders=self.folders,
                additional_fields=additional_fields,
                restriction=restriction,
                shape=shape,
                depth=depth,
                max_items=max_items,
                offset=offset,
        )

    def get_folders(self, additional_fields=None):
        # Expand folders with their full set of properties
        from .base import BaseFolder
        if not self.folders:
            log.debug('Folder list is empty')
            return
        if additional_fields is None:
            # Default to all complex properties
            additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=True)

        # Add required fields
        additional_fields.update(
            (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
        )

        yield from GetFolder(account=self.account).call(
                folders=self.folders,
                additional_fields=additional_fields,
                shape=ID_ONLY,
        )

    def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
        if not self.folders:
            log.debug('Folder list is empty')
            return
        yield from SubscribeToPull(account=self.account).call(
            folders=self.folders, event_types=event_types, watermark=watermark, timeout=timeout,
        )

    def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
                          status_frequency=1):
        if not self.folders:
            log.debug('Folder list is empty')
            return
        yield from SubscribeToPush(account=self.account).call(
            folders=self.folders, event_types=event_types, watermark=watermark, status_frequency=status_frequency,
            url=callback_url,
        )

    def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
        if not self.folders:
            log.debug('Folder list is empty')
            return
        yield from SubscribeToStreaming(account=self.account).call(folders=self.folders, event_types=event_types)

    def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
        folder = self._get_single_folder()
        if not folder:
            return
        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 folder.allowed_item_fields(version=self.account.version)}
        else:
            for field in only_fields:
                folder.validate_item_field(field=field, version=self.account.version)
            # Remove ItemId and ChangeKey. We get them unconditionally
            additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}

        svc = SyncFolderItems(account=self.account)
        while True:
            yield from svc.call(
                folder=folder,
                shape=ID_ONLY,
                additional_fields=additional_fields,
                sync_state=sync_state,
                ignore=ignore,
                max_changes_returned=max_changes_returned,
                sync_scope=sync_scope,
            )
            if svc.sync_state == sync_state:
                # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here
                break
            sync_state = svc.sync_state  # Set the new sync state in the next call
            if svc.includes_last_item_in_range:  # Try again if there are more items
                break
        raise SyncCompleted(sync_state=svc.sync_state)

    def sync_hierarchy(self, sync_state=None, only_fields=None):
        folder = self._get_single_folder()
        if not folder:
            return
        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 folder.supported_fields(version=self.account.version)}
        else:
            for f in only_fields:
                folder.validate_field(field=f, version=self.account.version)
            # Remove ItemId and ChangeKey. We get them unconditionally
            additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}

        # Add required fields
        additional_fields.update(
            (FieldPath(field=folder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
        )

        svc = SyncFolderHierarchy(account=self.account)
        while True:
            yield from svc.call(
                folder=folder,
                shape=ID_ONLY,
                additional_fields=additional_fields,
                sync_state=sync_state,
            )
            if svc.sync_state == sync_state:
                # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here
                break
            sync_state = svc.sync_state  # Set the new sync state in the next call
            if svc.includes_last_item_in_range:  # Try again if there are more items
                break
        raise SyncCompleted(sync_state=svc.sync_state)

Ancestors

Class variables

var REQUIRED_FOLDER_FIELDS

Instance variables

var folders
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 supported_item_models
Expand source code
@property
def supported_item_models(self):
    return tuple(item_model for folder in self.folders for item_model in folder.supported_item_models)

Methods

def all(self)
Expand source code
def all(self):
    return QuerySet(self).all()
def allowed_item_fields(self)
Expand source code
def allowed_item_fields(self):
    # Return non-ID fields of all item classes allowed in this folder type
    fields = set()
    for item_model in self.supported_item_models:
        fields.update(set(item_model.supported_fields(version=self.account.version)))
    return fields
def exclude(self, *args, **kwargs)
Expand source code
def exclude(self, *args, **kwargs):
    return QuerySet(self).exclude(*args, **kwargs)
def filter(self, *args, **kwargs)

Find items in the folder(s).

Non-keyword args may be a list of Q instances.

Optional extra keyword arguments follow a Django-like QuerySet filter syntax (see https://docs.djangoproject.com/en/1.10/ref/models/querysets/#field-lookups).

We don't support '__year' and other date-related lookups. We also don't support '__endswith' or '__iendswith'.

We support the additional '__not' lookup in place of Django's exclude() for simple cases. For more complicated cases you need to create a Q object and use ~Q().

Examples

my_account.inbox.filter(datetime_received__gt=EWSDateTime(2016, 1, 1)) my_account.calendar.filter(start__range=(EWSDateTime(2016, 1, 1), EWSDateTime(2017, 1, 1))) my_account.tasks.filter(subject='Hi mom') my_account.tasks.filter(subject__not='Hi mom') my_account.tasks.filter(subject__contains='Foo') my_account.tasks.filter(subject__icontains='foo')

'endswith' and 'iendswith' could be emulated by searching with 'contains' or 'icontains' and then post-processing items. Fetch the field in question with additional_fields and remove items where the search string is not a postfix.

Expand source code
def filter(self, *args, **kwargs):
    """Find items in the folder(s).

    Non-keyword args may be a list of Q instances.

    Optional extra keyword arguments follow a Django-like QuerySet filter syntax (see
       https://docs.djangoproject.com/en/1.10/ref/models/querysets/#field-lookups).

    We don't support '__year' and other date-related lookups. We also don't support '__endswith' or '__iendswith'.

    We support the additional '__not' lookup in place of Django's exclude() for simple cases. For more complicated
    cases you need to create a Q object and use ~Q().

    Examples:

        my_account.inbox.filter(datetime_received__gt=EWSDateTime(2016, 1, 1))
        my_account.calendar.filter(start__range=(EWSDateTime(2016, 1, 1), EWSDateTime(2017, 1, 1)))
        my_account.tasks.filter(subject='Hi mom')
        my_account.tasks.filter(subject__not='Hi mom')
        my_account.tasks.filter(subject__contains='Foo')
        my_account.tasks.filter(subject__icontains='foo')

    'endswith' and 'iendswith' could be emulated by searching with 'contains' or 'icontains' and then
    post-processing items. Fetch the field in question with additional_fields and remove items where the search
    string is not a postfix.
    """
    return QuerySet(self).filter(*args, **kwargs)
def find_folders(self, q=None, shape='IdOnly', depth=None, additional_fields=None, page_size=None, max_items=None, offset=0)
Expand source code
@require_account
def find_folders(self, q=None, shape=ID_ONLY, depth=None, additional_fields=None, page_size=None, max_items=None,
                 offset=0):
    # 'depth' controls whether to return direct children or recurse into sub-folders
    from .base import BaseFolder, Folder
    if q is None:
        q = Q()
    if not self.folders:
        log.debug('Folder list is empty')
        return
    if q.is_never():
        log.debug('Query will never return results')
        return
    if q.is_empty():
        restriction = None
    else:
        restriction = Restriction(q, folders=self.folders, applies_to=Restriction.FOLDERS)
    if shape not in SHAPE_CHOICES:
        raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
    if depth is None:
        depth = self._get_default_folder_traversal_depth()
    if depth not in FOLDER_TRAVERSAL_CHOICES:
        raise ValueError("'depth' %s must be one of %s" % (depth, FOLDER_TRAVERSAL_CHOICES))
    if additional_fields is None:
        # Default to all non-complex properties. Subfolders will always be of class Folder
        additional_fields = self.get_folder_fields(target_cls=Folder, is_complex=False)
    else:
        for f in additional_fields:
            if f.field.is_complex:
                raise ValueError("find_folders() does not support field '%s'. Use get_folders()." % f.field.name)

    # Add required fields
    additional_fields.update(
        (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
    )

    yield from FindFolder(account=self.account, chunk_size=page_size).call(
            folders=self.folders,
            additional_fields=additional_fields,
            restriction=restriction,
            shape=shape,
            depth=depth,
            max_items=max_items,
            offset=offset,
    )
def find_items(self, q, shape='IdOnly', depth=None, additional_fields=None, order_fields=None, calendar_view=None, page_size=None, max_items=None, offset=0)

Private method to call the FindItem service.

:param q: a Q instance containing any restrictions :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is non-null, we always return Item objects. (Default value = ID_ONLY) :param depth: controls the whether to return soft-deleted items or not. (Default value = None) :param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware that complex fields can only be fetched with fetch() (i.e. the GetItem service). :param order_fields: the SortOrder fields, if any (Default value = None) :param calendar_view: a CalendarView instance, if any (Default value = None) :param page_size: the requested number of items per page (Default value = None) :param max_items: the max number of items to return (Default value = None) :param offset: the offset relative to the first item in the item collection (Default value = 0)

:return: a generator for the returned item IDs or items

Expand source code
def find_items(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
               calendar_view=None, page_size=None, max_items=None, offset=0):
    """Private method to call the FindItem service.

    :param q: a Q instance containing any restrictions
    :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is
      non-null, we always return Item objects. (Default value = ID_ONLY)
    :param depth: controls the whether to return soft-deleted items or not. (Default value = None)
    :param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware
      that complex fields can only be fetched with fetch() (i.e. the GetItem service).
    :param order_fields: the SortOrder fields, if any (Default value = None)
    :param calendar_view: a CalendarView instance, if any (Default value = None)
    :param page_size: the requested number of items per page (Default value = None)
    :param max_items: the max number of items to return (Default value = None)
    :param offset: the offset relative to the first item in the item collection (Default value = 0)

    :return: a generator for the returned item IDs or items
    """
    if not self.folders:
        log.debug('Folder list is empty')
        return
    if q.is_never():
        log.debug('Query will never return results')
        return
    if shape not in SHAPE_CHOICES:
        raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
    if depth is None:
        depth = self._get_default_item_traversal_depth()
    if depth not in ITEM_TRAVERSAL_CHOICES:
        raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
    if additional_fields:
        for f in additional_fields:
            self.validate_item_field(field=f, version=self.account.version)
            if f.field.is_complex:
                raise ValueError("find_items() does not support field '%s'. Use fetch() instead" % f.field.name)
    if calendar_view is not None and not isinstance(calendar_view, CalendarView):
        raise ValueError("'calendar_view' %s must be a CalendarView instance" % calendar_view)

    # Build up any restrictions
    if q.is_empty():
        restriction = None
        query_string = None
    elif q.query_string:
        restriction = None
        query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
    else:
        restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS)
        query_string = None
    log.debug(
        'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)',
        self.account,
        self.folders,
        shape,
        depth,
        additional_fields,
        restriction.q if restriction else None,
    )
    yield from FindItem(account=self.account, chunk_size=page_size).call(
        folders=self.folders,
        additional_fields=additional_fields,
        restriction=restriction,
        order_fields=order_fields,
        shape=shape,
        query_string=query_string,
        depth=depth,
        calendar_view=calendar_view,
        max_items=calendar_view.max_items if calendar_view else max_items,
        offset=offset,
    )
def find_people(self, q, shape='IdOnly', depth=None, additional_fields=None, order_fields=None, page_size=None, max_items=None, offset=0)

Private method to call the FindPeople service.

:param q: a Q instance containing any restrictions :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is non-null, we always return Persona objects. (Default value = ID_ONLY) :param depth: controls the whether to return soft-deleted items or not. (Default value = None) :param additional_fields: the extra properties we want on the return objects. Default is no properties. :param order_fields: the SortOrder fields, if any (Default value = None) :param page_size: the requested number of items per page (Default value = None) :param max_items: the max number of items to return (Default value = None) :param offset: the offset relative to the first item in the item collection (Default value = 0)

:return: a generator for the returned personas

Expand source code
def find_people(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None,
                page_size=None, max_items=None, offset=0):
    """Private method to call the FindPeople service.

    :param q: a Q instance containing any restrictions
    :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is
      non-null, we always return Persona objects. (Default value = ID_ONLY)
    :param depth: controls the whether to return soft-deleted items or not. (Default value = None)
    :param additional_fields: the extra properties we want on the return objects. Default is no properties.
    :param order_fields: the SortOrder fields, if any (Default value = None)
    :param page_size: the requested number of items per page (Default value = None)
    :param max_items: the max number of items to return (Default value = None)
    :param offset: the offset relative to the first item in the item collection (Default value = 0)

    :return: a generator for the returned personas
    """
    folder = self._get_single_folder()
    if not folder:
        return
    if q.is_never():
        log.debug('Query will never return results')
        return
    if shape not in SHAPE_CHOICES:
        raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES))
    if depth is None:
        depth = self._get_default_item_traversal_depth()
    if depth not in ITEM_TRAVERSAL_CHOICES:
        raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES))
    if additional_fields:
        for f in additional_fields:
            Persona.validate_field(field=f, version=self.account.version)
            if f.field.is_complex:
                raise ValueError("find_people() does not support field '%s'" % f.field.name)

    # Build up any restrictions
    if q.is_empty():
        restriction = None
        query_string = None
    elif q.query_string:
        restriction = None
        query_string = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
    else:
        restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS)
        query_string = None
    yield from FindPeople(account=self.account, chunk_size=page_size).call(
            folder=folder,
            additional_fields=additional_fields,
            restriction=restriction,
            order_fields=order_fields,
            shape=shape,
            query_string=query_string,
            depth=depth,
            max_items=max_items,
            offset=offset,
    )
def get(self, *args, **kwargs)
Expand source code
def get(self, *args, **kwargs):
    return QuerySet(self).get(*args, **kwargs)
def get_folder_fields(self, target_cls, is_complex=None)
Expand source code
def get_folder_fields(self, target_cls, is_complex=None):
    return {
        FieldPath(field=f) for f in target_cls.supported_fields(version=self.account.version)
        if is_complex is None or f.is_complex is is_complex
    }
def get_folders(self, additional_fields=None)
Expand source code
def get_folders(self, additional_fields=None):
    # Expand folders with their full set of properties
    from .base import BaseFolder
    if not self.folders:
        log.debug('Folder list is empty')
        return
    if additional_fields is None:
        # Default to all complex properties
        additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=True)

    # Add required fields
    additional_fields.update(
        (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
    )

    yield from GetFolder(account=self.account).call(
            folders=self.folders,
            additional_fields=additional_fields,
            shape=ID_ONLY,
    )
def none(self)
Expand source code
def none(self):
    return QuerySet(self).none()
def people(self)
Expand source code
def people(self):
    return QuerySet(self).people()
def resolve(self)
Expand source code
def resolve(self):
    # Looks up the folders or folder IDs in the collection and returns full Folder instances with all fields set.
    from .base import BaseFolder
    resolveable_folders = []
    for f in self.folders:
        if isinstance(f, BaseFolder) and not f.get_folder_allowed:
            log.debug('GetFolder not allowed on folder %s. Non-complex fields must be fetched with FindFolder', f)
            yield f
        else:
            resolveable_folders.append(f)
    # Fetch all properties for the remaining folders of folder IDs
    additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=None)
    yield from self.__class__(account=self.account, folders=resolveable_folders).get_folders(
            additional_fields=additional_fields
    )
def subscribe_to_pull(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, timeout=60)
Expand source code
def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
    if not self.folders:
        log.debug('Folder list is empty')
        return
    yield from SubscribeToPull(account=self.account).call(
        folders=self.folders, event_types=event_types, watermark=watermark, timeout=timeout,
    )
def subscribe_to_push(self, callback_url, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, status_frequency=1)
Expand source code
def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
                      status_frequency=1):
    if not self.folders:
        log.debug('Folder list is empty')
        return
    yield from SubscribeToPush(account=self.account).call(
        folders=self.folders, event_types=event_types, watermark=watermark, status_frequency=status_frequency,
        url=callback_url,
    )
def subscribe_to_streaming(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'))
Expand source code
def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
    if not self.folders:
        log.debug('Folder list is empty')
        return
    yield from SubscribeToStreaming(account=self.account).call(folders=self.folders, event_types=event_types)
def sync_hierarchy(self, sync_state=None, only_fields=None)
Expand source code
def sync_hierarchy(self, sync_state=None, only_fields=None):
    folder = self._get_single_folder()
    if not folder:
        return
    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 folder.supported_fields(version=self.account.version)}
    else:
        for f in only_fields:
            folder.validate_field(field=f, version=self.account.version)
        # Remove ItemId and ChangeKey. We get them unconditionally
        additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}

    # Add required fields
    additional_fields.update(
        (FieldPath(field=folder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS)
    )

    svc = SyncFolderHierarchy(account=self.account)
    while True:
        yield from svc.call(
            folder=folder,
            shape=ID_ONLY,
            additional_fields=additional_fields,
            sync_state=sync_state,
        )
        if svc.sync_state == sync_state:
            # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here
            break
        sync_state = svc.sync_state  # Set the new sync state in the next call
        if svc.includes_last_item_in_range:  # Try again if there are more items
            break
    raise SyncCompleted(sync_state=svc.sync_state)
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None)
Expand source code
def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
    folder = self._get_single_folder()
    if not folder:
        return
    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 folder.allowed_item_fields(version=self.account.version)}
    else:
        for field in only_fields:
            folder.validate_item_field(field=field, version=self.account.version)
        # Remove ItemId and ChangeKey. We get them unconditionally
        additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute}

    svc = SyncFolderItems(account=self.account)
    while True:
        yield from svc.call(
            folder=folder,
            shape=ID_ONLY,
            additional_fields=additional_fields,
            sync_state=sync_state,
            ignore=ignore,
            max_changes_returned=max_changes_returned,
            sync_scope=sync_scope,
        )
        if svc.sync_state == sync_state:
            # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here
            break
        sync_state = svc.sync_state  # Set the new sync state in the next call
        if svc.includes_last_item_in_range:  # Try again if there are more items
            break
    raise SyncCompleted(sync_state=svc.sync_state)
def validate_item_field(self, field, version)
Expand source code
def validate_item_field(self, field, version):
    # For each field, check if the field is valid for any of the item models supported by this folder
    for item_model in self.supported_item_models:
        try:
            item_model.validate_field(field=field, version=version)
            break
        except InvalidField:
            continue
    else:
        raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))
def view(self, start, end, max_items=None, *args, **kwargs)

Implement the CalendarView option to FindItem. The difference between 'filter' and 'view' is that 'filter' only returns the master CalendarItem for recurring items, while 'view' unfolds recurring items and returns all CalendarItem occurrences as one would normally expect when presenting a calendar.

Supports the same semantics as filter, except for 'start' and 'end' keyword attributes which are both required and behave differently than filter. Here, they denote the start and end of the timespan of the view. All items the overlap the timespan are returned (items that end exactly on 'start' are also returned, for some reason).

EWS does not allow combining CalendarView with search restrictions (filter and exclude).

'max_items' defines the maximum number of items returned in this view. Optional.

:param start: :param end: :param max_items: (Default value = None) :return:

Expand source code
def view(self, start, end, max_items=None, *args, **kwargs):
    """Implement the CalendarView option to FindItem. The difference between 'filter' and 'view' is that 'filter'
    only returns the master CalendarItem for recurring items, while 'view' unfolds recurring items and returns all
    CalendarItem occurrences as one would normally expect when presenting a calendar.

    Supports the same semantics as filter, except for 'start' and 'end' keyword attributes which are both required
    and behave differently than filter. Here, they denote the start and end of the timespan of the view. All items
    the overlap the timespan are returned (items that end exactly on 'start' are also returned, for some reason).

    EWS does not allow combining CalendarView with search restrictions (filter and exclude).

    'max_items' defines the maximum number of items returned in this view. Optional.

    :param start:
    :param end:
    :param max_items:  (Default value = None)
    :return:
    """
    qs = QuerySet(self).filter(*args, **kwargs)
    qs.calendar_view = CalendarView(start=start, end=end, max_items=max_items)
    return qs
class ForwardItem (**kwargs)
Expand source code
class ForwardItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/forwarditem"""

    ELEMENT_NAME = 'ForwardItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class HTMLBody (...)

Helper to mark the 'body' field as a complex attribute.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/body

Expand source code
class HTMLBody(Body):
    """Helper to mark the 'body' field as a complex attribute.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/body
    """

    body_type = 'HTML'

Ancestors

Class variables

var body_type

Inherited members

class Identity (primary_smtp_address=None, smtp_address=None, upn=None, sid=None)

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:
    """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 ItemAttachment (**kwargs)
Expand source code
class ItemAttachment(Attachment):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/itemattachment"""

    ELEMENT_NAME = 'ItemAttachment'

    _item = ItemField(field_uri='Item')

    def __init__(self, **kwargs):
        kwargs['_item'] = kwargs.pop('item', None)
        super().__init__(**kwargs)

    @property
    def item(self):
        from .folders import BaseFolder
        if self.attachment_id is None:
            return self._item
        if self._item is not None:
            return self._item
        # We have an ID to the data but still haven't called GetAttachment to get the actual data. Do that now.
        if not self.parent_item or not self.parent_item.account:
            raise ValueError('%s must have an account' % self.__class__.__name__)
        additional_fields = {
            FieldPath(field=f) for f in BaseFolder.allowed_item_fields(version=self.parent_item.account.version)
        }
        attachment = GetAttachment(account=self.parent_item.account).get(
            items=[self.attachment_id], include_mime_content=True, body_type=None, filter_html_content=None,
            additional_fields=additional_fields,
        )
        self._item = attachment.item
        return self._item

    @item.setter
    def item(self, value):
        from .items import Item
        if not isinstance(value, Item):
            raise ValueError("'value' %r must be an Item object" % value)
        self._item = value

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
        kwargs['item'] = kwargs.pop('_item')
        cls._clear(elem)
        return cls(**kwargs)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
    kwargs['item'] = kwargs.pop('_item')
    cls._clear(elem)
    return cls(**kwargs)

Instance variables

var item
Expand source code
@property
def item(self):
    from .folders import BaseFolder
    if self.attachment_id is None:
        return self._item
    if self._item is not None:
        return self._item
    # We have an ID to the data but still haven't called GetAttachment to get the actual data. Do that now.
    if not self.parent_item or not self.parent_item.account:
        raise ValueError('%s must have an account' % self.__class__.__name__)
    additional_fields = {
        FieldPath(field=f) for f in BaseFolder.allowed_item_fields(version=self.parent_item.account.version)
    }
    attachment = GetAttachment(account=self.parent_item.account).get(
        items=[self.attachment_id], include_mime_content=True, body_type=None, filter_html_content=None,
        additional_fields=additional_fields,
    )
    self._item = attachment.item
    return self._item

Inherited members

class ItemId (*args, **kwargs)

'id' and 'changekey' are UUIDs generated by Exchange.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/itemid

Expand source code
class ItemId(BaseItemId):
    """'id' and 'changekey' are UUIDs generated by Exchange.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/itemid
    """

    ELEMENT_NAME = 'ItemId'
    ID_ATTR = 'Id'
    CHANGEKEY_ATTR = 'ChangeKey'

    id = IdField(field_uri=ID_ATTR, is_required=True)
    changekey = IdField(field_uri=CHANGEKEY_ATTR, is_required=False)

Ancestors

Subclasses

Class variables

var CHANGEKEY_ATTR
var ELEMENT_NAME
var FIELDS
var ID_ATTR

Instance variables

var changekey
var id

Inherited members

class Mailbox (**kwargs)
Expand source code
class Mailbox(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailbox"""

    ELEMENT_NAME = 'Mailbox'
    MAILBOX = 'Mailbox'
    ONE_OFF = 'OneOff'
    MAILBOX_TYPE_CHOICES = {
            Choice(MAILBOX), Choice('PublicDL'), Choice('PrivateDL'), Choice('Contact'), Choice('PublicFolder'),
            Choice('Unknown'), Choice(ONE_OFF), Choice('GroupMailbox', supported_from=EXCHANGE_2013)
        }

    name = TextField(field_uri='Name')
    email_address = EmailAddressField(field_uri='EmailAddress')
    # RoutingType values are not restricted:
    # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/routingtype-emailaddresstype
    routing_type = TextField(field_uri='RoutingType', default='SMTP')
    mailbox_type = ChoiceField(field_uri='MailboxType', choices=MAILBOX_TYPE_CHOICES, default=MAILBOX)
    item_id = EWSElementField(value_cls=ItemId, is_read_only=True)

    def clean(self, version=None):
        super().clean(version=version)

        if self.mailbox_type != self.ONE_OFF and not self.email_address and not self.item_id:
            # A OneOff Mailbox (a one-off member of a personal distribution list) may lack these fields, but other
            # Mailboxes require at least one. See also "Remarks" section of
            # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailbox
            raise ValueError("Mailbox type %r must have either 'email_address' or 'item_id' set" % self.mailbox_type)

    def __hash__(self):
        # Exchange may add 'mailbox_type' and 'name' on insert. We're satisfied if the item_id or email address matches.
        if self.item_id:
            return hash(self.item_id)
        if self.email_address:
            return hash(self.email_address.lower())
        return super().__hash__()

Ancestors

Subclasses

Class variables

var ELEMENT_NAME
var FIELDS
var MAILBOX
var MAILBOX_TYPE_CHOICES
var ONE_OFF

Instance variables

var email_address
var item_id
var mailbox_type
var name
var routing_type

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)

    if self.mailbox_type != self.ONE_OFF and not self.email_address and not self.item_id:
        # A OneOff Mailbox (a one-off member of a personal distribution list) may lack these fields, but other
        # Mailboxes require at least one. See also "Remarks" section of
        # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailbox
        raise ValueError("Mailbox type %r must have either 'email_address' or 'item_id' set" % self.mailbox_type)

Inherited members

class Message (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/message-ex15websvcsotherref

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class Message(Item):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/message-ex15websvcsotherref
    """

    ELEMENT_NAME = 'Message'

    sender = MailboxField(field_uri='message:Sender', is_read_only=True, is_read_only_after_send=True)
    to_recipients = MailboxListField(field_uri='message:ToRecipients', is_read_only_after_send=True,
                                     is_searchable=False)
    cc_recipients = MailboxListField(field_uri='message:CcRecipients', is_read_only_after_send=True,
                                     is_searchable=False)
    bcc_recipients = MailboxListField(field_uri='message:BccRecipients', is_read_only_after_send=True,
                                      is_searchable=False)
    is_read_receipt_requested = BooleanField(field_uri='message:IsReadReceiptRequested',
                                             is_required=True, default=False, is_read_only_after_send=True)
    is_delivery_receipt_requested = BooleanField(field_uri='message:IsDeliveryReceiptRequested', is_required=True,
                                                 default=False, is_read_only_after_send=True)
    conversation_index = Base64Field(field_uri='message:ConversationIndex', is_read_only=True)
    conversation_topic = CharField(field_uri='message:ConversationTopic', is_read_only=True)
    # Rename 'From' to 'author'. We can't use fieldname 'from' since it's a Python keyword.
    author = MailboxField(field_uri='message:From', is_read_only_after_send=True)
    message_id = CharField(field_uri='message:InternetMessageId', is_read_only_after_send=True)
    is_read = BooleanField(field_uri='message:IsRead', is_required=True, default=False)
    is_response_requested = BooleanField(field_uri='message:IsResponseRequested', default=False, is_required=True)
    references = TextField(field_uri='message:References')
    reply_to = MailboxListField(field_uri='message:ReplyTo', is_read_only_after_send=True, is_searchable=False)
    received_by = MailboxField(field_uri='message:ReceivedBy', is_read_only=True)
    received_representing = MailboxField(field_uri='message:ReceivedRepresenting', is_read_only=True)
    reminder_message_data = EWSElementField(field_uri='message:ReminderMessageData', value_cls=ReminderMessageData,
                                            supported_from=EXCHANGE_2013_SP1, is_read_only=True)

    @require_account
    def send(self, save_copy=True, copy_to_folder=None, conflict_resolution=AUTO_RESOLVE,
             send_meeting_invitations=SEND_TO_NONE):
        # Only sends a message. The message can either be an existing draft stored in EWS or a new message that does
        # not yet exist in EWS.
        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.account.sent  # 'Sent' is default EWS behaviour
        if self.id:
            SendItem(account=self.account).get(items=[self], saved_item_folder=copy_to_folder)
            # The item will be deleted from the original folder
            self._id = None
            self.folder = copy_to_folder
            return None

        # New message
        if copy_to_folder:
            # This would better be done via send_and_save() but lets just support it here
            self.folder = copy_to_folder
            return self.send_and_save(conflict_resolution=conflict_resolution,
                                      send_meeting_invitations=send_meeting_invitations)

        if self.account.version.build < EXCHANGE_2013 and self.attachments:
            # At least some versions prior to Exchange 2013 can't send attachments immediately. You need to first save,
            # then attach, then send. This is done in send_and_save(). send() will delete the item again.
            self.send_and_save(conflict_resolution=conflict_resolution,
                               send_meeting_invitations=send_meeting_invitations)
            return None

        self._create(message_disposition=SEND_ONLY, send_meeting_invitations=send_meeting_invitations)
        return None

    def send_and_save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE,
                      send_meeting_invitations=SEND_TO_NONE):
        # Sends Message and saves a copy in the parent folder. Does not return an ItemId.
        if self.id:
            self._update(
                update_fieldnames=update_fields,
                message_disposition=SEND_AND_SAVE_COPY,
                conflict_resolution=conflict_resolution,
                send_meeting_invitations=send_meeting_invitations
            )
        else:
            if self.account.version.build < EXCHANGE_2013 and self.attachments:
                # At least some versions prior to Exchange 2013 can't send-and-save attachments immediately. You need
                # to first save, then attach, then send. This is done in save().
                self.save(update_fields=update_fields, conflict_resolution=conflict_resolution,
                          send_meeting_invitations=send_meeting_invitations)
                self.send(save_copy=False, conflict_resolution=conflict_resolution,
                          send_meeting_invitations=send_meeting_invitations)
            else:
                res = self._create(
                    message_disposition=SEND_AND_SAVE_COPY,
                    send_meeting_invitations=send_meeting_invitations
                )
                if res is not True:
                    raise ValueError('Unexpected response in send-only mode')

    @require_id
    def create_reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
        if to_recipients is None:
            if not self.author:
                raise ValueError("'to_recipients' must be set when message has no 'author'")
            to_recipients = [self.author]
        return ReplyToItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            subject=subject,
            new_body=body,
            to_recipients=to_recipients,
            cc_recipients=cc_recipients,
            bcc_recipients=bcc_recipients,
        )

    def reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
        self.create_reply(
            subject,
            body,
            to_recipients,
            cc_recipients,
            bcc_recipients
        ).send()

    @require_id
    def create_reply_all(self, subject, body):
        to_recipients = list(self.to_recipients) if self.to_recipients else []
        if self.author:
            to_recipients.append(self.author)
        return ReplyAllToItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            subject=subject,
            new_body=body,
            to_recipients=to_recipients,
            cc_recipients=self.cc_recipients,
            bcc_recipients=self.bcc_recipients,
        )

    def reply_all(self, subject, body):
        self.create_reply_all(subject, body).send()

    def mark_as_junk(self, is_junk=True, move_item=True):
        """Mark or un-marks items as junk email.

        :param is_junk: If True, the sender will be added from the blocked sender list. Otherwise, the sender will be
        removed.
        :param move_item: If true, the item will be moved to the junk folder.
        :return:
        """
        res = MarkAsJunk(account=self.account).get(
            items=[self], is_junk=is_junk, move_item=move_item, expect_result=move_item
        )
        if res is None:
            return
        self.folder = self.account.junk if is_junk else self.account.inbox
        self.id, self.changekey = res

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var author
var bcc_recipients
var cc_recipients
var conversation_index
var conversation_topic
var is_delivery_receipt_requested
var is_read
var is_read_receipt_requested
var is_response_requested
var message_id
var received_by
var received_representing
var references
var reminder_message_data
var reply_to
var sender
var to_recipients

Methods

def create_reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None)
Expand source code
@require_id
def create_reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
    if to_recipients is None:
        if not self.author:
            raise ValueError("'to_recipients' must be set when message has no 'author'")
        to_recipients = [self.author]
    return ReplyToItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        subject=subject,
        new_body=body,
        to_recipients=to_recipients,
        cc_recipients=cc_recipients,
        bcc_recipients=bcc_recipients,
    )
def create_reply_all(self, subject, body)
Expand source code
@require_id
def create_reply_all(self, subject, body):
    to_recipients = list(self.to_recipients) if self.to_recipients else []
    if self.author:
        to_recipients.append(self.author)
    return ReplyAllToItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        subject=subject,
        new_body=body,
        to_recipients=to_recipients,
        cc_recipients=self.cc_recipients,
        bcc_recipients=self.bcc_recipients,
    )
def mark_as_junk(self, is_junk=True, move_item=True)

Mark or un-marks items as junk email.

:param is_junk: If True, the sender will be added from the blocked sender list. Otherwise, the sender will be removed. :param move_item: If true, the item will be moved to the junk folder. :return:

Expand source code
def mark_as_junk(self, is_junk=True, move_item=True):
    """Mark or un-marks items as junk email.

    :param is_junk: If True, the sender will be added from the blocked sender list. Otherwise, the sender will be
    removed.
    :param move_item: If true, the item will be moved to the junk folder.
    :return:
    """
    res = MarkAsJunk(account=self.account).get(
        items=[self], is_junk=is_junk, move_item=move_item, expect_result=move_item
    )
    if res is None:
        return
    self.folder = self.account.junk if is_junk else self.account.inbox
    self.id, self.changekey = res
def reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None)
Expand source code
def reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
    self.create_reply(
        subject,
        body,
        to_recipients,
        cc_recipients,
        bcc_recipients
    ).send()
def reply_all(self, subject, body)
Expand source code
def reply_all(self, subject, body):
    self.create_reply_all(subject, body).send()
def send(self, save_copy=True, copy_to_folder=None, conflict_resolution='AutoResolve', send_meeting_invitations='SendToNone')
Expand source code
@require_account
def send(self, save_copy=True, copy_to_folder=None, conflict_resolution=AUTO_RESOLVE,
         send_meeting_invitations=SEND_TO_NONE):
    # Only sends a message. The message can either be an existing draft stored in EWS or a new message that does
    # not yet exist in EWS.
    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.account.sent  # 'Sent' is default EWS behaviour
    if self.id:
        SendItem(account=self.account).get(items=[self], saved_item_folder=copy_to_folder)
        # The item will be deleted from the original folder
        self._id = None
        self.folder = copy_to_folder
        return None

    # New message
    if copy_to_folder:
        # This would better be done via send_and_save() but lets just support it here
        self.folder = copy_to_folder
        return self.send_and_save(conflict_resolution=conflict_resolution,
                                  send_meeting_invitations=send_meeting_invitations)

    if self.account.version.build < EXCHANGE_2013 and self.attachments:
        # At least some versions prior to Exchange 2013 can't send attachments immediately. You need to first save,
        # then attach, then send. This is done in send_and_save(). send() will delete the item again.
        self.send_and_save(conflict_resolution=conflict_resolution,
                           send_meeting_invitations=send_meeting_invitations)
        return None

    self._create(message_disposition=SEND_ONLY, send_meeting_invitations=send_meeting_invitations)
    return None
def send_and_save(self, update_fields=None, conflict_resolution='AutoResolve', send_meeting_invitations='SendToNone')
Expand source code
def send_and_save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE,
                  send_meeting_invitations=SEND_TO_NONE):
    # Sends Message and saves a copy in the parent folder. Does not return an ItemId.
    if self.id:
        self._update(
            update_fieldnames=update_fields,
            message_disposition=SEND_AND_SAVE_COPY,
            conflict_resolution=conflict_resolution,
            send_meeting_invitations=send_meeting_invitations
        )
    else:
        if self.account.version.build < EXCHANGE_2013 and self.attachments:
            # At least some versions prior to Exchange 2013 can't send-and-save attachments immediately. You need
            # to first save, then attach, then send. This is done in save().
            self.save(update_fields=update_fields, conflict_resolution=conflict_resolution,
                      send_meeting_invitations=send_meeting_invitations)
            self.send(save_copy=False, conflict_resolution=conflict_resolution,
                      send_meeting_invitations=send_meeting_invitations)
        else:
            res = self._create(
                message_disposition=SEND_AND_SAVE_COPY,
                send_meeting_invitations=send_meeting_invitations
            )
            if res is not True:
                raise ValueError('Unexpected response in send-only mode')

Inherited members

class NoVerifyHTTPAdapter (pool_connections=10, pool_maxsize=10, max_retries=0, pool_block=False)

An HTTP adapter that ignores TLS validation errors. Use at own risk.

Expand source code
class NoVerifyHTTPAdapter(requests.adapters.HTTPAdapter):
    """An HTTP adapter that ignores TLS validation errors. Use at own risk."""

    def cert_verify(self, conn, url, verify, cert):
        # pylint: disable=unused-argument
        # We're overriding a method so we have to keep the signature
        super().cert_verify(conn=conn, url=url, verify=False, cert=cert)

Ancestors

  • requests.adapters.HTTPAdapter
  • requests.adapters.BaseAdapter

Methods

def cert_verify(self, conn, url, verify, cert)

Verify a SSL certificate. This method should not be called from user code, and is only exposed for use when subclassing the :class:HTTPAdapter <requests.adapters.HTTPAdapter>.

:param conn: The urllib3 connection object associated with the cert. :param url: The requested URL. :param verify: Either a boolean, in which case it controls whether we verify the server's TLS certificate, or a string, in which case it must be a path to a CA bundle to use :param cert: The SSL certificate to verify.

Expand source code
def cert_verify(self, conn, url, verify, cert):
    # pylint: disable=unused-argument
    # We're overriding a method so we have to keep the signature
    super().cert_verify(conn=conn, url=url, verify=False, cert=cert)
class OAuth2AuthorizationCodeCredentials (authorization_code=None, access_token=None, **kwargs)

Login info for OAuth 2.0 authentication using the authorization code grant type. This can be used in one of several ways: * Given an authorization code, client ID, and client secret, fetch a token ourselves and refresh it as needed if supplied with a refresh token. * Given an existing access token, refresh token, client ID, and client secret, use the access token until it expires and then refresh it as needed. * Given only an existing access token, use it until it expires. This can be used to let the calling application refresh tokens itself by subclassing and implementing refresh().

Unlike the base (client credentials) grant, authorization code credentials don't require a Microsoft tenant ID because each access token (and the authorization code used to get the access token) is restricted to a single tenant.

:param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing :param client_secret: Secret associated with the OAuth application :param tenant_id: Microsoft tenant ID of the account to access :param identity: An Identity object representing the account that these credentials are connected to. :param authorization_code: Code obtained when authorizing the application to access an account. In combination with client_id and client_secret, will be used to obtain an access token. :param access_token: Previously-obtained access token. If a token exists and the application will handle refreshing by itself (or opts not to handle it), this parameter alone is sufficient.

Expand source code
class OAuth2AuthorizationCodeCredentials(OAuth2Credentials):
    """Login info for OAuth 2.0 authentication using the authorization code grant type. This can be used in one of
    several ways:
    * Given an authorization code, client ID, and client secret, fetch a token ourselves and refresh it as needed if
      supplied with a refresh token.
    * Given an existing access token, refresh token, client ID, and client secret, use the access token until it
      expires and then refresh it as needed.
    * Given only an existing access token, use it until it expires. This can be used to let the calling application
      refresh tokens itself by subclassing and implementing refresh().

    Unlike the base (client credentials) grant, authorization code credentials don't require a Microsoft tenant ID
    because each access token (and the authorization code used to get the access token) is restricted to a single
    tenant.
    """

    def __init__(self, authorization_code=None, access_token=None, **kwargs):
        """

        :param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing
        :param client_secret: Secret associated with the OAuth application
        :param tenant_id: Microsoft tenant ID of the account to access
        :param identity: An Identity object representing the account that these credentials are connected to.
        :param authorization_code: Code obtained when authorizing the application to access an account. In combination
          with client_id and client_secret, will be used to obtain an access token.
        :param access_token: Previously-obtained access token. If a token exists and the application will handle
          refreshing by itself (or opts not to handle it), this parameter alone is sufficient.
        """
        super().__init__(**kwargs)
        self.authorization_code = authorization_code
        if access_token is not None and not isinstance(access_token, dict):
            raise ValueError("'access_token' must be an OAuth2Token")
        self.access_token = access_token

    def __repr__(self):
        return self.__class__.__name__ + repr(
            (self.client_id, '[client_secret]', '[authorization_code]', '[access_token]')
        )

    def __str__(self):
        client_id = self.client_id
        credential = '[access_token]' if self.access_token is not None else \
            ('[authorization_code]' if self.authorization_code is not None else None)
        description = ' '.join(filter(None, [client_id, credential]))
        return description or '[underspecified credentials]'

Ancestors

Inherited members

class OAuth2Credentials (client_id, client_secret, tenant_id=None, identity=None)

Login info for OAuth 2.0 client credentials authentication, as well as a base for other OAuth 2.0 grant types.

This is primarily useful for in-house applications accessing data from a single Microsoft account. For applications that will access multiple tenants' data, the client credentials flow does not give the application enough information to restrict end users' access to the appropriate account. Use OAuth2AuthorizationCodeCredentials and the associated auth code grant type for multi-tenant applications.

:param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing :param client_secret: Secret associated with the OAuth application :param tenant_id: Microsoft tenant ID of the account to access :param identity: An Identity object representing the account that these credentials are connected to.

Expand source code
class OAuth2Credentials(BaseCredentials):
    """Login info for OAuth 2.0 client credentials authentication, as well as a base for other OAuth 2.0 grant types.

    This is primarily useful for in-house applications accessing data from a single Microsoft account. For applications
    that will access multiple tenants' data, the client credentials flow does not give the application enough
    information to restrict end users' access to the appropriate account. Use OAuth2AuthorizationCodeCredentials and
    the associated auth code grant type for multi-tenant applications.
    """

    def __init__(self, client_id, client_secret, tenant_id=None, identity=None):
        """

        :param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing
        :param client_secret: Secret associated with the OAuth application
        :param tenant_id: Microsoft tenant ID of the account to access
        :param identity: An Identity object representing the account that these credentials are connected to.
        """
        super().__init__()
        self.client_id = client_id
        self.client_secret = client_secret
        self.tenant_id = tenant_id
        self.identity = identity
        # When set, access_token is a dict (or an oauthlib.oauth2.OAuth2Token, which is also a dict)
        self.access_token = None

    def refresh(self, session):
        # Creating a new session gets a new access token, so there's no work here to refresh the credentials. This
        # implementation just makes sure we don't raise a NotImplementedError.
        pass

    def on_token_auto_refreshed(self, access_token):
        """Set the access_token. Called after the access token is refreshed (requests-oauthlib can automatically
        refresh tokens if given an OAuth client ID and secret, so this is how our copy of the token stays up-to-date).
        Applications that cache access tokens can override this to store the new token - just remember to call the
        super() method.

        :param access_token: New token obtained by refreshing
        """
        # Ensure we don't update the object in the middle of a new session being created, which could cause a race.
        if not isinstance(access_token, dict):
            raise ValueError("'access_token' must be an OAuth2Token")
        with self.lock:
            log.debug('%s auth token for %s', 'Refreshing' if self.access_token else 'Setting', self.client_id)
            self.access_token = access_token

    def _get_hash_values(self):
        # 'access_token' may be refreshed once in a while. This should not affect the hash signature.
        # 'identity' is just informational and should also not affect the hash signature.
        return (getattr(self, k) for k in self.__dict__ if k not in ('_lock', 'identity', 'access_token'))

    def sig(self):
        # Like hash(self), but pulls in the access token. Protocol.refresh_credentials() uses this to find out
        # if the access_token needs to be refreshed.
        res = []
        for k in self.__dict__:
            if k in ('_lock', 'identity'):
                continue
            if k == 'access_token':
                res.append(self.access_token['access_token'] if self.access_token else None)
                continue
            res.append(getattr(self, k))
        return hash(tuple(res))

    def __repr__(self):
        return self.__class__.__name__ + repr((self.client_id, '********'))

    def __str__(self):
        return self.client_id

Ancestors

Subclasses

Methods

def on_token_auto_refreshed(self, access_token)

Set the access_token. Called after the access token is refreshed (requests-oauthlib can automatically refresh tokens if given an OAuth client ID and secret, so this is how our copy of the token stays up-to-date). Applications that cache access tokens can override this to store the new token - just remember to call the super() method.

:param access_token: New token obtained by refreshing

Expand source code
def on_token_auto_refreshed(self, access_token):
    """Set the access_token. Called after the access token is refreshed (requests-oauthlib can automatically
    refresh tokens if given an OAuth client ID and secret, so this is how our copy of the token stays up-to-date).
    Applications that cache access tokens can override this to store the new token - just remember to call the
    super() method.

    :param access_token: New token obtained by refreshing
    """
    # Ensure we don't update the object in the middle of a new session being created, which could cause a race.
    if not isinstance(access_token, dict):
        raise ValueError("'access_token' must be an OAuth2Token")
    with self.lock:
        log.debug('%s auth token for %s', 'Refreshing' if self.access_token else 'Setting', self.client_id)
        self.access_token = access_token
def sig(self)
Expand source code
def sig(self):
    # Like hash(self), but pulls in the access token. Protocol.refresh_credentials() uses this to find out
    # if the access_token needs to be refreshed.
    res = []
    for k in self.__dict__:
        if k in ('_lock', 'identity'):
            continue
        if k == 'access_token':
            res.append(self.access_token['access_token'] if self.access_token else None)
            continue
        res.append(getattr(self, k))
    return hash(tuple(res))

Inherited members

class OofSettings (**kwargs)
Expand source code
class OofSettings(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/oofsettings"""

    ELEMENT_NAME = 'OofSettings'
    REQUEST_ELEMENT_NAME = 'UserOofSettings'

    ENABLED = 'Enabled'
    SCHEDULED = 'Scheduled'
    DISABLED = 'Disabled'
    STATE_CHOICES = (ENABLED, SCHEDULED, DISABLED)

    state = ChoiceField(field_uri='OofState', is_required=True, choices={Choice(c) for c in STATE_CHOICES})
    external_audience = ChoiceField(field_uri='ExternalAudience',
                                    choices={Choice('None'), Choice('Known'), Choice('All')}, default='All')
    start = DateTimeField(field_uri='StartTime')
    end = DateTimeField(field_uri='EndTime')
    internal_reply = MessageField(field_uri='InternalReply')
    external_reply = MessageField(field_uri='ExternalReply')

    def clean(self, version=None):
        super().clean(version=version)
        if self.state == self.SCHEDULED:
            if not self.start or not self.end:
                raise ValueError("'start' and 'end' must be set when state is '%s'" % self.SCHEDULED)
            if self.start >= self.end:
                raise ValueError("'start' must be before 'end'")
            if self.end < datetime.datetime.now(tz=UTC):
                raise ValueError("'end' must be in the future")
        if self.state != self.DISABLED and (not self.internal_reply or not self.external_reply):
            raise ValueError("'internal_reply' and 'external_reply' must be set when state is not '%s'" % self.DISABLED)

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {}
        for attr in ('state', 'external_audience', 'internal_reply', 'external_reply'):
            f = cls.get_field_by_fieldname(attr)
            kwargs[attr] = f.from_xml(elem=elem, account=account)
        kwargs.update(OutOfOffice.duration_to_start_end(elem=elem, account=account))
        cls._clear(elem)
        return cls(**kwargs)

    def to_xml(self, version):
        self.clean(version=version)
        elem = create_element('t:%s' % self.REQUEST_ELEMENT_NAME)
        for attr in ('state', 'external_audience'):
            value = getattr(self, attr)
            if value is None:
                continue
            f = self.get_field_by_fieldname(attr)
            set_xml_value(elem, f.to_xml(value, version=version), version=version)
        if self.start or self.end:
            duration = create_element('t:Duration')
            if self.start:
                f = self.get_field_by_fieldname('start')
                set_xml_value(duration, f.to_xml(self.start, version=version), version)
            if self.end:
                f = self.get_field_by_fieldname('end')
                set_xml_value(duration, f.to_xml(self.end, version=version), version)
            elem.append(duration)
        for attr in ('internal_reply', 'external_reply'):
            value = getattr(self, attr)
            if value is None:
                value = ''  # The value can be empty, but the XML element must always be present
            f = self.get_field_by_fieldname(attr)
            set_xml_value(elem, f.to_xml(value, version=version), version)
        return elem

    def __hash__(self):
        # Customize comparison
        if self.state == self.DISABLED:
            # All values except state are ignored by the server
            relevant_attrs = ('state',)
        elif self.state != self.SCHEDULED:
            # 'start' and 'end' values are ignored by the server, and the server always returns today's date
            relevant_attrs = tuple(f.name for f in self.FIELDS if f.name not in ('start', 'end'))
        else:
            relevant_attrs = tuple(f.name for f in self.FIELDS)
        return hash(tuple(getattr(self, attr) for attr in relevant_attrs))

Ancestors

Class variables

var DISABLED
var ELEMENT_NAME
var ENABLED
var FIELDS
var REQUEST_ELEMENT_NAME
var SCHEDULED
var STATE_CHOICES

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    kwargs = {}
    for attr in ('state', 'external_audience', 'internal_reply', 'external_reply'):
        f = cls.get_field_by_fieldname(attr)
        kwargs[attr] = f.from_xml(elem=elem, account=account)
    kwargs.update(OutOfOffice.duration_to_start_end(elem=elem, account=account))
    cls._clear(elem)
    return cls(**kwargs)

Instance variables

var end
var external_audience
var external_reply
var internal_reply
var start
var state

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)
    if self.state == self.SCHEDULED:
        if not self.start or not self.end:
            raise ValueError("'start' and 'end' must be set when state is '%s'" % self.SCHEDULED)
        if self.start >= self.end:
            raise ValueError("'start' must be before 'end'")
        if self.end < datetime.datetime.now(tz=UTC):
            raise ValueError("'end' must be in the future")
    if self.state != self.DISABLED and (not self.internal_reply or not self.external_reply):
        raise ValueError("'internal_reply' and 'external_reply' must be set when state is not '%s'" % self.DISABLED)
def to_xml(self, version)
Expand source code
def to_xml(self, version):
    self.clean(version=version)
    elem = create_element('t:%s' % self.REQUEST_ELEMENT_NAME)
    for attr in ('state', 'external_audience'):
        value = getattr(self, attr)
        if value is None:
            continue
        f = self.get_field_by_fieldname(attr)
        set_xml_value(elem, f.to_xml(value, version=version), version=version)
    if self.start or self.end:
        duration = create_element('t:Duration')
        if self.start:
            f = self.get_field_by_fieldname('start')
            set_xml_value(duration, f.to_xml(self.start, version=version), version)
        if self.end:
            f = self.get_field_by_fieldname('end')
            set_xml_value(duration, f.to_xml(self.end, version=version), version)
        elem.append(duration)
    for attr in ('internal_reply', 'external_reply'):
        value = getattr(self, attr)
        if value is None:
            value = ''  # The value can be empty, but the XML element must always be present
        f = self.get_field_by_fieldname(attr)
        set_xml_value(elem, f.to_xml(value, version=version), version)
    return elem

Inherited members

class PostItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postitem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class PostItem(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postitem"""

    ELEMENT_NAME = 'PostItem'

    conversation_index = Message.FIELDS['conversation_index']
    conversation_topic = Message.FIELDS['conversation_topic']

    author = Message.FIELDS['author']
    message_id = Message.FIELDS['message_id']
    is_read = Message.FIELDS['is_read']

    posted_time = DateTimeField(field_uri='postitem:PostedTime', is_read_only=True)
    references = TextField(field_uri='message:References')
    sender = MailboxField(field_uri='message:Sender', is_read_only=True, is_read_only_after_send=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var author
var conversation_index
var conversation_topic
var is_read
var message_id
var posted_time
var references
var sender

Inherited members

class Q (*args, **kwargs)

A class with an API similar to Django Q objects. Used to implemnt advanced filtering logic.

Expand source code
class Q:
    """A class with an API similar to Django Q objects. Used to implemnt advanced filtering logic."""

    # Connection types
    AND = 'AND'
    OR = 'OR'
    NOT = 'NOT'
    NEVER = 'NEVER'  # This is not specified by EWS. We use it for queries that will never match, e.g. 'foo__in=()'
    CONN_TYPES = {AND, OR, NOT, NEVER}

    # EWS Operators
    EQ = '=='
    NE = '!='
    GT = '>'
    GTE = '>='
    LT = '<'
    LTE = '<='
    EXACT = 'exact'
    IEXACT = 'iexact'
    CONTAINS = 'contains'
    ICONTAINS = 'icontains'
    STARTSWITH = 'startswith'
    ISTARTSWITH = 'istartswith'
    EXISTS = 'exists'
    OP_TYPES = {EQ, NE, GT, GTE, LT, LTE, EXACT, IEXACT, CONTAINS, ICONTAINS, STARTSWITH, ISTARTSWITH, EXISTS}
    CONTAINS_OPS = {EXACT, IEXACT, CONTAINS, ICONTAINS, STARTSWITH, ISTARTSWITH}

    # Valid lookups
    LOOKUP_RANGE = 'range'
    LOOKUP_IN = 'in'
    LOOKUP_NOT = 'not'
    LOOKUP_GT = 'gt'
    LOOKUP_GTE = 'gte'
    LOOKUP_LT = 'lt'
    LOOKUP_LTE = 'lte'
    LOOKUP_EXACT = 'exact'
    LOOKUP_IEXACT = 'iexact'
    LOOKUP_CONTAINS = 'contains'
    LOOKUP_ICONTAINS = 'icontains'
    LOOKUP_STARTSWITH = 'startswith'
    LOOKUP_ISTARTSWITH = 'istartswith'
    LOOKUP_EXISTS = 'exists'
    LOOKUP_TYPES = {LOOKUP_RANGE, LOOKUP_IN, LOOKUP_NOT, LOOKUP_GT, LOOKUP_GTE, LOOKUP_LT, LOOKUP_LTE, LOOKUP_EXACT,
                    LOOKUP_IEXACT, LOOKUP_CONTAINS, LOOKUP_ICONTAINS, LOOKUP_STARTSWITH, LOOKUP_ISTARTSWITH,
                    LOOKUP_EXISTS}

    __slots__ = 'conn_type', 'field_path', 'op', 'value', 'children', 'query_string'

    def __init__(self, *args, **kwargs):
        self.conn_type = kwargs.pop('conn_type', self.AND)

        self.field_path = None  # Name of the field we want to filter on
        self.op = None
        self.value = None
        self.query_string = None

        # Parsing of args and kwargs may require child elements
        self.children = []

        # Check for query string as the only argument
        if not kwargs and len(args) == 1 and isinstance(args[0], str):
            self.query_string = args[0]
            args = ()

        # Parse args which must now be Q objects
        for q in args:
            if not isinstance(q, self.__class__):
                raise ValueError("Non-keyword arg %r must be a Q instance" % q)
        self.children.extend(args)

        # Parse keyword args and extract the filter
        is_single_kwarg = len(args) == 0 and len(kwargs) == 1
        for key, value in kwargs.items():
            self.children.extend(
                self._get_children_from_kwarg(key=key, value=value, is_single_kwarg=is_single_kwarg)
            )

        # Simplify this object
        self.reduce()

        # Final sanity check
        self._check_integrity()

    def _get_children_from_kwarg(self, key, value, is_single_kwarg=False):
        """Generate Q objects corresponding to a single keyword argument. Make this a leaf if there are no children to
        generate.
        """
        key_parts = key.rsplit('__', 1)
        if len(key_parts) == 2 and key_parts[1] in self.LOOKUP_TYPES:
            # This is a kwarg with a lookup at the end
            field_path, lookup = key_parts
            if lookup == self.LOOKUP_EXISTS:
                # value=True will fall through to further processing
                if not value:
                    return [~self.__class__(**{key: True})]

            if lookup == self.LOOKUP_RANGE:
                # EWS doesn't have a 'range' operator. Emulate 'foo__range=(1, 2)' as 'foo__gte=1 and foo__lte=2'
                # (both values inclusive).
                if len(value) != 2:
                    raise ValueError("Value of lookup '%s' must have exactly 2 elements" % key)
                return [
                    self.__class__(**{'%s__gte' % field_path: value[0]}),
                    self.__class__(**{'%s__lte' % field_path: value[1]}),
                ]

            # Filtering on list types is a bit quirky. The only lookup type I have found to work is:
            #
            #     item:Categories == 'foo' AND item:Categories == 'bar' AND ...
            #
            #     item:Categories == 'foo' OR item:Categories == 'bar' OR ...
            #
            # The former returns items that have all these categories, but maybe also others. The latter returns
            # items that have at least one of these categories. This translates to the 'contains' and 'in' lookups,
            # respectively. Both versions are case-insensitive.
            #
            # Exact matching and case-sensitive or partial-string matching is not possible since that requires the
            # 'Contains' element which only supports matching on string elements, not arrays.
            #
            # Exact matching of categories (i.e. match ['a', 'b'] but not ['a', 'b', 'c']) could be implemented by
            # post-processing items by fetch the categories field unconditionally and removing the items that don't
            # have an exact match.
            if lookup == self.LOOKUP_IN:
                # EWS doesn't have an '__in' operator. Allow '__in' lookups on list and non-list field types,
                # specifying a list value. We'll emulate it as a set of OR'ed exact matches.
                if not is_iterable(value, generators_allowed=True):
                    raise ValueError("Value for lookup %r must be a list" % key)
                children = [self.__class__(**{field_path: v}) for v in value]
                if not children:
                    # This is an '__in' operator with an empty list as the value. We interpret it to mean "is foo
                    # contained in the empty set?" which is always false. Mark this Q object as such.
                    return [self.__class__(conn_type=self.NEVER)]
                return [self.__class__(*children, conn_type=self.OR)]

            if lookup == self.LOOKUP_CONTAINS and is_iterable(value, generators_allowed=True):
                # A '__contains' lookup with an list as the value ony makes sense for list fields, since exact match
                # on multiple distinct values will always fail for single-value fields.
                #
                # An empty list as value is allowed. We interpret it to mean "are all values in the empty set contained
                # in foo?" which is always true.
                children = [self.__class__(**{field_path: v}) for v in value]
                return [self.__class__(*children, conn_type=self.AND)]

            try:
                op = self._lookup_to_op(lookup)
            except KeyError:
                raise ValueError("Lookup '%s' is not supported (called as '%s=%r')" % (lookup, key, value))
        else:
            field_path, op = key, self.EQ

        if not is_single_kwarg:
            return [self.__class__(**{key: value})]

        # This is a single-kwarg Q object with a lookup that requires a single value. Make this a leaf
        self.field_path = field_path
        self.op = op
        self.value = value
        return []

    def reduce(self):
        """Simplify this object, if possible."""
        self._reduce_children()
        self._promote()

    def _reduce_children(self):
        """Look at the children of this object and remove unnecessary items."""
        children = self.children
        if any((isinstance(a, self.__class__) and a.is_never()) for a in children):
            # We have at least one 'never' arg
            if self.conn_type == self.AND:
                # Remove all other args since nothing we AND together with a 'never' arg can change the result
                children = [self.__class__(conn_type=self.NEVER)]
            elif self.conn_type == self.OR:
                # Remove all 'never' args because all other args will decide the result. Keep one 'never' arg in case
                # all args are 'never' args.
                children = [a for a in children if not (isinstance(a, self.__class__) and a.is_never())]
                if not children:
                    children = [self.__class__(conn_type=self.NEVER)]
            elif self.conn_type == self.NOT:
                # Let's interpret 'not never' to mean 'always'. Remove all 'never' args
                children = [a for a in children if not (isinstance(a, self.__class__) and a.is_never())]

        # Remove any empty Q elements in args before proceeding
        children = [a for a in children if not (isinstance(a, self.__class__) and a.is_empty())]
        self.children = children

    def _promote(self):
        """When we only have one child and no expression on ourselves, we are a no-op. Flatten by taking over the only
        child.
        """
        if len(self.children) != 1 or self.field_path is not None or self.conn_type == self.NOT:
            return

        q = self.children[0]
        self.conn_type = q.conn_type
        self.field_path = q.field_path
        self.op = q.op
        self.value = q.value
        self.query_string = q.query_string
        self.children = q.children

    def clean(self, version):
        """Do some basic checks on the attributes, using a generic folder. to_xml() does a really good job of
        validating. There's no reason to replicate much of that here.
        """
        from .folders import Folder
        self.to_xml(folders=[Folder()], version=version, applies_to=Restriction.ITEMS)

    @classmethod
    def _lookup_to_op(cls, lookup):
        return {
            cls.LOOKUP_NOT: cls.NE,
            cls.LOOKUP_GT: cls.GT,
            cls.LOOKUP_GTE: cls.GTE,
            cls.LOOKUP_LT: cls.LT,
            cls.LOOKUP_LTE: cls.LTE,
            cls.LOOKUP_EXACT: cls.EXACT,
            cls.LOOKUP_IEXACT: cls.IEXACT,
            cls.LOOKUP_CONTAINS: cls.CONTAINS,
            cls.LOOKUP_ICONTAINS: cls.ICONTAINS,
            cls.LOOKUP_STARTSWITH: cls.STARTSWITH,
            cls.LOOKUP_ISTARTSWITH: cls.ISTARTSWITH,
            cls.LOOKUP_EXISTS: cls.EXISTS,
        }[lookup]

    @classmethod
    def _conn_to_xml(cls, conn_type):
        xml_tag_map = {
            cls.AND: 't:And',
            cls.OR: 't:Or',
            cls.NOT: 't:Not',
        }
        return create_element(xml_tag_map[conn_type])

    @classmethod
    def _op_to_xml(cls, op):
        xml_tag_map = {
            cls.EQ: 't:IsEqualTo',
            cls.NE: 't:IsNotEqualTo',
            cls.GTE: 't:IsGreaterThanOrEqualTo',
            cls.LTE: 't:IsLessThanOrEqualTo',
            cls.LT: 't:IsLessThan',
            cls.GT: 't:IsGreaterThan',
            cls.EXISTS: 't:Exists',
        }
        if op in xml_tag_map:
            return create_element(xml_tag_map[op])
        valid_ops = cls.EXACT, cls.IEXACT, cls.CONTAINS, cls.ICONTAINS, cls.STARTSWITH, cls.ISTARTSWITH
        if op not in valid_ops:
            raise ValueError("'op' %s must be one of %s" % (op, valid_ops))

        # For description of Contains attribute values, see
        #     https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/contains
        #
        # Possible ContainmentMode values:
        #     FullString, Prefixed, Substring, PrefixOnWords, ExactPhrase
        # Django lookups have no equivalent of PrefixOnWords and ExactPhrase (and I'm unsure how they actually
        # work).
        #
        # EWS has no equivalent of '__endswith' or '__iendswith'. That could be emulated using '__contains' and
        # '__icontains' and filtering results afterwards in Python. But it could be inefficient because we might be
        # fetching and discarding a lot of non-matching items, plus we would need to always fetch the field we're
        # matching on, to be able to do the filtering. I think it's better to leave this to the consumer, i.e.:
        #
        # items = [i for i in fld.filter(subject__contains=suffix) if i.subject.endswith(suffix)]
        # items = [i for i in fld.filter(subject__icontains=suffix) if i.subject.lower().endswith(suffix.lower())]
        #
        # Possible ContainmentComparison values (there are more, but the rest are "To be removed"):
        #     Exact, IgnoreCase, IgnoreNonSpacingCharacters, IgnoreCaseAndNonSpacingCharacters
        # I'm unsure about non-spacing characters, but as I read
        #    https://en.wikipedia.org/wiki/Graphic_character#Spacing_and_non-spacing_characters
        # we shouldn't ignore them ('a' would match both 'a' and 'å', the latter having a non-spacing character).
        if op in {cls.EXACT, cls.IEXACT}:
            match_mode = 'FullString'
        elif op in (cls.CONTAINS, cls.ICONTAINS):
            match_mode = 'Substring'
        elif op in (cls.STARTSWITH, cls.ISTARTSWITH):
            match_mode = 'Prefixed'
        else:
            raise ValueError('Unsupported op: %s' % op)
        if op in (cls.IEXACT, cls.ICONTAINS, cls.ISTARTSWITH):
            compare_mode = 'IgnoreCase'
        else:
            compare_mode = 'Exact'
        return create_element(
            't:Contains',
            attrs=OrderedDict([
                ('ContainmentMode', match_mode),
                ('ContainmentComparison', compare_mode),
            ])
        )

    def is_leaf(self):
        return not self.children

    def is_empty(self):
        """Return True if this object is without any restrictions at all."""
        return self.is_leaf() and self.field_path is None and self.query_string is None and self.conn_type != self.NEVER

    def is_never(self):
        """Return True if this object has a restriction that will never match anything."""
        return self.conn_type == self.NEVER

    def expr(self):
        if self.is_empty():
            return None
        if self.is_never():
            return self.NEVER
        if self.query_string:
            return self.query_string
        if self.is_leaf():
            expr = '%s %s %r' % (self.field_path, self.op, self.value)
        else:
            # Sort children by field name so we get stable output (for easier testing). Children should never be empty.
            expr = (' %s ' % (self.AND if self.conn_type == self.NOT else self.conn_type)).join(
                (c.expr() if c.is_leaf() or c.conn_type == self.NOT else '(%s)' % c.expr())
                for c in sorted(self.children, key=lambda i: i.field_path or '')
            )
        if self.conn_type == self.NOT:
            # Add the NOT operator. Put children in parens if there is more than one child.
            if self.is_leaf() or len(self.children) == 1:
                return self.conn_type + ' %s' % expr
            return self.conn_type + ' (%s)' % expr
        return expr

    def to_xml(self, folders, version, applies_to):
        if self.query_string:
            self._check_integrity()
            if version.build < EXCHANGE_2010:
                raise NotImplementedError('QueryString filtering is only supported for Exchange 2010 servers and later')
            elem = create_element('m:QueryString')
            elem.text = self.query_string
            return elem
        # Translate this Q object to a valid Restriction XML tree
        elem = self.xml_elem(folders=folders, version=version, applies_to=applies_to)
        if elem is None:
            return None
        restriction = create_element('m:Restriction')
        restriction.append(elem)
        return restriction

    def _check_integrity(self):
        if self.is_empty():
            return
        if self.conn_type == self.NEVER:
            if any([self.field_path, self.op, self.value, self.children]):
                raise ValueError("'never' queries cannot be combined with other settings")
            return
        if self.query_string:
            if any([self.field_path, self.op, self.value, self.children]):
                raise ValueError('Query strings cannot be combined with other settings')
            return
        if self.conn_type not in self.CONN_TYPES:
            raise ValueError("'conn_type' %s must be one of %s" % (self.conn_type, self.CONN_TYPES))
        if not self.is_leaf():
            for q in self.children:
                if q.query_string and len(self.children) > 1:
                    raise ValueError(
                        'A query string cannot be combined with other restrictions'
                    )
            return
        if not self.field_path:
            raise ValueError("'field_path' must be set")
        if self.op not in self.OP_TYPES:
            raise ValueError("'op' %s must be one of %s" % (self.op, self.OP_TYPES))
        if self.op == self.EXISTS and self.value is not True:
            raise ValueError("'value' must be True when operator is EXISTS")
        if self.value is None:
            raise ValueError('Value for filter on field path "%s" cannot be None' % self.field_path)
        if is_iterable(self.value, generators_allowed=True):
            raise ValueError(
                'Value %r for filter on field path "%s" must be a single value' % (self.value, self.field_path)
            )

    def _validate_field_path(self, field_path, folder, applies_to, version):
        from .indexed_properties import MultiFieldIndexedElement
        if applies_to == Restriction.FOLDERS:
            # This is a restriction on Folder fields
            folder.validate_field(field=field_path.field, version=version)
        else:
            folder.validate_item_field(field=field_path.field, version=version)
        if not field_path.field.is_searchable:
            raise ValueError("EWS does not support filtering on field '%s'" % field_path.field.name)
        if field_path.subfield and not field_path.subfield.is_searchable:
            raise ValueError("EWS does not support filtering on subfield '%s'" % field_path.subfield.name)
        if issubclass(field_path.field.value_cls, MultiFieldIndexedElement) and not field_path.subfield:
            raise ValueError("Field path '%s' must contain a subfield" % self.field_path)

    def _get_field_path(self, folders, applies_to, version):
        # Convert the string field path to a real FieldPath object. The path is validated using the given folders.
        for folder in folders:
            try:
                if applies_to == Restriction.FOLDERS:
                    # This is a restriction on Folder fields
                    field = folder.get_field_by_fieldname(fieldname=self.field_path)
                    field_path = FieldPath(field=field)
                else:
                    field_path = FieldPath.from_string(field_path=self.field_path, folder=folder)
            except ValueError:
                continue
            self._validate_field_path(field_path=field_path, folder=folder, applies_to=applies_to, version=version)
            break
        else:
            raise InvalidField("Unknown field path %r on folders %s" % (self.field_path, folders))
        return field_path

    def _get_clean_value(self, field_path, version):
        if self.op == self.EXISTS:
            return None
        clean_field = field_path.subfield if (field_path.subfield and field_path.label) else field_path.field
        if clean_field.is_list:
            # __contains and __in are implemented as multiple leaves, with one value per leaf. clean() on list fields
            # only works on lists, so clean a one-element list.
            return clean_field.clean(value=[self.value], version=version)[0]
        return clean_field.clean(value=self.value, version=version)

    def xml_elem(self, folders, version, applies_to):
        # Recursively build an XML tree structure of this Q object. If this is an empty leaf (the equivalent of Q()),
        # return None.
        from .indexed_properties import SingleFieldIndexedElement
        # Don't check self.value just yet. We want to return error messages on the field path first, and then the value.
        # This is done in _get_field_path() and _get_clean_value(), respectively.
        self._check_integrity()
        if self.is_empty():
            return None
        if self.is_never():
            raise ValueError("EWS does not support 'never' queries")
        if self.is_leaf():
            elem = self._op_to_xml(self.op)
            field_path = self._get_field_path(folders, applies_to=applies_to, version=version)
            clean_value = self._get_clean_value(field_path=field_path, version=version)
            if issubclass(field_path.field.value_cls, SingleFieldIndexedElement) and not field_path.label:
                # We allow a filter shortcut of e.g. email_addresses__contains=EmailAddress(label='Foo', ...) instead of
                # email_addresses__Foo_email_address=.... Set FieldPath label now so we can generate the field_uri.
                field_path.label = clean_value.label
            elif isinstance(field_path.field, DateTimeBackedDateField):
                # We need to convert to datetime
                clean_value = field_path.field.date_to_datetime(clean_value)
            elem.append(field_path.to_xml())
            constant = create_element('t:Constant')
            if self.op != self.EXISTS:
                # Use .set() to not fill up the create_element() cache with unique values
                constant.set('Value', value_to_xml_text(clean_value))
                if self.op in self.CONTAINS_OPS:
                    elem.append(constant)
                else:
                    uriorconst = create_element('t:FieldURIOrConstant')
                    uriorconst.append(constant)
                    elem.append(uriorconst)
        elif len(self.children) == 1:
            # We have only one child
            elem = self.children[0].xml_elem(folders=folders, version=version, applies_to=applies_to)
        else:
            # We have multiple children. If conn_type is NOT, then group children with AND. We'll add the NOT later
            elem = self._conn_to_xml(self.AND if self.conn_type == self.NOT else self.conn_type)
            # Sort children by field name so we get stable output (for easier testing). Children should never be empty
            for c in sorted(self.children, key=lambda i: i.field_path or ''):
                elem.append(c.xml_elem(folders=folders, version=version, applies_to=applies_to))
        if elem is None:
            return None  # Should not be necessary, but play safe
        if self.conn_type == self.NOT:
            # Encapsulate everything in the NOT element
            not_elem = self._conn_to_xml(self.conn_type)
            not_elem.append(elem)
            return not_elem
        return elem

    def __and__(self, other):
        # & operator. Return a new Q with two children and conn_type AND
        return self.__class__(self, other, conn_type=self.AND)

    def __or__(self, other):
        # | operator. Return a new Q with two children and conn_type OR
        return self.__class__(self, other, conn_type=self.OR)

    def __invert__(self):
        # ~ operator. If op has an inverse, change op. Else return a new Q with conn_type NOT
        if self.conn_type == self.NOT:
            # This is NOT NOT. Change to AND
            new = copy(self)
            new.conn_type = self.AND
            new.reduce()
            return new
        if self.is_leaf():
            inverse_ops = {
                self.EQ: self.NE,
                self.NE: self.EQ,
                self.GT: self.LTE,
                self.GTE: self.LT,
                self.LT: self.GTE,
                self.LTE: self.GT,
            }
            try:
                new = copy(self)
                new.op = inverse_ops[self.op]
                new.reduce()
                return new
            except KeyError:
                pass
        return self.__class__(self, conn_type=self.NOT)

    def __eq__(self, other):
        return repr(self) == repr(other)

    def __hash__(self):
        return hash(repr(self))

    def __str__(self):
        return self.expr() or 'Q()'

    def __repr__(self):
        if self.is_leaf():
            if self.query_string:
                return self.__class__.__name__ + '(%r)' % self.query_string
            if self.is_never():
                return self.__class__.__name__ + '(conn_type=%r)' % (self.conn_type)
            return self.__class__.__name__ + '(%s %s %r)' % (self.field_path, self.op, self.value)
        sorted_children = tuple(sorted(self.children, key=lambda i: i.field_path or ''))
        if self.conn_type == self.NOT or len(self.children) > 1:
            return self.__class__.__name__ + repr((self.conn_type,) + sorted_children)
        return self.__class__.__name__ + repr(sorted_children)

Class variables

var AND
var CONN_TYPES
var CONTAINS
var CONTAINS_OPS
var EQ
var EXACT
var EXISTS
var GT
var GTE
var ICONTAINS
var IEXACT
var ISTARTSWITH
var LOOKUP_CONTAINS
var LOOKUP_EXACT
var LOOKUP_EXISTS
var LOOKUP_GT
var LOOKUP_GTE
var LOOKUP_ICONTAINS
var LOOKUP_IEXACT
var LOOKUP_IN
var LOOKUP_ISTARTSWITH
var LOOKUP_LT
var LOOKUP_LTE
var LOOKUP_NOT
var LOOKUP_RANGE
var LOOKUP_STARTSWITH
var LOOKUP_TYPES
var LT
var LTE
var NE
var NEVER
var NOT
var OP_TYPES
var OR
var STARTSWITH

Instance variables

var children

Return an attribute of instance, which is of type owner.

var conn_type

Return an attribute of instance, which is of type owner.

var field_path

Return an attribute of instance, which is of type owner.

var op

Return an attribute of instance, which is of type owner.

var query_string

Return an attribute of instance, which is of type owner.

var value

Return an attribute of instance, which is of type owner.

Methods

def clean(self, version)

Do some basic checks on the attributes, using a generic folder. to_xml() does a really good job of validating. There's no reason to replicate much of that here.

Expand source code
def clean(self, version):
    """Do some basic checks on the attributes, using a generic folder. to_xml() does a really good job of
    validating. There's no reason to replicate much of that here.
    """
    from .folders import Folder
    self.to_xml(folders=[Folder()], version=version, applies_to=Restriction.ITEMS)
def expr(self)
Expand source code
def expr(self):
    if self.is_empty():
        return None
    if self.is_never():
        return self.NEVER
    if self.query_string:
        return self.query_string
    if self.is_leaf():
        expr = '%s %s %r' % (self.field_path, self.op, self.value)
    else:
        # Sort children by field name so we get stable output (for easier testing). Children should never be empty.
        expr = (' %s ' % (self.AND if self.conn_type == self.NOT else self.conn_type)).join(
            (c.expr() if c.is_leaf() or c.conn_type == self.NOT else '(%s)' % c.expr())
            for c in sorted(self.children, key=lambda i: i.field_path or '')
        )
    if self.conn_type == self.NOT:
        # Add the NOT operator. Put children in parens if there is more than one child.
        if self.is_leaf() or len(self.children) == 1:
            return self.conn_type + ' %s' % expr
        return self.conn_type + ' (%s)' % expr
    return expr
def is_empty(self)

Return True if this object is without any restrictions at all.

Expand source code
def is_empty(self):
    """Return True if this object is without any restrictions at all."""
    return self.is_leaf() and self.field_path is None and self.query_string is None and self.conn_type != self.NEVER
def is_leaf(self)
Expand source code
def is_leaf(self):
    return not self.children
def is_never(self)

Return True if this object has a restriction that will never match anything.

Expand source code
def is_never(self):
    """Return True if this object has a restriction that will never match anything."""
    return self.conn_type == self.NEVER
def reduce(self)

Simplify this object, if possible.

Expand source code
def reduce(self):
    """Simplify this object, if possible."""
    self._reduce_children()
    self._promote()
def to_xml(self, folders, version, applies_to)
Expand source code
def to_xml(self, folders, version, applies_to):
    if self.query_string:
        self._check_integrity()
        if version.build < EXCHANGE_2010:
            raise NotImplementedError('QueryString filtering is only supported for Exchange 2010 servers and later')
        elem = create_element('m:QueryString')
        elem.text = self.query_string
        return elem
    # Translate this Q object to a valid Restriction XML tree
    elem = self.xml_elem(folders=folders, version=version, applies_to=applies_to)
    if elem is None:
        return None
    restriction = create_element('m:Restriction')
    restriction.append(elem)
    return restriction
def xml_elem(self, folders, version, applies_to)
Expand source code
def xml_elem(self, folders, version, applies_to):
    # Recursively build an XML tree structure of this Q object. If this is an empty leaf (the equivalent of Q()),
    # return None.
    from .indexed_properties import SingleFieldIndexedElement
    # Don't check self.value just yet. We want to return error messages on the field path first, and then the value.
    # This is done in _get_field_path() and _get_clean_value(), respectively.
    self._check_integrity()
    if self.is_empty():
        return None
    if self.is_never():
        raise ValueError("EWS does not support 'never' queries")
    if self.is_leaf():
        elem = self._op_to_xml(self.op)
        field_path = self._get_field_path(folders, applies_to=applies_to, version=version)
        clean_value = self._get_clean_value(field_path=field_path, version=version)
        if issubclass(field_path.field.value_cls, SingleFieldIndexedElement) and not field_path.label:
            # We allow a filter shortcut of e.g. email_addresses__contains=EmailAddress(label='Foo', ...) instead of
            # email_addresses__Foo_email_address=.... Set FieldPath label now so we can generate the field_uri.
            field_path.label = clean_value.label
        elif isinstance(field_path.field, DateTimeBackedDateField):
            # We need to convert to datetime
            clean_value = field_path.field.date_to_datetime(clean_value)
        elem.append(field_path.to_xml())
        constant = create_element('t:Constant')
        if self.op != self.EXISTS:
            # Use .set() to not fill up the create_element() cache with unique values
            constant.set('Value', value_to_xml_text(clean_value))
            if self.op in self.CONTAINS_OPS:
                elem.append(constant)
            else:
                uriorconst = create_element('t:FieldURIOrConstant')
                uriorconst.append(constant)
                elem.append(uriorconst)
    elif len(self.children) == 1:
        # We have only one child
        elem = self.children[0].xml_elem(folders=folders, version=version, applies_to=applies_to)
    else:
        # We have multiple children. If conn_type is NOT, then group children with AND. We'll add the NOT later
        elem = self._conn_to_xml(self.AND if self.conn_type == self.NOT else self.conn_type)
        # Sort children by field name so we get stable output (for easier testing). Children should never be empty
        for c in sorted(self.children, key=lambda i: i.field_path or ''):
            elem.append(c.xml_elem(folders=folders, version=version, applies_to=applies_to))
    if elem is None:
        return None  # Should not be necessary, but play safe
    if self.conn_type == self.NOT:
        # Encapsulate everything in the NOT element
        not_elem = self._conn_to_xml(self.conn_type)
        not_elem.append(elem)
        return not_elem
    return elem
class ReplyAllToItem (**kwargs)
Expand source code
class ReplyAllToItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/replyalltoitem"""

    ELEMENT_NAME = 'ReplyAllToItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class ReplyToItem (**kwargs)
Expand source code
class ReplyToItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/replytoitem"""

    ELEMENT_NAME = 'ReplyToItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class Room (**kwargs)
Expand source code
class Room(Mailbox):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/room"""

    ELEMENT_NAME = 'Room'

    @classmethod
    def from_xml(cls, elem, account):
        id_elem = elem.find('{%s}Id' % TNS)
        item_id_elem = id_elem.find(ItemId.response_tag())
        kwargs = dict(
            name=get_xml_attr(id_elem, '{%s}Name' % TNS),
            email_address=get_xml_attr(id_elem, '{%s}EmailAddress' % TNS),
            mailbox_type=get_xml_attr(id_elem, '{%s}MailboxType' % TNS),
            item_id=ItemId.from_xml(elem=item_id_elem, account=account) if item_id_elem else None,
        )
        cls._clear(elem)
        return cls(**kwargs)

Ancestors

Class variables

var ELEMENT_NAME

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    id_elem = elem.find('{%s}Id' % TNS)
    item_id_elem = id_elem.find(ItemId.response_tag())
    kwargs = dict(
        name=get_xml_attr(id_elem, '{%s}Name' % TNS),
        email_address=get_xml_attr(id_elem, '{%s}EmailAddress' % TNS),
        mailbox_type=get_xml_attr(id_elem, '{%s}MailboxType' % TNS),
        item_id=ItemId.from_xml(elem=item_id_elem, account=account) if item_id_elem else None,
    )
    cls._clear(elem)
    return cls(**kwargs)

Inherited members

class RoomList (**kwargs)
Expand source code
class RoomList(Mailbox):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/roomlist"""

    ELEMENT_NAME = 'RoomList'
    NAMESPACE = MNS

    @classmethod
    def response_tag(cls):
        # In a GetRoomLists response, room lists are delivered as Address elements. See
        # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/address-emailaddresstype
        return '{%s}Address' % TNS

Ancestors

Class variables

var ELEMENT_NAME
var NAMESPACE

Static methods

def response_tag()
Expand source code
@classmethod
def response_tag(cls):
    # In a GetRoomLists response, room lists are delivered as Address elements. See
    # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/address-emailaddresstype
    return '{%s}Address' % TNS

Inherited members

class RootOfHierarchy (**kwargs)

Base class for folders that implement the root of a folder hierarchy.

Expand source code
class RootOfHierarchy(BaseFolder, metaclass=EWSMeta):
    """Base class for folders that implement the root of a folder hierarchy."""

    # A list of wellknown, or "distinguished", folders that are belong in this folder hierarchy. See
    # https://docs.microsoft.com/en-us/dotnet/api/microsoft.exchange.webservices.data.wellknownfoldername
    # and https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid
    # 'RootOfHierarchy' subclasses must not be in this list.
    WELLKNOWN_FOLDERS = []

    _subfolders_lock = Lock()

    # This folder type also has 'folder:PermissionSet' on some server versions, but requesting it sometimes causes
    # 'ErrorAccessDenied', as reported by some users. Ignore it entirely for root folders - it's usefulness is
    # deemed minimal at best.
    effective_rights = EffectiveRightsField(field_uri='folder:EffectiveRights', is_read_only=True,
                                            supported_from=EXCHANGE_2007_SP1)

    __slots__ = '_account', '_subfolders'

    # A special folder that acts as the top of a folder hierarchy. Finds and caches subfolders at arbitrary depth.
    def __init__(self, **kwargs):
        self._account = kwargs.pop('account', None)  # A pointer back to the account holding the folder hierarchy
        super().__init__(**kwargs)
        self._subfolders = None  # See self._folders_map()

    @property
    def account(self):
        return self._account

    @property
    def root(self):
        return self

    @property
    def parent(self):
        return None

    @classmethod
    def register(cls, *args, **kwargs):
        if cls is not RootOfHierarchy:
            raise TypeError('For folder roots, custom fields must be registered on the RootOfHierarchy class')
        return super().register(*args, **kwargs)

    @classmethod
    def deregister(cls, *args, **kwargs):
        if cls is not RootOfHierarchy:
            raise TypeError('For folder roots, custom fields must be registered on the RootOfHierarchy class')
        return super().deregister(*args, **kwargs)

    def get_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        return self._folders_map.get(folder.id, None)

    def add_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        self._folders_map[folder.id] = folder

    def update_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        self._folders_map[folder.id] = folder

    def remove_folder(self, folder):
        if not folder.id:
            raise ValueError("'folder' must have an ID")
        try:
            del self._folders_map[folder.id]
        except KeyError:
            pass

    def clear_cache(self):
        with self._subfolders_lock:
            self._subfolders = None

    def get_children(self, folder):
        for f in self._folders_map.values():
            if not f.parent:
                continue
            if f.parent.id == folder.id:
                yield f

    @classmethod
    def get_distinguished(cls, account):
        """Get the distinguished folder for this folder class.

        :param account:
        """
        if not cls.DISTINGUISHED_FOLDER_ID:
            raise ValueError('Class %s must have a DISTINGUISHED_FOLDER_ID value' % cls)
        try:
            return cls.resolve(
                account=account,
                folder=cls(account=account, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
            )
        except MISSING_FOLDER_ERRORS:
            raise ErrorFolderNotFound('Could not find distinguished folder %s' % cls.DISTINGUISHED_FOLDER_ID)

    def get_default_folder(self, folder_cls):
        """Return the distinguished folder instance of type folder_cls belonging to this account. If no distinguished
        folder was found, try as best we can to return the default folder of type 'folder_cls'
        """
        if not folder_cls.DISTINGUISHED_FOLDER_ID:
            raise ValueError("'folder_cls' %s must have a DISTINGUISHED_FOLDER_ID value" % folder_cls)
        # Use cached distinguished folder instance, but only if cache has already been prepped. This is an optimization
        # for accessing e.g. 'account.contacts' without fetching all folders of the account.
        if self._subfolders is not None:
            for f in self._folders_map.values():
                # Require exact class, to not match subclasses, e.g. RecipientCache instead of Contacts
                if f.__class__ == folder_cls and f.is_distinguished:
                    log.debug('Found cached distinguished %s folder', folder_cls)
                    return f
        try:
            log.debug('Requesting distinguished %s folder explicitly', folder_cls)
            return folder_cls.get_distinguished(root=self)
        except ErrorAccessDenied:
            # Maybe we just don't have GetFolder access? Try FindItems instead
            log.debug('Testing default %s folder with FindItem', folder_cls)
            fld = folder_cls(root=self, name=folder_cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
            fld.test_access()
            return self._folders_map.get(fld.id, fld)  # Use cached instance if available
        except MISSING_FOLDER_ERRORS:
            # The Exchange server does not return a distinguished folder of this type
            pass
        raise ErrorFolderNotFound('No usable default %s folders' % folder_cls)

    @property
    def _folders_map(self):
        if self._subfolders is not None:
            return self._subfolders

        with self._subfolders_lock:
            # Map root, and all subfolders of root, at arbitrary depth by folder ID. First get distinguished folders,
            # so we are sure to apply the correct Folder class, then fetch all subfolders of this root.
            folders_map = {self.id: self}
            distinguished_folders = [
                cls(root=self, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
                for cls in self.WELLKNOWN_FOLDERS
                if cls.get_folder_allowed and cls.supports_version(self.account.version)
            ]
            for f in FolderCollection(account=self.account, folders=distinguished_folders).resolve():
                if isinstance(f, MISSING_FOLDER_ERRORS):
                    # This is just a distinguished folder the server does not have
                    continue
                if isinstance(f, ErrorInvalidOperation):
                    # This is probably a distinguished folder the server does not have. We previously tested the exact
                    # error message (f.value), but some Exchange servers return localized error messages, so that's not
                    # possible to do reliably.
                    continue
                if isinstance(f, ErrorAccessDenied):
                    # We may not have GetFolder access, either to this folder or at all
                    continue
                if isinstance(f, Exception):
                    raise f
                folders_map[f.id] = f
            for f in SingleFolderQuerySet(account=self.account, folder=self).depth(
                    self.DEFAULT_FOLDER_TRAVERSAL_DEPTH
            ).all():
                if isinstance(f, ErrorAccessDenied):
                    # We may not have FindFolder access, or GetFolder access, either to this folder or at all
                    continue
                if isinstance(f, Exception):
                    raise f
                if f.id in folders_map:
                    # Already exists. Probably a distinguished folder
                    continue
                folders_map[f.id] = f
            self._subfolders = folders_map
            return folders_map

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = cls._kwargs_from_elem(elem=elem, account=account)
        cls._clear(elem)
        return cls(account=account, **kwargs)

    @classmethod
    def folder_cls_from_folder_name(cls, folder_name, locale):
        """Return the folder class that matches a localized folder name.

        :param folder_name:
        :param locale: a string, e.g. 'da_DK'
        """
        for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS:
            if folder_name.lower() in folder_cls.localized_names(locale):
                return folder_cls
        raise KeyError()

    def __repr__(self):
        # Let's not create an infinite loop when printing self.root
        return self.__class__.__name__ + \
               repr((self.account, '[self]', self.name, self.total_count, self.unread_count, self.child_folder_count,
                     self.folder_class, self.id, self.changekey))

Ancestors

Subclasses

Class variables

var FIELDS
var WELLKNOWN_FOLDERS

Static methods

def folder_cls_from_folder_name(folder_name, locale)

Return the folder class that matches a localized folder name.

:param folder_name: :param locale: a string, e.g. 'da_DK'

Expand source code
@classmethod
def folder_cls_from_folder_name(cls, folder_name, locale):
    """Return the folder class that matches a localized folder name.

    :param folder_name:
    :param locale: a string, e.g. 'da_DK'
    """
    for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS:
        if folder_name.lower() in folder_cls.localized_names(locale):
            return folder_cls
    raise KeyError()
def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    kwargs = cls._kwargs_from_elem(elem=elem, account=account)
    cls._clear(elem)
    return cls(account=account, **kwargs)
def get_distinguished(account)

Get the distinguished folder for this folder class.

:param account:

Expand source code
@classmethod
def get_distinguished(cls, account):
    """Get the distinguished folder for this folder class.

    :param account:
    """
    if not cls.DISTINGUISHED_FOLDER_ID:
        raise ValueError('Class %s must have a DISTINGUISHED_FOLDER_ID value' % cls)
    try:
        return cls.resolve(
            account=account,
            folder=cls(account=account, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
        )
    except MISSING_FOLDER_ERRORS:
        raise ErrorFolderNotFound('Could not find distinguished folder %s' % cls.DISTINGUISHED_FOLDER_ID)

Instance variables

var account
Expand source code
@property
def account(self):
    return self._account
var effective_rights
var parent
Expand source code
@property
def parent(self):
    return None
var root
Expand source code
@property
def root(self):
    return self

Methods

def add_folder(self, folder)
Expand source code
def add_folder(self, folder):
    if not folder.id:
        raise ValueError("'folder' must have an ID")
    self._folders_map[folder.id] = folder
def clear_cache(self)
Expand source code
def clear_cache(self):
    with self._subfolders_lock:
        self._subfolders = None
def get_children(self, folder)
Expand source code
def get_children(self, folder):
    for f in self._folders_map.values():
        if not f.parent:
            continue
        if f.parent.id == folder.id:
            yield f
def get_default_folder(self, folder_cls)

Return the distinguished folder instance of type folder_cls belonging to this account. If no distinguished folder was found, try as best we can to return the default folder of type 'folder_cls'

Expand source code
def get_default_folder(self, folder_cls):
    """Return the distinguished folder instance of type folder_cls belonging to this account. If no distinguished
    folder was found, try as best we can to return the default folder of type 'folder_cls'
    """
    if not folder_cls.DISTINGUISHED_FOLDER_ID:
        raise ValueError("'folder_cls' %s must have a DISTINGUISHED_FOLDER_ID value" % folder_cls)
    # Use cached distinguished folder instance, but only if cache has already been prepped. This is an optimization
    # for accessing e.g. 'account.contacts' without fetching all folders of the account.
    if self._subfolders is not None:
        for f in self._folders_map.values():
            # Require exact class, to not match subclasses, e.g. RecipientCache instead of Contacts
            if f.__class__ == folder_cls and f.is_distinguished:
                log.debug('Found cached distinguished %s folder', folder_cls)
                return f
    try:
        log.debug('Requesting distinguished %s folder explicitly', folder_cls)
        return folder_cls.get_distinguished(root=self)
    except ErrorAccessDenied:
        # Maybe we just don't have GetFolder access? Try FindItems instead
        log.debug('Testing default %s folder with FindItem', folder_cls)
        fld = folder_cls(root=self, name=folder_cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
        fld.test_access()
        return self._folders_map.get(fld.id, fld)  # Use cached instance if available
    except MISSING_FOLDER_ERRORS:
        # The Exchange server does not return a distinguished folder of this type
        pass
    raise ErrorFolderNotFound('No usable default %s folders' % folder_cls)
def get_folder(self, folder)
Expand source code
def get_folder(self, folder):
    if not folder.id:
        raise ValueError("'folder' must have an ID")
    return self._folders_map.get(folder.id, None)
def remove_folder(self, folder)
Expand source code
def remove_folder(self, folder):
    if not folder.id:
        raise ValueError("'folder' must have an ID")
    try:
        del self._folders_map[folder.id]
    except KeyError:
        pass
def update_folder(self, folder)
Expand source code
def update_folder(self, folder):
    if not folder.id:
        raise ValueError("'folder' must have an ID")
    self._folders_map[folder.id] = folder

Inherited members

class TLSClientAuth (pool_connections=10, pool_maxsize=10, max_retries=0, pool_block=False)

An HTTP adapter that implements Certificate Based Authentication (CBA).

Expand source code
class TLSClientAuth(requests.adapters.HTTPAdapter):
    """An HTTP adapter that implements Certificate Based Authentication (CBA)."""

    cert_file = None

    def init_poolmanager(self, *args, **kwargs):
        kwargs['cert_file'] = self.cert_file
        return super().init_poolmanager(*args, **kwargs)

Ancestors

  • requests.adapters.HTTPAdapter
  • requests.adapters.BaseAdapter

Class variables

var cert_file

Methods

def init_poolmanager(self, *args, **kwargs)

Initializes a urllib3 PoolManager.

This method should not be called from user code, and is only exposed for use when subclassing the :class:HTTPAdapter <requests.adapters.HTTPAdapter>.

:param connections: The number of urllib3 connection pools to cache. :param maxsize: The maximum number of connections to save in the pool. :param block: Block when no free connections are available. :param pool_kwargs: Extra keyword arguments used to initialize the Pool Manager.

Expand source code
def init_poolmanager(self, *args, **kwargs):
    kwargs['cert_file'] = self.cert_file
    return super().init_poolmanager(*args, **kwargs)
class Task (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/task

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class Task(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/task"""

    ELEMENT_NAME = 'Task'
    NOT_STARTED = 'NotStarted'
    COMPLETED = 'Completed'

    actual_work = IntegerField(field_uri='task:ActualWork', min=0)
    assigned_time = DateTimeField(field_uri='task:AssignedTime', is_read_only=True)
    billing_information = TextField(field_uri='task:BillingInformation')
    change_count = IntegerField(field_uri='task:ChangeCount', is_read_only=True, min=0)
    companies = TextListField(field_uri='task:Companies')
    # 'complete_date' can be set, but is ignored by the server, which sets it to now()
    complete_date = DateTimeField(field_uri='task:CompleteDate', is_read_only=True)
    contacts = TextListField(field_uri='task:Contacts')
    delegation_state = ChoiceField(field_uri='task:DelegationState', choices={
        Choice('NoMatch'), Choice('OwnNew'), Choice('Owned'), Choice('Accepted'), Choice('Declined'), Choice('Max')
    }, is_read_only=True)
    delegator = CharField(field_uri='task:Delegator', is_read_only=True)
    due_date = DateTimeBackedDateField(field_uri='task:DueDate')
    is_editable = BooleanField(field_uri='task:IsAssignmentEditable', is_read_only=True)
    is_complete = BooleanField(field_uri='task:IsComplete', is_read_only=True)
    is_recurring = BooleanField(field_uri='task:IsRecurring', is_read_only=True)
    is_team_task = BooleanField(field_uri='task:IsTeamTask', is_read_only=True)
    mileage = TextField(field_uri='task:Mileage')
    owner = CharField(field_uri='task:Owner', is_read_only=True)
    percent_complete = DecimalField(field_uri='task:PercentComplete', is_required=True, default=Decimal(0.0),
                                    min=Decimal(0), max=Decimal(100), is_searchable=False)
    recurrence = TaskRecurrenceField(field_uri='task:Recurrence', is_searchable=False)
    start_date = DateTimeBackedDateField(field_uri='task:StartDate')
    status = ChoiceField(field_uri='task:Status', choices={
        Choice(NOT_STARTED), Choice('InProgress'), Choice(COMPLETED), Choice('WaitingOnOthers'), Choice('Deferred')
    }, is_required=True, is_searchable=False, default=NOT_STARTED)
    status_description = CharField(field_uri='task:StatusDescription', is_read_only=True)
    total_work = IntegerField(field_uri='task:TotalWork', min=0)

    def clean(self, version=None):
        super().clean(version=version)
        if self.due_date and self.start_date and self.due_date < self.start_date:
            log.warning("'due_date' must be greater than 'start_date' (%s vs %s). Resetting 'due_date'",
                        self.due_date, self.start_date)
            self.due_date = self.start_date
        if self.complete_date:
            if self.status != self.COMPLETED:
                log.warning("'status' must be '%s' when 'complete_date' is set (%s). Resetting",
                            self.COMPLETED, self.status)
                self.status = self.COMPLETED
            now = datetime.datetime.now(tz=UTC)
            if (self.complete_date - now).total_seconds() > 120:
                # Reset complete_date values that are in the future
                # 'complete_date' can be set automatically by the server. Allow some grace between local and server time
                log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now)
                self.complete_date = now
            if self.start_date and self.complete_date.date() < self.start_date:
                log.warning("'complete_date' must be greater than 'start_date' (%s vs %s). Resetting",
                            self.complete_date, self.start_date)
                self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC)
        if self.percent_complete is not None:
            if self.status == self.COMPLETED and self.percent_complete != Decimal(100):
                # percent_complete must be 100% if task is complete
                log.warning("'percent_complete' must be 100 when 'status' is '%s' (%s). Resetting",
                            self.COMPLETED, self.percent_complete)
                self.percent_complete = Decimal(100)
            elif self.status == self.NOT_STARTED and self.percent_complete != Decimal(0):
                # percent_complete must be 0% if task is not started
                log.warning("'percent_complete' must be 0 when 'status' is '%s' (%s). Resetting",
                            self.NOT_STARTED, self.percent_complete)
                self.percent_complete = Decimal(0)

    def complete(self):
        # A helper method to mark a task as complete on the server
        self.status = Task.COMPLETED
        self.percent_complete = Decimal(100)
        self.save()

Ancestors

Class variables

var COMPLETED
var ELEMENT_NAME
var FIELDS
var NOT_STARTED

Instance variables

var actual_work
var assigned_time
var billing_information
var change_count
var companies
var complete_date
var contacts
var delegation_state
var delegator
var due_date
var is_complete
var is_editable
var is_recurring
var is_team_task
var mileage
var owner
var percent_complete
var recurrence
var start_date
var status
var status_description
var total_work

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)
    if self.due_date and self.start_date and self.due_date < self.start_date:
        log.warning("'due_date' must be greater than 'start_date' (%s vs %s). Resetting 'due_date'",
                    self.due_date, self.start_date)
        self.due_date = self.start_date
    if self.complete_date:
        if self.status != self.COMPLETED:
            log.warning("'status' must be '%s' when 'complete_date' is set (%s). Resetting",
                        self.COMPLETED, self.status)
            self.status = self.COMPLETED
        now = datetime.datetime.now(tz=UTC)
        if (self.complete_date - now).total_seconds() > 120:
            # Reset complete_date values that are in the future
            # 'complete_date' can be set automatically by the server. Allow some grace between local and server time
            log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now)
            self.complete_date = now
        if self.start_date and self.complete_date.date() < self.start_date:
            log.warning("'complete_date' must be greater than 'start_date' (%s vs %s). Resetting",
                        self.complete_date, self.start_date)
            self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC)
    if self.percent_complete is not None:
        if self.status == self.COMPLETED and self.percent_complete != Decimal(100):
            # percent_complete must be 100% if task is complete
            log.warning("'percent_complete' must be 100 when 'status' is '%s' (%s). Resetting",
                        self.COMPLETED, self.percent_complete)
            self.percent_complete = Decimal(100)
        elif self.status == self.NOT_STARTED and self.percent_complete != Decimal(0):
            # percent_complete must be 0% if task is not started
            log.warning("'percent_complete' must be 0 when 'status' is '%s' (%s). Resetting",
                        self.NOT_STARTED, self.percent_complete)
            self.percent_complete = Decimal(0)
def complete(self)
Expand source code
def complete(self):
    # A helper method to mark a task as complete on the server
    self.status = Task.COMPLETED
    self.percent_complete = Decimal(100)
    self.save()

Inherited members

class TentativelyAcceptItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/tentativelyacceptitem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class TentativelyAcceptItem(BaseMeetingReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/tentativelyacceptitem"""

    ELEMENT_NAME = 'TentativelyAcceptItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class UID (uid)

Helper class to encode Calendar UIDs. See issue #453. Example:

class GlobalObjectId(ExtendedProperty): distinguished_property_set_id = 'Meeting' property_id = 3 property_type = 'Binary'

CalendarItem.register('global_object_id', GlobalObjectId) account.calendar.filter(global_object_id=UID('261cbc18-1f65-5a0a-bd11-23b1e224cc2f'))

Expand source code
class UID(bytes):
    """Helper class to encode Calendar UIDs. See issue #453. Example:

    class GlobalObjectId(ExtendedProperty):
        distinguished_property_set_id = 'Meeting'
        property_id = 3
        property_type = 'Binary'

    CalendarItem.register('global_object_id', GlobalObjectId)
    account.calendar.filter(global_object_id=UID('261cbc18-1f65-5a0a-bd11-23b1e224cc2f'))
    """

    _HEADER = binascii.hexlify(bytearray((
        0x04, 0x00, 0x00, 0x00,
        0x82, 0x00, 0xE0, 0x00,
        0x74, 0xC5, 0xB7, 0x10,
        0x1A, 0x82, 0xE0, 0x08)))

    _EXCEPTION_REPLACEMENT_TIME = binascii.hexlify(bytearray((
        0, 0, 0, 0)))

    _CREATION_TIME = binascii.hexlify(bytearray((
        0, 0, 0, 0,
        0, 0, 0, 0)))

    _RESERVED = binascii.hexlify(bytearray((
        0, 0, 0, 0,
        0, 0, 0, 0)))

    # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxocal/1d3aac05-a7b9-45cc-a213-47f0a0a2c5c1
    # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-asemail/e7424ddc-dd10-431e-a0b7-5c794863370e
    # https://stackoverflow.com/questions/42259122
    # https://stackoverflow.com/questions/33757805

    def __new__(cls, uid):
        payload = binascii.hexlify(bytearray('vCal-Uid\x01\x00\x00\x00{}\x00'.format(uid).encode('ascii')))
        length = binascii.hexlify(bytearray(struct.pack('<I', int(len(payload)/2))))
        encoding = b''.join([
            cls._HEADER, cls._EXCEPTION_REPLACEMENT_TIME, cls._CREATION_TIME, cls._RESERVED, length, payload
        ])
        return super().__new__(cls, codecs.decode(encoding, 'hex'))

    @classmethod
    def to_global_object_id(cls, uid):
        """Converts a UID as returned by EWS to GlobalObjectId format"""
        return binascii.unhexlify(uid)

Ancestors

  • builtins.bytes

Static methods

def to_global_object_id(uid)

Converts a UID as returned by EWS to GlobalObjectId format

Expand source code
@classmethod
def to_global_object_id(cls, uid):
    """Converts a UID as returned by EWS to GlobalObjectId format"""
    return binascii.unhexlify(uid)
class Version (build, api_version=None)

Holds information about the server version.

Expand source code
class Version:
    """Holds information about the server version."""

    __slots__ = 'build', 'api_version'

    def __init__(self, build, api_version=None):
        if not isinstance(build, (Build, type(None))):
            raise ValueError("'build' must be a Build instance")
        self.build = build
        if api_version is None:
            self.api_version = build.api_version()
        else:
            if not isinstance(api_version, str):
                raise ValueError("'api_version' must be a string")
            self.api_version = api_version

    @property
    def fullname(self):
        return VERSIONS[self.api_version][1]

    @classmethod
    def guess(cls, protocol, api_version_hint=None):
        """Ask the server which version it has. We haven't set up an Account object yet, so we generate requests
        by hand. We only need a response header containing a ServerVersionInfo element.

        To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that
        without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this
        package supports, until we get a valid response.

        :param protocol:
        :param api_version_hint:  (Default value = None)
        """
        from .services import ResolveNames
        # The protocol doesn't have a version yet, so default to latest supported version if we don't have a hint.
        api_version = api_version_hint or API_VERSIONS[0]
        log.debug('Asking server for version info using API version %s', api_version)
        # We don't know the build version yet. Hopefully, the server will report it in the SOAP header. Lots of
        # places expect a version to have a build, so this is a bit dangerous, but passing a fake build around is also
        # dangerous. Make sure the call to ResolveNames does not require a version build.
        protocol.config.version = Version(build=None, api_version=api_version)
        # Use ResolveNames as a minimal request to the server to test if the version is correct. If not, ResolveNames
        # will try to guess the version automatically.
        name = str(protocol.credentials) if protocol.credentials and str(protocol.credentials) else 'DUMMY'
        try:
            list(ResolveNames(protocol=protocol).call(unresolved_entries=[name]))
        except ResponseMessageError as e:
            # We may have survived long enough to get a new version
            if not protocol.config.version.build:
                raise TransportError('No valid version headers found in response (%r)' % e)
        if not protocol.config.version.build:
            raise TransportError('No valid version headers found in response')
        return protocol.version

    @staticmethod
    def _is_invalid_version_string(version):
        # Check if a version string is bogus, e.g. V2_, V2015_ or V2018_
        return re.match(r'V[0-9]{1,4}_.*', version)

    @classmethod
    def from_soap_header(cls, requested_api_version, header):
        info = header.find('{%s}ServerVersionInfo' % TNS)
        if info is None:
            raise TransportError('No ServerVersionInfo in header: %r' % xml_to_str(header))
        try:
            build = Build.from_xml(elem=info)
        except ValueError:
            raise TransportError('Bad ServerVersionInfo in response: %r' % xml_to_str(header))
        # Not all Exchange servers send the Version element
        api_version_from_server = info.get('Version') or build.api_version()
        if api_version_from_server != requested_api_version:
            if cls._is_invalid_version_string(api_version_from_server):
                # For unknown reasons, Office 365 may respond with an API version strings that is invalid in a request.
                # Detect these so we can fallback to a valid version string.
                log.debug('API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version,
                          api_version_from_server, requested_api_version)
                api_version_from_server = requested_api_version
            else:
                # Trust API version from server response
                log.debug('API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version,
                          api_version_from_server, api_version_from_server)
        return cls(build=build, api_version=api_version_from_server)

    def __eq__(self, other):
        if self.api_version != other.api_version:
            return False
        if self.build and not other.build:
            return False
        if other.build and not self.build:
            return False
        return self.build == other.build

    def __repr__(self):
        return self.__class__.__name__ + repr((self.build, self.api_version))

    def __str__(self):
        return 'Build=%s, API=%s, Fullname=%s' % (self.build, self.api_version, self.fullname)

Static methods

def from_soap_header(requested_api_version, header)
Expand source code
@classmethod
def from_soap_header(cls, requested_api_version, header):
    info = header.find('{%s}ServerVersionInfo' % TNS)
    if info is None:
        raise TransportError('No ServerVersionInfo in header: %r' % xml_to_str(header))
    try:
        build = Build.from_xml(elem=info)
    except ValueError:
        raise TransportError('Bad ServerVersionInfo in response: %r' % xml_to_str(header))
    # Not all Exchange servers send the Version element
    api_version_from_server = info.get('Version') or build.api_version()
    if api_version_from_server != requested_api_version:
        if cls._is_invalid_version_string(api_version_from_server):
            # For unknown reasons, Office 365 may respond with an API version strings that is invalid in a request.
            # Detect these so we can fallback to a valid version string.
            log.debug('API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version,
                      api_version_from_server, requested_api_version)
            api_version_from_server = requested_api_version
        else:
            # Trust API version from server response
            log.debug('API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version,
                      api_version_from_server, api_version_from_server)
    return cls(build=build, api_version=api_version_from_server)
def guess(protocol, api_version_hint=None)

Ask the server which version it has. We haven't set up an Account object yet, so we generate requests by hand. We only need a response header containing a ServerVersionInfo element.

To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this package supports, until we get a valid response.

:param protocol: :param api_version_hint: (Default value = None)

Expand source code
@classmethod
def guess(cls, protocol, api_version_hint=None):
    """Ask the server which version it has. We haven't set up an Account object yet, so we generate requests
    by hand. We only need a response header containing a ServerVersionInfo element.

    To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that
    without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this
    package supports, until we get a valid response.

    :param protocol:
    :param api_version_hint:  (Default value = None)
    """
    from .services import ResolveNames
    # The protocol doesn't have a version yet, so default to latest supported version if we don't have a hint.
    api_version = api_version_hint or API_VERSIONS[0]
    log.debug('Asking server for version info using API version %s', api_version)
    # We don't know the build version yet. Hopefully, the server will report it in the SOAP header. Lots of
    # places expect a version to have a build, so this is a bit dangerous, but passing a fake build around is also
    # dangerous. Make sure the call to ResolveNames does not require a version build.
    protocol.config.version = Version(build=None, api_version=api_version)
    # Use ResolveNames as a minimal request to the server to test if the version is correct. If not, ResolveNames
    # will try to guess the version automatically.
    name = str(protocol.credentials) if protocol.credentials and str(protocol.credentials) else 'DUMMY'
    try:
        list(ResolveNames(protocol=protocol).call(unresolved_entries=[name]))
    except ResponseMessageError as e:
        # We may have survived long enough to get a new version
        if not protocol.config.version.build:
            raise TransportError('No valid version headers found in response (%r)' % e)
    if not protocol.config.version.build:
        raise TransportError('No valid version headers found in response')
    return protocol.version

Instance variables

var api_version

Return an attribute of instance, which is of type owner.

var build

Return an attribute of instance, which is of type owner.

var fullname
Expand source code
@property
def fullname(self):
    return VERSIONS[self.api_version][1]
exchangelib-4.6.1/docs/exchangelib/indexed_properties.html000066400000000000000000001032451414601472700237710ustar00rootroot00000000000000 exchangelib.indexed_properties API documentation

Module exchangelib.indexed_properties

Expand source code
import logging

from .fields import EmailSubField, LabelField, SubField, NamedSubField, Choice
from .properties import EWSElement, EWSMeta

log = logging.getLogger(__name__)


class IndexedElement(EWSElement, metaclass=EWSMeta):
    """Base class for all classes that implement an indexed element."""

    LABEL_CHOICES = ()


class SingleFieldIndexedElement(IndexedElement, metaclass=EWSMeta):
    """Base class for all classes that implement an indexed element with a single field."""

    @classmethod
    def value_field(cls, version=None):
        fields = cls.supported_fields(version=version)
        if len(fields) != 1:
            raise ValueError('This class must have only one field (found %s)' % (fields,))
        return fields[0]


class EmailAddress(SingleFieldIndexedElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/entry-emailaddress"""

    ELEMENT_NAME = 'Entry'
    LABEL_CHOICES = ('EmailAddress1', 'EmailAddress2', 'EmailAddress3')

    label = LabelField(field_uri='Key', choices={Choice(c) for c in LABEL_CHOICES}, default=LABEL_CHOICES[0])
    email = EmailSubField(is_required=True)


class PhoneNumber(SingleFieldIndexedElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/entry-phonenumber"""

    ELEMENT_NAME = 'Entry'
    LABEL_CHOICES = (
        'AssistantPhone', 'BusinessFax', 'BusinessPhone', 'BusinessPhone2', 'Callback', 'CarPhone', 'CompanyMainPhone',
        'HomeFax', 'HomePhone', 'HomePhone2', 'Isdn', 'MobilePhone', 'OtherFax', 'OtherTelephone', 'Pager',
        'PrimaryPhone', 'RadioPhone', 'Telex', 'TtyTddPhone'
    )

    label = LabelField(field_uri='Key', choices={Choice(c) for c in LABEL_CHOICES}, default='PrimaryPhone')
    phone_number = SubField(is_required=True)


class MultiFieldIndexedElement(IndexedElement, metaclass=EWSMeta):
    """Base class for all classes that implement an indexed element with multiple fields."""


class PhysicalAddress(MultiFieldIndexedElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/entry-physicaladdress"""

    ELEMENT_NAME = 'Entry'
    LABEL_CHOICES = ('Business', 'Home', 'Other')

    label = LabelField(field_uri='Key', choices={Choice(c) for c in LABEL_CHOICES}, default=LABEL_CHOICES[0])
    street = NamedSubField(field_uri='Street')  # Street, house number, etc.
    city = NamedSubField(field_uri='City')
    state = NamedSubField(field_uri='State')
    country = NamedSubField(field_uri='CountryOrRegion')
    zipcode = NamedSubField(field_uri='PostalCode')

    def clean(self, version=None):
        if isinstance(self.zipcode, int):
            self.zipcode = str(self.zipcode)
        super().clean(version=version)

Classes

class EmailAddress (**kwargs)
Expand source code
class EmailAddress(SingleFieldIndexedElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/entry-emailaddress"""

    ELEMENT_NAME = 'Entry'
    LABEL_CHOICES = ('EmailAddress1', 'EmailAddress2', 'EmailAddress3')

    label = LabelField(field_uri='Key', choices={Choice(c) for c in LABEL_CHOICES}, default=LABEL_CHOICES[0])
    email = EmailSubField(is_required=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var LABEL_CHOICES

Instance variables

var email
var label

Inherited members

class IndexedElement (**kwargs)

Base class for all classes that implement an indexed element.

Expand source code
class IndexedElement(EWSElement, metaclass=EWSMeta):
    """Base class for all classes that implement an indexed element."""

    LABEL_CHOICES = ()

Ancestors

Subclasses

Class variables

var LABEL_CHOICES

Inherited members

class MultiFieldIndexedElement (**kwargs)

Base class for all classes that implement an indexed element with multiple fields.

Expand source code
class MultiFieldIndexedElement(IndexedElement, metaclass=EWSMeta):
    """Base class for all classes that implement an indexed element with multiple fields."""

Ancestors

Subclasses

Inherited members

class PhoneNumber (**kwargs)
Expand source code
class PhoneNumber(SingleFieldIndexedElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/entry-phonenumber"""

    ELEMENT_NAME = 'Entry'
    LABEL_CHOICES = (
        'AssistantPhone', 'BusinessFax', 'BusinessPhone', 'BusinessPhone2', 'Callback', 'CarPhone', 'CompanyMainPhone',
        'HomeFax', 'HomePhone', 'HomePhone2', 'Isdn', 'MobilePhone', 'OtherFax', 'OtherTelephone', 'Pager',
        'PrimaryPhone', 'RadioPhone', 'Telex', 'TtyTddPhone'
    )

    label = LabelField(field_uri='Key', choices={Choice(c) for c in LABEL_CHOICES}, default='PrimaryPhone')
    phone_number = SubField(is_required=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var LABEL_CHOICES

Instance variables

var label
var phone_number

Inherited members

class PhysicalAddress (**kwargs)
Expand source code
class PhysicalAddress(MultiFieldIndexedElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/entry-physicaladdress"""

    ELEMENT_NAME = 'Entry'
    LABEL_CHOICES = ('Business', 'Home', 'Other')

    label = LabelField(field_uri='Key', choices={Choice(c) for c in LABEL_CHOICES}, default=LABEL_CHOICES[0])
    street = NamedSubField(field_uri='Street')  # Street, house number, etc.
    city = NamedSubField(field_uri='City')
    state = NamedSubField(field_uri='State')
    country = NamedSubField(field_uri='CountryOrRegion')
    zipcode = NamedSubField(field_uri='PostalCode')

    def clean(self, version=None):
        if isinstance(self.zipcode, int):
            self.zipcode = str(self.zipcode)
        super().clean(version=version)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var LABEL_CHOICES

Instance variables

var city
var country
var label
var state
var street
var zipcode

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    if isinstance(self.zipcode, int):
        self.zipcode = str(self.zipcode)
    super().clean(version=version)

Inherited members

class SingleFieldIndexedElement (**kwargs)

Base class for all classes that implement an indexed element with a single field.

Expand source code
class SingleFieldIndexedElement(IndexedElement, metaclass=EWSMeta):
    """Base class for all classes that implement an indexed element with a single field."""

    @classmethod
    def value_field(cls, version=None):
        fields = cls.supported_fields(version=version)
        if len(fields) != 1:
            raise ValueError('This class must have only one field (found %s)' % (fields,))
        return fields[0]

Ancestors

Subclasses

Static methods

def value_field(version=None)
Expand source code
@classmethod
def value_field(cls, version=None):
    fields = cls.supported_fields(version=version)
    if len(fields) != 1:
        raise ValueError('This class must have only one field (found %s)' % (fields,))
    return fields[0]

Inherited members

exchangelib-4.6.1/docs/exchangelib/items/000077500000000000000000000000001414601472700203235ustar00rootroot00000000000000exchangelib-4.6.1/docs/exchangelib/items/base.html000066400000000000000000001352221414601472700221300ustar00rootroot00000000000000 exchangelib.items.base API documentation

Module exchangelib.items.base

Expand source code
import logging

from ..extended_properties import ExtendedProperty
from ..fields import BooleanField, ExtendedPropertyField, BodyField, MailboxField, MailboxListField, EWSElementField, \
    CharField, IdElementField, AttachmentField
from ..properties import InvalidField, IdChangeKeyMixIn, EWSElement, ReferenceItemId, ItemId, EWSMeta
from ..services import CreateItem
from ..util import require_account
from ..version import EXCHANGE_2007_SP1

log = logging.getLogger(__name__)

# Shape enums
ID_ONLY = 'IdOnly'
DEFAULT = 'Default'
# AllProperties doesn't actually get all properties in FindItem, just the "first-class" ones. See
# https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/email-properties-and-elements-in-ews-in-exchange
ALL_PROPERTIES = 'AllProperties'
SHAPE_CHOICES = (ID_ONLY, DEFAULT, ALL_PROPERTIES)

# MessageDisposition values. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem
SAVE_ONLY = 'SaveOnly'
SEND_ONLY = 'SendOnly'
SEND_AND_SAVE_COPY = 'SendAndSaveCopy'
MESSAGE_DISPOSITION_CHOICES = (SAVE_ONLY, SEND_ONLY, SEND_AND_SAVE_COPY)

# SendMeetingInvitations values. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem
# SendMeetingInvitationsOrCancellations. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem
# SendMeetingCancellations values. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteitem
SEND_TO_NONE = 'SendToNone'
SEND_ONLY_TO_ALL = 'SendOnlyToAll'
SEND_ONLY_TO_CHANGED = 'SendOnlyToChanged'
SEND_TO_ALL_AND_SAVE_COPY = 'SendToAllAndSaveCopy'
SEND_TO_CHANGED_AND_SAVE_COPY = 'SendToChangedAndSaveCopy'
SEND_MEETING_INVITATIONS_CHOICES = (SEND_TO_NONE, SEND_ONLY_TO_ALL, SEND_TO_ALL_AND_SAVE_COPY)
SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES = (SEND_TO_NONE, SEND_ONLY_TO_ALL, SEND_ONLY_TO_CHANGED,
                                                      SEND_TO_ALL_AND_SAVE_COPY, SEND_TO_CHANGED_AND_SAVE_COPY)
SEND_MEETING_CANCELLATIONS_CHOICES = (SEND_TO_NONE, SEND_ONLY_TO_ALL, SEND_TO_ALL_AND_SAVE_COPY)

# AffectedTaskOccurrences values. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteitem
ALL_OCCURRENCIES = 'AllOccurrences'
SPECIFIED_OCCURRENCE_ONLY = 'SpecifiedOccurrenceOnly'
AFFECTED_TASK_OCCURRENCES_CHOICES = (ALL_OCCURRENCIES, SPECIFIED_OCCURRENCE_ONLY)

# ConflictResolution values. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem
NEVER_OVERWRITE = 'NeverOverwrite'
AUTO_RESOLVE = 'AutoResolve'
ALWAYS_OVERWRITE = 'AlwaysOverwrite'
CONFLICT_RESOLUTION_CHOICES = (NEVER_OVERWRITE, AUTO_RESOLVE, ALWAYS_OVERWRITE)

# DeleteType values. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteitem
HARD_DELETE = 'HardDelete'
SOFT_DELETE = 'SoftDelete'
MOVE_TO_DELETED_ITEMS = 'MoveToDeletedItems'
DELETE_TYPE_CHOICES = (HARD_DELETE, SOFT_DELETE, MOVE_TO_DELETED_ITEMS)


class RegisterMixIn(IdChangeKeyMixIn, metaclass=EWSMeta):
    """Base class for classes that can change their list of supported fields dynamically."""

    # This class implements dynamic fields on an element class, so we need to include __dict__ in __slots__
    __slots__ = '__dict__',

    INSERT_AFTER_FIELD = None

    @classmethod
    def register(cls, attr_name, attr_cls):
        """Register a custom extended property in this item class so they can be accessed just like any other attribute

        :param attr_name:
        :param attr_cls:
        :return:
        """
        if not cls.INSERT_AFTER_FIELD:
            raise ValueError('Class %s is missing INSERT_AFTER_FIELD value' % cls)
        try:
            cls.get_field_by_fieldname(attr_name)
        except InvalidField:
            pass
        else:
            raise ValueError("'%s' is already registered" % attr_name)
        if not issubclass(attr_cls, ExtendedProperty):
            raise ValueError("%r must be a subclass of ExtendedProperty" % attr_cls)
        # Check if class attributes are properly defined
        attr_cls.validate_cls()
        # ExtendedProperty is not a real field, but a placeholder in the fields list. See
        #   https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item
        #
        # Find the correct index for the new extended property, and insert.
        field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
        cls.add_field(field, insert_after=cls.INSERT_AFTER_FIELD)

    @classmethod
    def deregister(cls, attr_name):
        """De-register an extended property that has been registered with register().

        :param attr_name:
        :return:
        """
        try:
            field = cls.get_field_by_fieldname(attr_name)
        except InvalidField:
            raise ValueError("'%s' is not registered" % attr_name)
        if not isinstance(field, ExtendedPropertyField):
            raise ValueError("'%s' is not registered as an ExtendedProperty" % attr_name)
        cls.remove_field(field)


class BaseItem(RegisterMixIn, metaclass=EWSMeta):
    """Base class for all other classes that implement EWS items."""

    ID_ELEMENT_CLS = ItemId
    _id = IdElementField(field_uri='item:ItemId', value_cls=ID_ELEMENT_CLS)

    __slots__ = 'account', 'folder'

    def __init__(self, **kwargs):
        """Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

        :param kwargs:
            'account' is optional but allows calling 'send()' and 'delete()'
            'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set,
                we use folder.account.
        """
        from ..folders import BaseFolder
        from ..account import Account
        self.account = kwargs.pop('account', None)
        if self.account is not None and not isinstance(self.account, Account):
            raise ValueError("'account' %r must be an Account instance" % self.account)
        self.folder = kwargs.pop('folder', None)
        if self.folder is not None:
            if not isinstance(self.folder, BaseFolder):
                raise ValueError("'folder' %r must be a Folder instance" % self.folder)
            if self.folder.account is not None:
                if self.account is not None:
                    # Make sure the account from kwargs matches the folder account
                    if self.account != self.folder.account:
                        raise ValueError("'account' does not match 'folder.account'")
                self.account = self.folder.account
        super().__init__(**kwargs)

    @classmethod
    def from_xml(cls, elem, account):
        item = super().from_xml(elem=elem, account=account)
        item.account = account
        return item


class BaseReplyItem(EWSElement, metaclass=EWSMeta):
    """Base class for reply/forward elements that share the same fields."""

    subject = CharField(field_uri='Subject')
    body = BodyField(field_uri='Body')  # Accepts and returns Body or HTMLBody instances
    to_recipients = MailboxListField(field_uri='ToRecipients')
    cc_recipients = MailboxListField(field_uri='CcRecipients')
    bcc_recipients = MailboxListField(field_uri='BccRecipients')
    is_read_receipt_requested = BooleanField(field_uri='IsReadReceiptRequested')
    is_delivery_receipt_requested = BooleanField(field_uri='IsDeliveryReceiptRequested')
    author = MailboxField(field_uri='From')
    reference_item_id = EWSElementField(value_cls=ReferenceItemId)
    new_body = BodyField(field_uri='NewBodyContent')  # Accepts and returns Body or HTMLBody instances
    received_by = MailboxField(field_uri='ReceivedBy', supported_from=EXCHANGE_2007_SP1)
    received_by_representing = MailboxField(field_uri='ReceivedRepresenting', supported_from=EXCHANGE_2007_SP1)

    __slots__ = 'account',

    def __init__(self, **kwargs):
        # 'account' is optional but allows calling 'send()' and 'save()'
        from ..account import Account
        self.account = kwargs.pop('account', None)
        if self.account is not None and not isinstance(self.account, Account):
            raise ValueError("'account' %r must be an Account instance" % self.account)
        super().__init__(**kwargs)

    @require_account
    def send(self, save_copy=True, copy_to_folder=None):
        if copy_to_folder and not save_copy:
            raise AttributeError("'save_copy' must be True when 'copy_to_folder' is set")
        message_disposition = SEND_AND_SAVE_COPY if save_copy else SEND_ONLY
        return CreateItem(account=self.account).get(
            items=[self],
            folder=copy_to_folder,
            message_disposition=message_disposition,
            send_meeting_invitations=SEND_TO_NONE,
        )

    @require_account
    def save(self, folder):
        """Save the item for later modification. You may want to use account.drafts as the folder.

        :param folder:
        :return:
        """
        return CreateItem(account=self.account).get(
            items=[self],
            folder=folder,
            message_disposition=SAVE_ONLY,
            send_meeting_invitations=SEND_TO_NONE,
        )


class BulkCreateResult(BaseItem):
    """A dummy class to store return values from a CreateItem service call."""

    attachments = AttachmentField(field_uri='item:Attachments')  # ItemAttachment or FileAttachment

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if self.attachments is None:
            self.attachments = []

Classes

class BaseItem (**kwargs)

Base class for all other classes that implement EWS items.

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class BaseItem(RegisterMixIn, metaclass=EWSMeta):
    """Base class for all other classes that implement EWS items."""

    ID_ELEMENT_CLS = ItemId
    _id = IdElementField(field_uri='item:ItemId', value_cls=ID_ELEMENT_CLS)

    __slots__ = 'account', 'folder'

    def __init__(self, **kwargs):
        """Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

        :param kwargs:
            'account' is optional but allows calling 'send()' and 'delete()'
            'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set,
                we use folder.account.
        """
        from ..folders import BaseFolder
        from ..account import Account
        self.account = kwargs.pop('account', None)
        if self.account is not None and not isinstance(self.account, Account):
            raise ValueError("'account' %r must be an Account instance" % self.account)
        self.folder = kwargs.pop('folder', None)
        if self.folder is not None:
            if not isinstance(self.folder, BaseFolder):
                raise ValueError("'folder' %r must be a Folder instance" % self.folder)
            if self.folder.account is not None:
                if self.account is not None:
                    # Make sure the account from kwargs matches the folder account
                    if self.account != self.folder.account:
                        raise ValueError("'account' does not match 'folder.account'")
                self.account = self.folder.account
        super().__init__(**kwargs)

    @classmethod
    def from_xml(cls, elem, account):
        item = super().from_xml(elem=elem, account=account)
        item.account = account
        return item

Ancestors

Subclasses

Class variables

var FIELDS
var ID_ELEMENT_CLS

'id' and 'changekey' are UUIDs generated by Exchange.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/itemid

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    item = super().from_xml(elem=elem, account=account)
    item.account = account
    return item

Instance variables

var account

Return an attribute of instance, which is of type owner.

var folder

Return an attribute of instance, which is of type owner.

Inherited members

class BaseReplyItem (**kwargs)

Base class for reply/forward elements that share the same fields.

Expand source code
class BaseReplyItem(EWSElement, metaclass=EWSMeta):
    """Base class for reply/forward elements that share the same fields."""

    subject = CharField(field_uri='Subject')
    body = BodyField(field_uri='Body')  # Accepts and returns Body or HTMLBody instances
    to_recipients = MailboxListField(field_uri='ToRecipients')
    cc_recipients = MailboxListField(field_uri='CcRecipients')
    bcc_recipients = MailboxListField(field_uri='BccRecipients')
    is_read_receipt_requested = BooleanField(field_uri='IsReadReceiptRequested')
    is_delivery_receipt_requested = BooleanField(field_uri='IsDeliveryReceiptRequested')
    author = MailboxField(field_uri='From')
    reference_item_id = EWSElementField(value_cls=ReferenceItemId)
    new_body = BodyField(field_uri='NewBodyContent')  # Accepts and returns Body or HTMLBody instances
    received_by = MailboxField(field_uri='ReceivedBy', supported_from=EXCHANGE_2007_SP1)
    received_by_representing = MailboxField(field_uri='ReceivedRepresenting', supported_from=EXCHANGE_2007_SP1)

    __slots__ = 'account',

    def __init__(self, **kwargs):
        # 'account' is optional but allows calling 'send()' and 'save()'
        from ..account import Account
        self.account = kwargs.pop('account', None)
        if self.account is not None and not isinstance(self.account, Account):
            raise ValueError("'account' %r must be an Account instance" % self.account)
        super().__init__(**kwargs)

    @require_account
    def send(self, save_copy=True, copy_to_folder=None):
        if copy_to_folder and not save_copy:
            raise AttributeError("'save_copy' must be True when 'copy_to_folder' is set")
        message_disposition = SEND_AND_SAVE_COPY if save_copy else SEND_ONLY
        return CreateItem(account=self.account).get(
            items=[self],
            folder=copy_to_folder,
            message_disposition=message_disposition,
            send_meeting_invitations=SEND_TO_NONE,
        )

    @require_account
    def save(self, folder):
        """Save the item for later modification. You may want to use account.drafts as the folder.

        :param folder:
        :return:
        """
        return CreateItem(account=self.account).get(
            items=[self],
            folder=folder,
            message_disposition=SAVE_ONLY,
            send_meeting_invitations=SEND_TO_NONE,
        )

Ancestors

Subclasses

Class variables

var FIELDS

Instance variables

var account

Return an attribute of instance, which is of type owner.

var author
var bcc_recipients
var body
var cc_recipients
var is_delivery_receipt_requested
var is_read_receipt_requested
var new_body
var received_by
var received_by_representing
var reference_item_id
var subject
var to_recipients

Methods

def save(self, folder)

Save the item for later modification. You may want to use account.drafts as the folder.

:param folder: :return:

Expand source code
@require_account
def save(self, folder):
    """Save the item for later modification. You may want to use account.drafts as the folder.

    :param folder:
    :return:
    """
    return CreateItem(account=self.account).get(
        items=[self],
        folder=folder,
        message_disposition=SAVE_ONLY,
        send_meeting_invitations=SEND_TO_NONE,
    )
def send(self, save_copy=True, copy_to_folder=None)
Expand source code
@require_account
def send(self, save_copy=True, copy_to_folder=None):
    if copy_to_folder and not save_copy:
        raise AttributeError("'save_copy' must be True when 'copy_to_folder' is set")
    message_disposition = SEND_AND_SAVE_COPY if save_copy else SEND_ONLY
    return CreateItem(account=self.account).get(
        items=[self],
        folder=copy_to_folder,
        message_disposition=message_disposition,
        send_meeting_invitations=SEND_TO_NONE,
    )

Inherited members

class BulkCreateResult (**kwargs)

A dummy class to store return values from a CreateItem service call.

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class BulkCreateResult(BaseItem):
    """A dummy class to store return values from a CreateItem service call."""

    attachments = AttachmentField(field_uri='item:Attachments')  # ItemAttachment or FileAttachment

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if self.attachments is None:
            self.attachments = []

Ancestors

Class variables

var FIELDS

Instance variables

var attachments

Inherited members

class RegisterMixIn (**kwargs)

Base class for classes that can change their list of supported fields dynamically.

Expand source code
class RegisterMixIn(IdChangeKeyMixIn, metaclass=EWSMeta):
    """Base class for classes that can change their list of supported fields dynamically."""

    # This class implements dynamic fields on an element class, so we need to include __dict__ in __slots__
    __slots__ = '__dict__',

    INSERT_AFTER_FIELD = None

    @classmethod
    def register(cls, attr_name, attr_cls):
        """Register a custom extended property in this item class so they can be accessed just like any other attribute

        :param attr_name:
        :param attr_cls:
        :return:
        """
        if not cls.INSERT_AFTER_FIELD:
            raise ValueError('Class %s is missing INSERT_AFTER_FIELD value' % cls)
        try:
            cls.get_field_by_fieldname(attr_name)
        except InvalidField:
            pass
        else:
            raise ValueError("'%s' is already registered" % attr_name)
        if not issubclass(attr_cls, ExtendedProperty):
            raise ValueError("%r must be a subclass of ExtendedProperty" % attr_cls)
        # Check if class attributes are properly defined
        attr_cls.validate_cls()
        # ExtendedProperty is not a real field, but a placeholder in the fields list. See
        #   https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item
        #
        # Find the correct index for the new extended property, and insert.
        field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
        cls.add_field(field, insert_after=cls.INSERT_AFTER_FIELD)

    @classmethod
    def deregister(cls, attr_name):
        """De-register an extended property that has been registered with register().

        :param attr_name:
        :return:
        """
        try:
            field = cls.get_field_by_fieldname(attr_name)
        except InvalidField:
            raise ValueError("'%s' is not registered" % attr_name)
        if not isinstance(field, ExtendedPropertyField):
            raise ValueError("'%s' is not registered as an ExtendedProperty" % attr_name)
        cls.remove_field(field)

Ancestors

Subclasses

Class variables

var INSERT_AFTER_FIELD

Static methods

def deregister(attr_name)

De-register an extended property that has been registered with register().

:param attr_name: :return:

Expand source code
@classmethod
def deregister(cls, attr_name):
    """De-register an extended property that has been registered with register().

    :param attr_name:
    :return:
    """
    try:
        field = cls.get_field_by_fieldname(attr_name)
    except InvalidField:
        raise ValueError("'%s' is not registered" % attr_name)
    if not isinstance(field, ExtendedPropertyField):
        raise ValueError("'%s' is not registered as an ExtendedProperty" % attr_name)
    cls.remove_field(field)
def register(attr_name, attr_cls)

Register a custom extended property in this item class so they can be accessed just like any other attribute

:param attr_name: :param attr_cls: :return:

Expand source code
@classmethod
def register(cls, attr_name, attr_cls):
    """Register a custom extended property in this item class so they can be accessed just like any other attribute

    :param attr_name:
    :param attr_cls:
    :return:
    """
    if not cls.INSERT_AFTER_FIELD:
        raise ValueError('Class %s is missing INSERT_AFTER_FIELD value' % cls)
    try:
        cls.get_field_by_fieldname(attr_name)
    except InvalidField:
        pass
    else:
        raise ValueError("'%s' is already registered" % attr_name)
    if not issubclass(attr_cls, ExtendedProperty):
        raise ValueError("%r must be a subclass of ExtendedProperty" % attr_cls)
    # Check if class attributes are properly defined
    attr_cls.validate_cls()
    # ExtendedProperty is not a real field, but a placeholder in the fields list. See
    #   https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item
    #
    # Find the correct index for the new extended property, and insert.
    field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
    cls.add_field(field, insert_after=cls.INSERT_AFTER_FIELD)

Inherited members

exchangelib-4.6.1/docs/exchangelib/items/calendar_item.html000066400000000000000000004633541414601472700240170ustar00rootroot00000000000000 exchangelib.items.calendar_item API documentation

Module exchangelib.items.calendar_item

Expand source code
import datetime
import logging

from .base import BaseItem, BaseReplyItem, SEND_AND_SAVE_COPY, SEND_TO_NONE
from .item import Item
from .message import Message
from ..ewsdatetime import EWSDate, EWSDateTime
from ..fields import BooleanField, IntegerField, TextField, ChoiceField, URIField, BodyField, DateTimeField, \
    MessageHeaderField, AttachmentField, RecurrenceField, MailboxField, AttendeesField, Choice, OccurrenceField, \
    OccurrenceListField, TimeZoneField, CharField, EnumAsIntField, FreeBusyStatusField, ReferenceItemIdField, \
    AssociatedCalendarItemIdField, DateOrDateTimeField, EWSElementListField, AppointmentStateField
from ..properties import Attendee, ReferenceItemId, OccurrenceItemId, RecurringMasterItemId, EWSMeta
from ..recurrence import FirstOccurrence, LastOccurrence, Occurrence, DeletedOccurrence
from ..services import CreateItem
from ..util import set_xml_value, require_account
from ..version import EXCHANGE_2010, EXCHANGE_2013

log = logging.getLogger(__name__)

# Conference Type values. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/conferencetype
CONFERENCE_TYPES = ('NetMeeting', 'NetShow', 'Chat')

# CalendarItemType enums
SINGLE = 'Single'
OCCURRENCE = 'Occurrence'
EXCEPTION = 'Exception'
RECURRING_MASTER = 'RecurringMaster'
CALENDAR_ITEM_CHOICES = (SINGLE, OCCURRENCE, EXCEPTION, RECURRING_MASTER)


class AcceptDeclineMixIn:
    """A mixin for items that can be declined or accepted."""

    def accept(self, message_disposition=SEND_AND_SAVE_COPY, **kwargs):
        return AcceptItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            **kwargs
        ).send(message_disposition)

    def decline(self, message_disposition=SEND_AND_SAVE_COPY, **kwargs):
        return DeclineItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            **kwargs
        ).send(message_disposition)

    def tentatively_accept(self, message_disposition=SEND_AND_SAVE_COPY, **kwargs):
        return TentativelyAcceptItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            **kwargs
        ).send(message_disposition)


class CalendarItem(Item, AcceptDeclineMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendaritem"""

    ELEMENT_NAME = 'CalendarItem'

    uid = TextField(field_uri='calendar:UID', is_required_after_save=True, is_searchable=False)
    recurrence_id = DateTimeField(field_uri='calendar:RecurrenceId', is_read_only=True)
    start = DateOrDateTimeField(field_uri='calendar:Start', is_required=True)
    end = DateOrDateTimeField(field_uri='calendar:End', is_required=True)
    original_start = DateTimeField(field_uri='calendar:OriginalStart', is_read_only=True)
    is_all_day = BooleanField(field_uri='calendar:IsAllDayEvent', is_required=True, default=False)
    legacy_free_busy_status = FreeBusyStatusField(field_uri='calendar:LegacyFreeBusyStatus', is_required=True,
                                                  default='Busy')
    location = TextField(field_uri='calendar:Location')
    when = TextField(field_uri='calendar:When')
    is_meeting = BooleanField(field_uri='calendar:IsMeeting', is_read_only=True)
    is_cancelled = BooleanField(field_uri='calendar:IsCancelled', is_read_only=True)
    is_recurring = BooleanField(field_uri='calendar:IsRecurring', is_read_only=True)
    meeting_request_was_sent = BooleanField(field_uri='calendar:MeetingRequestWasSent', is_read_only=True)
    is_response_requested = BooleanField(field_uri='calendar:IsResponseRequested', default=None,
                                         is_required_after_save=True, is_searchable=False)
    type = ChoiceField(field_uri='calendar:CalendarItemType', choices={Choice(c) for c in CALENDAR_ITEM_CHOICES},
                       is_read_only=True)
    my_response_type = ChoiceField(field_uri='calendar:MyResponseType', choices={
            Choice(c) for c in Attendee.RESPONSE_TYPES
    }, is_read_only=True)
    organizer = MailboxField(field_uri='calendar:Organizer', is_read_only=True)
    required_attendees = AttendeesField(field_uri='calendar:RequiredAttendees', is_searchable=False)
    optional_attendees = AttendeesField(field_uri='calendar:OptionalAttendees', is_searchable=False)
    resources = AttendeesField(field_uri='calendar:Resources', is_searchable=False)
    conflicting_meeting_count = IntegerField(field_uri='calendar:ConflictingMeetingCount', is_read_only=True)
    adjacent_meeting_count = IntegerField(field_uri='calendar:AdjacentMeetingCount', is_read_only=True)
    conflicting_meetings = EWSElementListField(field_uri='calendar:ConflictingMeetings', value_cls='CalendarItem',
                                               namespace=Item.NAMESPACE, is_read_only=True)
    adjacent_meetings = EWSElementListField(field_uri='calendar:AdjacentMeetings', value_cls='CalendarItem',
                                            namespace=Item.NAMESPACE, is_read_only=True)
    duration = CharField(field_uri='calendar:Duration', is_read_only=True)
    appointment_reply_time = DateTimeField(field_uri='calendar:AppointmentReplyTime', is_read_only=True)
    appointment_sequence_number = IntegerField(field_uri='calendar:AppointmentSequenceNumber', is_read_only=True)
    appointment_state = AppointmentStateField(field_uri='calendar:AppointmentState', is_read_only=True)
    recurrence = RecurrenceField(field_uri='calendar:Recurrence', is_searchable=False)
    first_occurrence = OccurrenceField(field_uri='calendar:FirstOccurrence', value_cls=FirstOccurrence,
                                       is_read_only=True)
    last_occurrence = OccurrenceField(field_uri='calendar:LastOccurrence', value_cls=LastOccurrence,
                                      is_read_only=True)
    modified_occurrences = OccurrenceListField(field_uri='calendar:ModifiedOccurrences', value_cls=Occurrence,
                                               is_read_only=True)
    deleted_occurrences = OccurrenceListField(field_uri='calendar:DeletedOccurrences', value_cls=DeletedOccurrence,
                                              is_read_only=True)
    _meeting_timezone = TimeZoneField(field_uri='calendar:MeetingTimeZone', deprecated_from=EXCHANGE_2010,
                                      is_searchable=False)
    _start_timezone = TimeZoneField(field_uri='calendar:StartTimeZone', supported_from=EXCHANGE_2010,
                                    is_searchable=False)
    _end_timezone = TimeZoneField(field_uri='calendar:EndTimeZone', supported_from=EXCHANGE_2010,
                                  is_searchable=False)
    conference_type = EnumAsIntField(field_uri='calendar:ConferenceType', enum=CONFERENCE_TYPES, min=0,
                                     default=None, is_required_after_save=True)
    allow_new_time_proposal = BooleanField(field_uri='calendar:AllowNewTimeProposal', default=None,
                                           is_required_after_save=True, is_searchable=False)
    is_online_meeting = BooleanField(field_uri='calendar:IsOnlineMeeting', default=None,
                                     is_read_only=True)
    meeting_workspace_url = URIField(field_uri='calendar:MeetingWorkspaceUrl')
    net_show_url = URIField(field_uri='calendar:NetShowUrl')

    def occurrence(self, index):
        """Get an occurrence of a recurring master by index. No query is sent to the server to actually fetch the item.
        Call refresh() on the item do do so.

        Only call this method on a recurring master.

        :param index: The index, which is 1-based

        :return The occurrence
        """
        return self.__class__(
            account=self.account,
            folder=self.folder,
            _id=OccurrenceItemId(id=self.id, changekey=self.changekey, instance_index=index),
        )

    def recurring_master(self):
        """Get the recurring master of an occurrence. No query is sent to the server to actually fetch the item.
        Call refresh() on the item do do so.

        Only call this method on an occurrence of a recurring master.

        :return: The master occurrence
        """
        return self.__class__(
            account=self.account,
            folder=self.folder,
            _id=RecurringMasterItemId(id=self.id, changekey=self.changekey),
        )

    @classmethod
    def timezone_fields(cls):
        return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)]

    def clean_timezone_fields(self, version):
        # Sets proper values on the timezone fields if they are not already set
        if self.start is None:
            start_tz = None
        elif type(self.start) in (EWSDate, datetime.date):
            start_tz = self.account.default_timezone
        else:
            start_tz = self.start.tzinfo
        if self.end is None:
            end_tz = None
        elif type(self.end) in (EWSDate, datetime.date):
            end_tz = self.account.default_timezone
        else:
            end_tz = self.end.tzinfo
        if version.build < EXCHANGE_2010:
            if self._meeting_timezone is None:
                self._meeting_timezone = start_tz
            self._start_timezone = None
            self._end_timezone = None
        else:
            self._meeting_timezone = None
            if self._start_timezone is None:
                self._start_timezone = start_tz
            if self._end_timezone is None:
                self._end_timezone = end_tz

    def clean(self, version=None):
        super().clean(version=version)
        if self.start and self.end and self.end < self.start:
            raise ValueError("'end' must be greater than 'start' (%s -> %s)" % (self.start, self.end))
        if version:
            self.clean_timezone_fields(version=version)

    def cancel(self, **kwargs):
        return CancelCalendarItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            **kwargs
        ).send()

    def _update_fieldnames(self):
        update_fields = super()._update_fieldnames()
        if self.type == OCCURRENCE:
            # Some CalendarItem fields cannot be updated when the item is an occurrence. The values are empty when we
            # receive them so would have been updated because they are set to None.
            update_fields.remove('recurrence')
            update_fields.remove('uid')
        return update_fields

    @classmethod
    def from_xml(cls, elem, account):
        item = super().from_xml(elem=elem, account=account)
        # EWS returns the start and end values as a datetime regardless of the is_all_day status. Convert to date if
        # applicable.
        if not item.is_all_day:
            return item
        for field_name in ('start', 'end'):
            val = getattr(item, field_name)
            if val is None:
                continue
            # Return just the date part of the value. Subtract 1 day from the date if this is the end field. This is
            # the inverse of what we do in .to_xml(). Convert to the local timezone before getting the date.
            if field_name == 'end':
                val -= datetime.timedelta(days=1)
            tz = getattr(item, '_%s_timezone' % field_name)
            setattr(item, field_name, val.astimezone(tz).date())
        return item

    def tz_field_for_field_name(self, field_name):
        meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
        if self.account.version.build < EXCHANGE_2010:
            return meeting_tz_field
        if field_name == 'start':
            return start_tz_field
        if field_name == 'end':
            return end_tz_field
        raise ValueError('Unsupported field_name')

    def date_to_datetime(self, field_name):
        # EWS always expects a datetime. If we have a date value, then convert it to datetime in the local
        # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both
        # start and end values and let EWS apply its logic, but that seems hacky.
        value = getattr(self, field_name)
        tz = getattr(self, self.tz_field_for_field_name(field_name).name)
        value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz)
        if field_name == 'end':
            value += datetime.timedelta(days=1)
        return value

    def to_xml(self, version):
        # EWS has some special logic related to all-day start and end values. Non-midnight start values are pushed to
        # the previous midnight. Non-midnight end values are pushed to the following midnight. Midnight in this context
        # refers to midnight in the local timezone. See
        #
        # https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-create-all-day-events-by-using-ews-in-exchange
        #
        elem = super().to_xml(version=version)
        if not self.is_all_day:
            return elem
        for field_name in ('start', 'end'):
            value = getattr(self, field_name)
            if value is None:
                continue
            if type(value) in (EWSDate, datetime.date):
                # EWS always expects a datetime
                value = self.date_to_datetime(field_name=field_name)
                # We already generated an XML element for this field, but it contains a plain date at this point, which
                # is invalid. Replace the value.
                field = self.get_field_by_fieldname(field_name)
                set_xml_value(elem=elem.find(field.response_tag()), value=value, version=version)
        return elem


class BaseMeetingItem(Item):
    """A base class for meeting requests that share the same fields (Message, Request, Response, Cancellation)

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/responsecode
        Certain types are created as a side effect of doing something else. Meeting messages, for example, are created
        when you send a calendar item to attendees; they are not explicitly created.

    Therefore BaseMeetingItem inherits from  EWSElement has no save() or send() method
    """

    associated_calendar_item_id = AssociatedCalendarItemIdField(field_uri='meeting:AssociatedCalendarItemId')
    is_delegated = BooleanField(field_uri='meeting:IsDelegated', is_read_only=True, default=False)
    is_out_of_date = BooleanField(field_uri='meeting:IsOutOfDate', is_read_only=True, default=False)
    has_been_processed = BooleanField(field_uri='meeting:HasBeenProcessed', is_read_only=True, default=False)
    response_type = ChoiceField(field_uri='meeting:ResponseType', choices={
        Choice('Unknown'), Choice('Organizer'), Choice('Tentative'), Choice('Accept'), Choice('Decline'),
        Choice('NoResponseReceived')
    }, is_required=True, default='Unknown')

    effective_rights_idx = Item.FIELDS.index_by_name('effective_rights')
    sender_idx = Message.FIELDS.index_by_name('sender')
    reply_to_idx = Message.FIELDS.index_by_name('reply_to')
    FIELDS = Item.FIELDS[:effective_rights_idx] \
        + Message.FIELDS[sender_idx:reply_to_idx + 1] \
        + Item.FIELDS[effective_rights_idx:]


class MeetingRequest(BaseMeetingItem, AcceptDeclineMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingrequest"""

    ELEMENT_NAME = 'MeetingRequest'

    meeting_request_type = ChoiceField(field_uri='meetingRequest:MeetingRequestType', choices={
        Choice('FullUpdate'), Choice('InformationalUpdate'), Choice('NewMeetingRequest'), Choice('None'),
        Choice('Outdated'), Choice('PrincipalWantsCopy'), Choice('SilentUpdate')
    }, default='None')
    intended_free_busy_status = ChoiceField(field_uri='meetingRequest:IntendedFreeBusyStatus', choices={
            Choice('Free'), Choice('Tentative'), Choice('Busy'), Choice('OOF'), Choice('NoData')
    }, is_required=True, default='Busy')

    # This element also has some fields from CalendarItem
    start_idx = CalendarItem.FIELDS.index_by_name('start')
    is_response_requested_idx = CalendarItem.FIELDS.index_by_name('is_response_requested')
    FIELDS = BaseMeetingItem.FIELDS \
        + CalendarItem.FIELDS[start_idx:is_response_requested_idx]\
        + CalendarItem.FIELDS[is_response_requested_idx + 1:]


class MeetingMessage(BaseMeetingItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingmessage"""

    ELEMENT_NAME = 'MeetingMessage'


class MeetingResponse(BaseMeetingItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingresponse"""

    ELEMENT_NAME = 'MeetingResponse'

    received_by = MailboxField(field_uri='message:ReceivedBy', is_read_only=True)
    received_representing = MailboxField(field_uri='message:ReceivedRepresenting', is_read_only=True)
    proposed_start = DateTimeField(field_uri='meeting:ProposedStart', supported_from=EXCHANGE_2013)
    proposed_end = DateTimeField(field_uri='meeting:ProposedEnd', supported_from=EXCHANGE_2013)


class MeetingCancellation(BaseMeetingItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingcancellation"""

    ELEMENT_NAME = 'MeetingCancellation'


class BaseMeetingReplyItem(BaseItem, metaclass=EWSMeta):
    """Base class for meeting request reply items that share the same fields (Accept, TentativelyAccept, Decline)."""

    item_class = CharField(field_uri='item:ItemClass', is_read_only=True)
    sensitivity = ChoiceField(field_uri='item:Sensitivity', choices={
        Choice('Normal'), Choice('Personal'), Choice('Private'), Choice('Confidential')
    }, is_required=True, default='Normal')
    body = BodyField(field_uri='item:Body')  # Accepts and returns Body or HTMLBody instances
    attachments = AttachmentField(field_uri='item:Attachments')  # ItemAttachment or FileAttachment
    headers = MessageHeaderField(field_uri='item:InternetMessageHeaders', is_read_only=True)

    sender = Message.FIELDS['sender']
    to_recipients = Message.FIELDS['to_recipients']
    cc_recipients = Message.FIELDS['cc_recipients']
    bcc_recipients = Message.FIELDS['bcc_recipients']
    is_read_receipt_requested = Message.FIELDS['is_read_receipt_requested']
    is_delivery_receipt_requested = Message.FIELDS['is_delivery_receipt_requested']

    reference_item_id = ReferenceItemIdField(field_uri='item:ReferenceItemId')
    received_by = MailboxField(field_uri='message:ReceivedBy', is_read_only=True)
    received_representing = MailboxField(field_uri='message:ReceivedRepresenting', is_read_only=True)
    proposed_start = DateTimeField(field_uri='meeting:ProposedStart', supported_from=EXCHANGE_2013)
    proposed_end = DateTimeField(field_uri='meeting:ProposedEnd', supported_from=EXCHANGE_2013)

    @require_account
    def send(self, message_disposition=SEND_AND_SAVE_COPY):
        # Some responses contain multiple response IDs, e.g. MeetingRequest.accept(). Return either the single ID or
        # the list of IDs.
        res = list(CreateItem(account=self.account).call(
            items=[self],
            folder=self.folder,
            message_disposition=message_disposition,
            send_meeting_invitations=SEND_TO_NONE,
        ))
        for r in res:
            if isinstance(r, Exception):
                raise r
        if len(res) == 1:
            return res[0]
        return res


class AcceptItem(BaseMeetingReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/acceptitem"""

    ELEMENT_NAME = 'AcceptItem'


class TentativelyAcceptItem(BaseMeetingReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/tentativelyacceptitem"""

    ELEMENT_NAME = 'TentativelyAcceptItem'


class DeclineItem(BaseMeetingReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/declineitem"""

    ELEMENT_NAME = 'DeclineItem'


class CancelCalendarItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/cancelcalendaritem"""

    ELEMENT_NAME = 'CancelCalendarItem'
    author_idx = BaseReplyItem.FIELDS.index_by_name('author')
    FIELDS = BaseReplyItem.FIELDS[:author_idx] + BaseReplyItem.FIELDS[author_idx + 1:]

Classes

class AcceptDeclineMixIn

A mixin for items that can be declined or accepted.

Expand source code
class AcceptDeclineMixIn:
    """A mixin for items that can be declined or accepted."""

    def accept(self, message_disposition=SEND_AND_SAVE_COPY, **kwargs):
        return AcceptItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            **kwargs
        ).send(message_disposition)

    def decline(self, message_disposition=SEND_AND_SAVE_COPY, **kwargs):
        return DeclineItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            **kwargs
        ).send(message_disposition)

    def tentatively_accept(self, message_disposition=SEND_AND_SAVE_COPY, **kwargs):
        return TentativelyAcceptItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            **kwargs
        ).send(message_disposition)

Subclasses

Methods

def accept(self, message_disposition='SendAndSaveCopy', **kwargs)
Expand source code
def accept(self, message_disposition=SEND_AND_SAVE_COPY, **kwargs):
    return AcceptItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        **kwargs
    ).send(message_disposition)
def decline(self, message_disposition='SendAndSaveCopy', **kwargs)
Expand source code
def decline(self, message_disposition=SEND_AND_SAVE_COPY, **kwargs):
    return DeclineItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        **kwargs
    ).send(message_disposition)
def tentatively_accept(self, message_disposition='SendAndSaveCopy', **kwargs)
Expand source code
def tentatively_accept(self, message_disposition=SEND_AND_SAVE_COPY, **kwargs):
    return TentativelyAcceptItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        **kwargs
    ).send(message_disposition)
class AcceptItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/acceptitem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class AcceptItem(BaseMeetingReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/acceptitem"""

    ELEMENT_NAME = 'AcceptItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class BaseMeetingItem (**kwargs)

A base class for meeting requests that share the same fields (Message, Request, Response, Cancellation)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/responsecode Certain types are created as a side effect of doing something else. Meeting messages, for example, are created when you send a calendar item to attendees; they are not explicitly created.

Therefore BaseMeetingItem inherits from EWSElement has no save() or send() method

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class BaseMeetingItem(Item):
    """A base class for meeting requests that share the same fields (Message, Request, Response, Cancellation)

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/responsecode
        Certain types are created as a side effect of doing something else. Meeting messages, for example, are created
        when you send a calendar item to attendees; they are not explicitly created.

    Therefore BaseMeetingItem inherits from  EWSElement has no save() or send() method
    """

    associated_calendar_item_id = AssociatedCalendarItemIdField(field_uri='meeting:AssociatedCalendarItemId')
    is_delegated = BooleanField(field_uri='meeting:IsDelegated', is_read_only=True, default=False)
    is_out_of_date = BooleanField(field_uri='meeting:IsOutOfDate', is_read_only=True, default=False)
    has_been_processed = BooleanField(field_uri='meeting:HasBeenProcessed', is_read_only=True, default=False)
    response_type = ChoiceField(field_uri='meeting:ResponseType', choices={
        Choice('Unknown'), Choice('Organizer'), Choice('Tentative'), Choice('Accept'), Choice('Decline'),
        Choice('NoResponseReceived')
    }, is_required=True, default='Unknown')

    effective_rights_idx = Item.FIELDS.index_by_name('effective_rights')
    sender_idx = Message.FIELDS.index_by_name('sender')
    reply_to_idx = Message.FIELDS.index_by_name('reply_to')
    FIELDS = Item.FIELDS[:effective_rights_idx] \
        + Message.FIELDS[sender_idx:reply_to_idx + 1] \
        + Item.FIELDS[effective_rights_idx:]

Ancestors

Subclasses

Class variables

var FIELDS
var effective_rights_idx
var reply_to_idx
var sender_idx

Instance variables

var associated_calendar_item_id
var author
var bcc_recipients
var cc_recipients
var conversation_index
var conversation_topic
var has_been_processed
var is_delegated
var is_delivery_receipt_requested
var is_out_of_date
var is_read
var is_read_receipt_requested
var is_response_requested
var message_id
var references
var reply_to
var response_type
var sender
var to_recipients

Inherited members

class BaseMeetingReplyItem (**kwargs)

Base class for meeting request reply items that share the same fields (Accept, TentativelyAccept, Decline).

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class BaseMeetingReplyItem(BaseItem, metaclass=EWSMeta):
    """Base class for meeting request reply items that share the same fields (Accept, TentativelyAccept, Decline)."""

    item_class = CharField(field_uri='item:ItemClass', is_read_only=True)
    sensitivity = ChoiceField(field_uri='item:Sensitivity', choices={
        Choice('Normal'), Choice('Personal'), Choice('Private'), Choice('Confidential')
    }, is_required=True, default='Normal')
    body = BodyField(field_uri='item:Body')  # Accepts and returns Body or HTMLBody instances
    attachments = AttachmentField(field_uri='item:Attachments')  # ItemAttachment or FileAttachment
    headers = MessageHeaderField(field_uri='item:InternetMessageHeaders', is_read_only=True)

    sender = Message.FIELDS['sender']
    to_recipients = Message.FIELDS['to_recipients']
    cc_recipients = Message.FIELDS['cc_recipients']
    bcc_recipients = Message.FIELDS['bcc_recipients']
    is_read_receipt_requested = Message.FIELDS['is_read_receipt_requested']
    is_delivery_receipt_requested = Message.FIELDS['is_delivery_receipt_requested']

    reference_item_id = ReferenceItemIdField(field_uri='item:ReferenceItemId')
    received_by = MailboxField(field_uri='message:ReceivedBy', is_read_only=True)
    received_representing = MailboxField(field_uri='message:ReceivedRepresenting', is_read_only=True)
    proposed_start = DateTimeField(field_uri='meeting:ProposedStart', supported_from=EXCHANGE_2013)
    proposed_end = DateTimeField(field_uri='meeting:ProposedEnd', supported_from=EXCHANGE_2013)

    @require_account
    def send(self, message_disposition=SEND_AND_SAVE_COPY):
        # Some responses contain multiple response IDs, e.g. MeetingRequest.accept(). Return either the single ID or
        # the list of IDs.
        res = list(CreateItem(account=self.account).call(
            items=[self],
            folder=self.folder,
            message_disposition=message_disposition,
            send_meeting_invitations=SEND_TO_NONE,
        ))
        for r in res:
            if isinstance(r, Exception):
                raise r
        if len(res) == 1:
            return res[0]
        return res

Ancestors

Subclasses

Class variables

var FIELDS

Instance variables

var attachments
var bcc_recipients
var body
var cc_recipients
var headers
var is_delivery_receipt_requested
var is_read_receipt_requested
var item_class
var proposed_end
var proposed_start
var received_by
var received_representing
var reference_item_id
var sender
var sensitivity
var to_recipients

Methods

def send(self, message_disposition='SendAndSaveCopy')
Expand source code
@require_account
def send(self, message_disposition=SEND_AND_SAVE_COPY):
    # Some responses contain multiple response IDs, e.g. MeetingRequest.accept(). Return either the single ID or
    # the list of IDs.
    res = list(CreateItem(account=self.account).call(
        items=[self],
        folder=self.folder,
        message_disposition=message_disposition,
        send_meeting_invitations=SEND_TO_NONE,
    ))
    for r in res:
        if isinstance(r, Exception):
            raise r
    if len(res) == 1:
        return res[0]
    return res

Inherited members

class CalendarItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendaritem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class CalendarItem(Item, AcceptDeclineMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendaritem"""

    ELEMENT_NAME = 'CalendarItem'

    uid = TextField(field_uri='calendar:UID', is_required_after_save=True, is_searchable=False)
    recurrence_id = DateTimeField(field_uri='calendar:RecurrenceId', is_read_only=True)
    start = DateOrDateTimeField(field_uri='calendar:Start', is_required=True)
    end = DateOrDateTimeField(field_uri='calendar:End', is_required=True)
    original_start = DateTimeField(field_uri='calendar:OriginalStart', is_read_only=True)
    is_all_day = BooleanField(field_uri='calendar:IsAllDayEvent', is_required=True, default=False)
    legacy_free_busy_status = FreeBusyStatusField(field_uri='calendar:LegacyFreeBusyStatus', is_required=True,
                                                  default='Busy')
    location = TextField(field_uri='calendar:Location')
    when = TextField(field_uri='calendar:When')
    is_meeting = BooleanField(field_uri='calendar:IsMeeting', is_read_only=True)
    is_cancelled = BooleanField(field_uri='calendar:IsCancelled', is_read_only=True)
    is_recurring = BooleanField(field_uri='calendar:IsRecurring', is_read_only=True)
    meeting_request_was_sent = BooleanField(field_uri='calendar:MeetingRequestWasSent', is_read_only=True)
    is_response_requested = BooleanField(field_uri='calendar:IsResponseRequested', default=None,
                                         is_required_after_save=True, is_searchable=False)
    type = ChoiceField(field_uri='calendar:CalendarItemType', choices={Choice(c) for c in CALENDAR_ITEM_CHOICES},
                       is_read_only=True)
    my_response_type = ChoiceField(field_uri='calendar:MyResponseType', choices={
            Choice(c) for c in Attendee.RESPONSE_TYPES
    }, is_read_only=True)
    organizer = MailboxField(field_uri='calendar:Organizer', is_read_only=True)
    required_attendees = AttendeesField(field_uri='calendar:RequiredAttendees', is_searchable=False)
    optional_attendees = AttendeesField(field_uri='calendar:OptionalAttendees', is_searchable=False)
    resources = AttendeesField(field_uri='calendar:Resources', is_searchable=False)
    conflicting_meeting_count = IntegerField(field_uri='calendar:ConflictingMeetingCount', is_read_only=True)
    adjacent_meeting_count = IntegerField(field_uri='calendar:AdjacentMeetingCount', is_read_only=True)
    conflicting_meetings = EWSElementListField(field_uri='calendar:ConflictingMeetings', value_cls='CalendarItem',
                                               namespace=Item.NAMESPACE, is_read_only=True)
    adjacent_meetings = EWSElementListField(field_uri='calendar:AdjacentMeetings', value_cls='CalendarItem',
                                            namespace=Item.NAMESPACE, is_read_only=True)
    duration = CharField(field_uri='calendar:Duration', is_read_only=True)
    appointment_reply_time = DateTimeField(field_uri='calendar:AppointmentReplyTime', is_read_only=True)
    appointment_sequence_number = IntegerField(field_uri='calendar:AppointmentSequenceNumber', is_read_only=True)
    appointment_state = AppointmentStateField(field_uri='calendar:AppointmentState', is_read_only=True)
    recurrence = RecurrenceField(field_uri='calendar:Recurrence', is_searchable=False)
    first_occurrence = OccurrenceField(field_uri='calendar:FirstOccurrence', value_cls=FirstOccurrence,
                                       is_read_only=True)
    last_occurrence = OccurrenceField(field_uri='calendar:LastOccurrence', value_cls=LastOccurrence,
                                      is_read_only=True)
    modified_occurrences = OccurrenceListField(field_uri='calendar:ModifiedOccurrences', value_cls=Occurrence,
                                               is_read_only=True)
    deleted_occurrences = OccurrenceListField(field_uri='calendar:DeletedOccurrences', value_cls=DeletedOccurrence,
                                              is_read_only=True)
    _meeting_timezone = TimeZoneField(field_uri='calendar:MeetingTimeZone', deprecated_from=EXCHANGE_2010,
                                      is_searchable=False)
    _start_timezone = TimeZoneField(field_uri='calendar:StartTimeZone', supported_from=EXCHANGE_2010,
                                    is_searchable=False)
    _end_timezone = TimeZoneField(field_uri='calendar:EndTimeZone', supported_from=EXCHANGE_2010,
                                  is_searchable=False)
    conference_type = EnumAsIntField(field_uri='calendar:ConferenceType', enum=CONFERENCE_TYPES, min=0,
                                     default=None, is_required_after_save=True)
    allow_new_time_proposal = BooleanField(field_uri='calendar:AllowNewTimeProposal', default=None,
                                           is_required_after_save=True, is_searchable=False)
    is_online_meeting = BooleanField(field_uri='calendar:IsOnlineMeeting', default=None,
                                     is_read_only=True)
    meeting_workspace_url = URIField(field_uri='calendar:MeetingWorkspaceUrl')
    net_show_url = URIField(field_uri='calendar:NetShowUrl')

    def occurrence(self, index):
        """Get an occurrence of a recurring master by index. No query is sent to the server to actually fetch the item.
        Call refresh() on the item do do so.

        Only call this method on a recurring master.

        :param index: The index, which is 1-based

        :return The occurrence
        """
        return self.__class__(
            account=self.account,
            folder=self.folder,
            _id=OccurrenceItemId(id=self.id, changekey=self.changekey, instance_index=index),
        )

    def recurring_master(self):
        """Get the recurring master of an occurrence. No query is sent to the server to actually fetch the item.
        Call refresh() on the item do do so.

        Only call this method on an occurrence of a recurring master.

        :return: The master occurrence
        """
        return self.__class__(
            account=self.account,
            folder=self.folder,
            _id=RecurringMasterItemId(id=self.id, changekey=self.changekey),
        )

    @classmethod
    def timezone_fields(cls):
        return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)]

    def clean_timezone_fields(self, version):
        # Sets proper values on the timezone fields if they are not already set
        if self.start is None:
            start_tz = None
        elif type(self.start) in (EWSDate, datetime.date):
            start_tz = self.account.default_timezone
        else:
            start_tz = self.start.tzinfo
        if self.end is None:
            end_tz = None
        elif type(self.end) in (EWSDate, datetime.date):
            end_tz = self.account.default_timezone
        else:
            end_tz = self.end.tzinfo
        if version.build < EXCHANGE_2010:
            if self._meeting_timezone is None:
                self._meeting_timezone = start_tz
            self._start_timezone = None
            self._end_timezone = None
        else:
            self._meeting_timezone = None
            if self._start_timezone is None:
                self._start_timezone = start_tz
            if self._end_timezone is None:
                self._end_timezone = end_tz

    def clean(self, version=None):
        super().clean(version=version)
        if self.start and self.end and self.end < self.start:
            raise ValueError("'end' must be greater than 'start' (%s -> %s)" % (self.start, self.end))
        if version:
            self.clean_timezone_fields(version=version)

    def cancel(self, **kwargs):
        return CancelCalendarItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            **kwargs
        ).send()

    def _update_fieldnames(self):
        update_fields = super()._update_fieldnames()
        if self.type == OCCURRENCE:
            # Some CalendarItem fields cannot be updated when the item is an occurrence. The values are empty when we
            # receive them so would have been updated because they are set to None.
            update_fields.remove('recurrence')
            update_fields.remove('uid')
        return update_fields

    @classmethod
    def from_xml(cls, elem, account):
        item = super().from_xml(elem=elem, account=account)
        # EWS returns the start and end values as a datetime regardless of the is_all_day status. Convert to date if
        # applicable.
        if not item.is_all_day:
            return item
        for field_name in ('start', 'end'):
            val = getattr(item, field_name)
            if val is None:
                continue
            # Return just the date part of the value. Subtract 1 day from the date if this is the end field. This is
            # the inverse of what we do in .to_xml(). Convert to the local timezone before getting the date.
            if field_name == 'end':
                val -= datetime.timedelta(days=1)
            tz = getattr(item, '_%s_timezone' % field_name)
            setattr(item, field_name, val.astimezone(tz).date())
        return item

    def tz_field_for_field_name(self, field_name):
        meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
        if self.account.version.build < EXCHANGE_2010:
            return meeting_tz_field
        if field_name == 'start':
            return start_tz_field
        if field_name == 'end':
            return end_tz_field
        raise ValueError('Unsupported field_name')

    def date_to_datetime(self, field_name):
        # EWS always expects a datetime. If we have a date value, then convert it to datetime in the local
        # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both
        # start and end values and let EWS apply its logic, but that seems hacky.
        value = getattr(self, field_name)
        tz = getattr(self, self.tz_field_for_field_name(field_name).name)
        value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz)
        if field_name == 'end':
            value += datetime.timedelta(days=1)
        return value

    def to_xml(self, version):
        # EWS has some special logic related to all-day start and end values. Non-midnight start values are pushed to
        # the previous midnight. Non-midnight end values are pushed to the following midnight. Midnight in this context
        # refers to midnight in the local timezone. See
        #
        # https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-create-all-day-events-by-using-ews-in-exchange
        #
        elem = super().to_xml(version=version)
        if not self.is_all_day:
            return elem
        for field_name in ('start', 'end'):
            value = getattr(self, field_name)
            if value is None:
                continue
            if type(value) in (EWSDate, datetime.date):
                # EWS always expects a datetime
                value = self.date_to_datetime(field_name=field_name)
                # We already generated an XML element for this field, but it contains a plain date at this point, which
                # is invalid. Replace the value.
                field = self.get_field_by_fieldname(field_name)
                set_xml_value(elem=elem.find(field.response_tag()), value=value, version=version)
        return elem

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    item = super().from_xml(elem=elem, account=account)
    # EWS returns the start and end values as a datetime regardless of the is_all_day status. Convert to date if
    # applicable.
    if not item.is_all_day:
        return item
    for field_name in ('start', 'end'):
        val = getattr(item, field_name)
        if val is None:
            continue
        # Return just the date part of the value. Subtract 1 day from the date if this is the end field. This is
        # the inverse of what we do in .to_xml(). Convert to the local timezone before getting the date.
        if field_name == 'end':
            val -= datetime.timedelta(days=1)
        tz = getattr(item, '_%s_timezone' % field_name)
        setattr(item, field_name, val.astimezone(tz).date())
    return item
def timezone_fields()
Expand source code
@classmethod
def timezone_fields(cls):
    return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)]

Instance variables

var adjacent_meeting_count
var adjacent_meetings
var allow_new_time_proposal
var appointment_reply_time
var appointment_sequence_number
var appointment_state
var conference_type
var conflicting_meeting_count
var conflicting_meetings
var deleted_occurrences
var duration
var end
var first_occurrence
var is_all_day
var is_cancelled
var is_meeting
var is_online_meeting
var is_recurring
var is_response_requested
var last_occurrence
var legacy_free_busy_status
var location
var meeting_request_was_sent
var meeting_workspace_url
var modified_occurrences
var my_response_type
var net_show_url
var optional_attendees
var organizer
var original_start
var recurrence
var recurrence_id
var required_attendees
var resources
var start
var type
var uid
var when

Methods

def cancel(self, **kwargs)
Expand source code
def cancel(self, **kwargs):
    return CancelCalendarItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        **kwargs
    ).send()
def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)
    if self.start and self.end and self.end < self.start:
        raise ValueError("'end' must be greater than 'start' (%s -> %s)" % (self.start, self.end))
    if version:
        self.clean_timezone_fields(version=version)
def clean_timezone_fields(self, version)
Expand source code
def clean_timezone_fields(self, version):
    # Sets proper values on the timezone fields if they are not already set
    if self.start is None:
        start_tz = None
    elif type(self.start) in (EWSDate, datetime.date):
        start_tz = self.account.default_timezone
    else:
        start_tz = self.start.tzinfo
    if self.end is None:
        end_tz = None
    elif type(self.end) in (EWSDate, datetime.date):
        end_tz = self.account.default_timezone
    else:
        end_tz = self.end.tzinfo
    if version.build < EXCHANGE_2010:
        if self._meeting_timezone is None:
            self._meeting_timezone = start_tz
        self._start_timezone = None
        self._end_timezone = None
    else:
        self._meeting_timezone = None
        if self._start_timezone is None:
            self._start_timezone = start_tz
        if self._end_timezone is None:
            self._end_timezone = end_tz
def date_to_datetime(self, field_name)
Expand source code
def date_to_datetime(self, field_name):
    # EWS always expects a datetime. If we have a date value, then convert it to datetime in the local
    # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both
    # start and end values and let EWS apply its logic, but that seems hacky.
    value = getattr(self, field_name)
    tz = getattr(self, self.tz_field_for_field_name(field_name).name)
    value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz)
    if field_name == 'end':
        value += datetime.timedelta(days=1)
    return value
def occurrence(self, index)

Get an occurrence of a recurring master by index. No query is sent to the server to actually fetch the item. Call refresh() on the item do do so.

Only call this method on a recurring master.

:param index: The index, which is 1-based

:return The occurrence

Expand source code
def occurrence(self, index):
    """Get an occurrence of a recurring master by index. No query is sent to the server to actually fetch the item.
    Call refresh() on the item do do so.

    Only call this method on a recurring master.

    :param index: The index, which is 1-based

    :return The occurrence
    """
    return self.__class__(
        account=self.account,
        folder=self.folder,
        _id=OccurrenceItemId(id=self.id, changekey=self.changekey, instance_index=index),
    )
def recurring_master(self)

Get the recurring master of an occurrence. No query is sent to the server to actually fetch the item. Call refresh() on the item do do so.

Only call this method on an occurrence of a recurring master.

:return: The master occurrence

Expand source code
def recurring_master(self):
    """Get the recurring master of an occurrence. No query is sent to the server to actually fetch the item.
    Call refresh() on the item do do so.

    Only call this method on an occurrence of a recurring master.

    :return: The master occurrence
    """
    return self.__class__(
        account=self.account,
        folder=self.folder,
        _id=RecurringMasterItemId(id=self.id, changekey=self.changekey),
    )
def to_xml(self, version)
Expand source code
def to_xml(self, version):
    # EWS has some special logic related to all-day start and end values. Non-midnight start values are pushed to
    # the previous midnight. Non-midnight end values are pushed to the following midnight. Midnight in this context
    # refers to midnight in the local timezone. See
    #
    # https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-create-all-day-events-by-using-ews-in-exchange
    #
    elem = super().to_xml(version=version)
    if not self.is_all_day:
        return elem
    for field_name in ('start', 'end'):
        value = getattr(self, field_name)
        if value is None:
            continue
        if type(value) in (EWSDate, datetime.date):
            # EWS always expects a datetime
            value = self.date_to_datetime(field_name=field_name)
            # We already generated an XML element for this field, but it contains a plain date at this point, which
            # is invalid. Replace the value.
            field = self.get_field_by_fieldname(field_name)
            set_xml_value(elem=elem.find(field.response_tag()), value=value, version=version)
    return elem
def tz_field_for_field_name(self, field_name)
Expand source code
def tz_field_for_field_name(self, field_name):
    meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
    if self.account.version.build < EXCHANGE_2010:
        return meeting_tz_field
    if field_name == 'start':
        return start_tz_field
    if field_name == 'end':
        return end_tz_field
    raise ValueError('Unsupported field_name')

Inherited members

class CancelCalendarItem (**kwargs)
Expand source code
class CancelCalendarItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/cancelcalendaritem"""

    ELEMENT_NAME = 'CancelCalendarItem'
    author_idx = BaseReplyItem.FIELDS.index_by_name('author')
    FIELDS = BaseReplyItem.FIELDS[:author_idx] + BaseReplyItem.FIELDS[author_idx + 1:]

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var author_idx

Inherited members

class DeclineItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/declineitem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class DeclineItem(BaseMeetingReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/declineitem"""

    ELEMENT_NAME = 'DeclineItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class MeetingCancellation (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingcancellation

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class MeetingCancellation(BaseMeetingItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingcancellation"""

    ELEMENT_NAME = 'MeetingCancellation'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class MeetingMessage (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingmessage

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class MeetingMessage(BaseMeetingItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingmessage"""

    ELEMENT_NAME = 'MeetingMessage'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class MeetingRequest (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingrequest

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class MeetingRequest(BaseMeetingItem, AcceptDeclineMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingrequest"""

    ELEMENT_NAME = 'MeetingRequest'

    meeting_request_type = ChoiceField(field_uri='meetingRequest:MeetingRequestType', choices={
        Choice('FullUpdate'), Choice('InformationalUpdate'), Choice('NewMeetingRequest'), Choice('None'),
        Choice('Outdated'), Choice('PrincipalWantsCopy'), Choice('SilentUpdate')
    }, default='None')
    intended_free_busy_status = ChoiceField(field_uri='meetingRequest:IntendedFreeBusyStatus', choices={
            Choice('Free'), Choice('Tentative'), Choice('Busy'), Choice('OOF'), Choice('NoData')
    }, is_required=True, default='Busy')

    # This element also has some fields from CalendarItem
    start_idx = CalendarItem.FIELDS.index_by_name('start')
    is_response_requested_idx = CalendarItem.FIELDS.index_by_name('is_response_requested')
    FIELDS = BaseMeetingItem.FIELDS \
        + CalendarItem.FIELDS[start_idx:is_response_requested_idx]\
        + CalendarItem.FIELDS[is_response_requested_idx + 1:]

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var is_response_requested_idx
var start_idx

Instance variables

var adjacent_meeting_count
var adjacent_meetings
var allow_new_time_proposal
var appointment_reply_time
var appointment_sequence_number
var appointment_state
var conference_type
var conflicting_meeting_count
var conflicting_meetings
var deleted_occurrences
var duration
var end
var first_occurrence
var intended_free_busy_status
var is_all_day
var is_cancelled
var is_meeting
var is_online_meeting
var is_recurring
var last_occurrence
var legacy_free_busy_status
var location
var meeting_request_type
var meeting_request_was_sent
var meeting_workspace_url
var modified_occurrences
var my_response_type
var net_show_url
var optional_attendees
var organizer
var original_start
var recurrence
var required_attendees
var resources
var start
var type
var when

Inherited members

class MeetingResponse (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingresponse

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class MeetingResponse(BaseMeetingItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingresponse"""

    ELEMENT_NAME = 'MeetingResponse'

    received_by = MailboxField(field_uri='message:ReceivedBy', is_read_only=True)
    received_representing = MailboxField(field_uri='message:ReceivedRepresenting', is_read_only=True)
    proposed_start = DateTimeField(field_uri='meeting:ProposedStart', supported_from=EXCHANGE_2013)
    proposed_end = DateTimeField(field_uri='meeting:ProposedEnd', supported_from=EXCHANGE_2013)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var proposed_end
var proposed_start
var received_by
var received_representing

Inherited members

class TentativelyAcceptItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/tentativelyacceptitem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class TentativelyAcceptItem(BaseMeetingReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/tentativelyacceptitem"""

    ELEMENT_NAME = 'TentativelyAcceptItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

exchangelib-4.6.1/docs/exchangelib/items/contact.html000066400000000000000000002716271414601472700226630ustar00rootroot00000000000000 exchangelib.items.contact API documentation

Module exchangelib.items.contact

Expand source code
import datetime
import logging

from .item import Item
from ..fields import BooleanField, Base64Field, TextField, ChoiceField, URIField, DateTimeBackedDateField, \
    PhoneNumberField, EmailAddressesField, PhysicalAddressField, Choice, MemberListField, CharField, TextListField, \
    EmailAddressField, IdElementField, EWSElementField, DateTimeField, EWSElementListField, \
    BodyContentAttributedValueField, StringAttributedValueField, PhoneNumberAttributedValueField, \
    PersonaPhoneNumberField, EmailAddressAttributedValueField, PostalAddressAttributedValueField, MailboxField, \
    MailboxListField
from ..properties import PersonaId, IdChangeKeyMixIn, CompleteName, Attribution, EmailAddress, Address, FolderId
from ..util import TNS
from ..version import EXCHANGE_2010, EXCHANGE_2010_SP2

log = logging.getLogger(__name__)


class Contact(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/contact"""

    ELEMENT_NAME = 'Contact'

    file_as = TextField(field_uri='contacts:FileAs')
    file_as_mapping = ChoiceField(field_uri='contacts:FileAsMapping', choices={
        Choice('None'), Choice('LastCommaFirst'), Choice('FirstSpaceLast'), Choice('Company'),
        Choice('LastCommaFirstCompany'), Choice('CompanyLastFirst'), Choice('LastFirst'),
        Choice('LastFirstCompany'), Choice('CompanyLastCommaFirst'), Choice('LastFirstSuffix'),
        Choice('LastSpaceFirstCompany'), Choice('CompanyLastSpaceFirst'), Choice('LastSpaceFirst'),
        Choice('DisplayName'), Choice('FirstName'), Choice('LastFirstMiddleSuffix'), Choice('LastName'),
        Choice('Empty'),
    })
    display_name = TextField(field_uri='contacts:DisplayName', is_required=True)
    given_name = CharField(field_uri='contacts:GivenName')
    initials = TextField(field_uri='contacts:Initials')
    middle_name = CharField(field_uri='contacts:MiddleName')
    nickname = TextField(field_uri='contacts:Nickname')
    complete_name = EWSElementField(field_uri='contacts:CompleteName', value_cls=CompleteName, is_read_only=True)
    company_name = TextField(field_uri='contacts:CompanyName')
    email_addresses = EmailAddressesField(field_uri='contacts:EmailAddress')
    physical_addresses = PhysicalAddressField(field_uri='contacts:PhysicalAddress')
    phone_numbers = PhoneNumberField(field_uri='contacts:PhoneNumber')
    assistant_name = TextField(field_uri='contacts:AssistantName')
    birthday = DateTimeBackedDateField(field_uri='contacts:Birthday', default_time=datetime.time(11, 59))
    business_homepage = URIField(field_uri='contacts:BusinessHomePage')
    children = TextListField(field_uri='contacts:Children')
    companies = TextListField(field_uri='contacts:Companies', is_searchable=False)
    contact_source = ChoiceField(field_uri='contacts:ContactSource', choices={
        Choice('Store'), Choice('ActiveDirectory')
    }, is_read_only=True)
    department = TextField(field_uri='contacts:Department')
    generation = TextField(field_uri='contacts:Generation')
    im_addresses = CharField(field_uri='contacts:ImAddresses', is_read_only=True)
    job_title = TextField(field_uri='contacts:JobTitle')
    manager = TextField(field_uri='contacts:Manager')
    mileage = TextField(field_uri='contacts:Mileage')
    office = TextField(field_uri='contacts:OfficeLocation')
    postal_address_index = ChoiceField(field_uri='contacts:PostalAddressIndex', choices={
        Choice('Business'), Choice('Home'), Choice('Other'), Choice('None')
    }, default='None', is_required_after_save=True)
    profession = TextField(field_uri='contacts:Profession')
    spouse_name = TextField(field_uri='contacts:SpouseName')
    surname = CharField(field_uri='contacts:Surname')
    wedding_anniversary = DateTimeBackedDateField(field_uri='contacts:WeddingAnniversary',
                                                  default_time=datetime.time(11, 59))
    has_picture = BooleanField(field_uri='contacts:HasPicture', supported_from=EXCHANGE_2010, is_read_only=True)
    phonetic_full_name = TextField(field_uri='contacts:PhoneticFullName', supported_from=EXCHANGE_2010_SP2,
                                   is_read_only=True)
    phonetic_first_name = TextField(field_uri='contacts:PhoneticFirstName', supported_from=EXCHANGE_2010_SP2,
                                    is_read_only=True)
    phonetic_last_name = TextField(field_uri='contacts:PhoneticLastName', supported_from=EXCHANGE_2010_SP2,
                                   is_read_only=True)
    email_alias = EmailAddressField(field_uri='contacts:Alias', is_read_only=True,
                                    supported_from=EXCHANGE_2010_SP2)
    # 'notes' is documented in MSDN but apparently unused. Writing to it raises ErrorInvalidPropertyRequest. OWA
    # put entries into the 'notes' form field into the 'body' field.
    notes = CharField(field_uri='contacts:Notes', supported_from=EXCHANGE_2010_SP2, is_read_only=True)
    # 'photo' is documented in MSDN but apparently unused. Writing to it raises ErrorInvalidPropertyRequest. OWA
    # adds photos as FileAttachments on the contact item (with 'is_contact_photo=True'), which automatically flips
    # the 'has_picture' field.
    photo = Base64Field(field_uri='contacts:Photo', supported_from=EXCHANGE_2010_SP2, is_read_only=True)
    user_smime_certificate = Base64Field(field_uri='contacts:UserSMIMECertificate', supported_from=EXCHANGE_2010_SP2,
                                         is_read_only=True)
    ms_exchange_certificate = Base64Field(field_uri='contacts:MSExchangeCertificate', supported_from=EXCHANGE_2010_SP2,
                                          is_read_only=True)
    directory_id = TextField(field_uri='contacts:DirectoryId', supported_from=EXCHANGE_2010_SP2, is_read_only=True)
    manager_mailbox = MailboxField(field_uri='contacts:ManagerMailbox', supported_from=EXCHANGE_2010_SP2,
                                   is_read_only=True)
    direct_reports = MailboxListField(field_uri='contacts:DirectReports', supported_from=EXCHANGE_2010_SP2,
                                      is_read_only=True)


class Persona(IdChangeKeyMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/persona"""

    ELEMENT_NAME = 'Persona'
    ID_ELEMENT_CLS = PersonaId

    _id = IdElementField(field_uri='persona:PersonaId', value_cls=ID_ELEMENT_CLS, namespace=TNS)
    persona_type = CharField(field_uri='persona:PersonaType')
    persona_object_type = TextField(field_uri='persona:PersonaObjectStatus')
    creation_time = DateTimeField(field_uri='persona:CreationTime')
    bodies = BodyContentAttributedValueField(field_uri='persona:Bodies')
    display_name_first_last_sort_key = TextField(field_uri='persona:DisplayNameFirstLastSortKey')
    display_name_last_first_sort_key = TextField(field_uri='persona:DisplayNameLastFirstSortKey')
    company_sort_key = TextField(field_uri='persona:CompanyNameSortKey')
    home_sort_key = TextField(field_uri='persona:HomeCitySortKey')
    work_city_sort_key = TextField(field_uri='persona:WorkCitySortKey')
    display_name_first_last_header = CharField(field_uri='persona:DisplayNameFirstLastHeader')
    display_name_last_first_header = CharField(field_uri='persona:DisplayNameLastFirstHeader')
    file_as_header = TextField(field_uri='persona:FileAsHeader')
    display_name = CharField(field_uri='persona:DisplayName')
    display_name_first_last = CharField(field_uri='persona:DisplayNameFirstLast')
    display_name_last_first = CharField(field_uri='persona:DisplayNameLastFirst')
    file_as = CharField(field_uri='persona:FileAs')
    file_as_id = TextField(field_uri='persona:FileAsId')
    display_name_prefix = CharField(field_uri='persona:DisplayNamePrefix')
    given_name = CharField(field_uri='persona:GivenName')
    middle_name = CharField(field_uri='persona:MiddleName')
    surname = CharField(field_uri='persona:Surname')
    generation = CharField(field_uri='persona:Generation')
    nickname = TextField(field_uri='persona:Nickname')
    yomi_company_name = TextField(field_uri='persona:YomiCompanyName')
    yomi_first_name = TextField(field_uri='persona:YomiFirstName')
    yomi_last_name = TextField(field_uri='persona:YomiLastName')
    title = CharField(field_uri='persona:Title')
    department = TextField(field_uri='persona:Department')
    company_name = CharField(field_uri='persona:CompanyName')
    email_address = EWSElementField(field_uri='persona:EmailAddress', value_cls=EmailAddress)
    email_addresses = EWSElementListField(field_uri='persona:EmailAddresses', value_cls=Address)
    PhoneNumber = PersonaPhoneNumberField(field_uri='persona:PhoneNumber')
    im_address = CharField(field_uri='persona:ImAddress')
    home_city = CharField(field_uri='persona:HomeCity')
    work_city = CharField(field_uri='persona:WorkCity')
    relevance_score = CharField(field_uri='persona:RelevanceScore')
    folder_ids = EWSElementListField(field_uri='persona:FolderIds', value_cls=FolderId)
    attributions = EWSElementListField(field_uri='persona:Attributions', value_cls=Attribution)
    display_names = StringAttributedValueField(field_uri='persona:DisplayNames')
    file_ases = StringAttributedValueField(field_uri='persona:FileAses')
    file_as_ids = StringAttributedValueField(field_uri='persona:FileAsIds')
    display_name_prefixes = StringAttributedValueField(field_uri='persona:DisplayNamePrefixes')
    given_names = StringAttributedValueField(field_uri='persona:GivenNames')
    middle_names = StringAttributedValueField(field_uri='persona:MiddleNames')
    surnames = StringAttributedValueField(field_uri='persona:Surnames')
    generations = StringAttributedValueField(field_uri='persona:Generations')
    nicknames = StringAttributedValueField(field_uri='persona:Nicknames')
    initials = StringAttributedValueField(field_uri='persona:Initials')
    yomi_company_names = StringAttributedValueField(field_uri='persona:YomiCompanyNames')
    yomi_first_names = StringAttributedValueField(field_uri='persona:YomiFirstNames')
    yomi_last_names = StringAttributedValueField(field_uri='persona:YomiLastNames')
    business_phone_numbers = PhoneNumberAttributedValueField(field_uri='persona:BusinessPhoneNumbers')
    business_phone_numbers2 = PhoneNumberAttributedValueField(field_uri='persona:BusinessPhoneNumbers2')
    home_phones = PhoneNumberAttributedValueField(field_uri='persona:HomePhones')
    home_phones2 = PhoneNumberAttributedValueField(field_uri='persona:HomePhones2')
    mobile_phones = PhoneNumberAttributedValueField(field_uri='persona:MobilePhones')
    mobile_phones2 = PhoneNumberAttributedValueField(field_uri='persona:MobilePhones2')
    assistant_phone_numbers = PhoneNumberAttributedValueField(field_uri='persona:AssistantPhoneNumbers')
    callback_phones = PhoneNumberAttributedValueField(field_uri='persona:CallbackPhones')
    car_phones = PhoneNumberAttributedValueField(field_uri='persona:CarPhones')
    home_faxes = PhoneNumberAttributedValueField(field_uri='persona:HomeFaxes')
    orgnaization_main_phones = PhoneNumberAttributedValueField(field_uri='persona:OrganizationMainPhones')
    other_faxes = PhoneNumberAttributedValueField(field_uri='persona:OtherFaxes')
    other_telephones = PhoneNumberAttributedValueField(field_uri='persona:OtherTelephones')
    other_phones2 = PhoneNumberAttributedValueField(field_uri='persona:OtherPhones2')
    pagers = PhoneNumberAttributedValueField(field_uri='persona:Pagers')
    radio_phones = PhoneNumberAttributedValueField(field_uri='persona:RadioPhones')
    telex_numbers = PhoneNumberAttributedValueField(field_uri='persona:TelexNumbers')
    tty_tdd_phone_numbers = PhoneNumberAttributedValueField(field_uri='persona:TTYTDDPhoneNumbers')
    work_faxes = PhoneNumberAttributedValueField(field_uri='persona:WorkFaxes')
    emails1 = EmailAddressAttributedValueField(field_uri='persona:Emails1')
    emails2 = EmailAddressAttributedValueField(field_uri='persona:Emails2')
    emails3 = EmailAddressAttributedValueField(field_uri='persona:Emails3')
    business_home_pages = StringAttributedValueField(field_uri='persona:BusinessHomePages')
    personal_home_pages = StringAttributedValueField(field_uri='persona:PersonalHomePages')
    office_locations = StringAttributedValueField(field_uri='persona:OfficeLocations')
    im_addresses = StringAttributedValueField(field_uri='persona:ImAddresses')
    im_addresses2 = StringAttributedValueField(field_uri='persona:ImAddresses2')
    im_addresses3 = StringAttributedValueField(field_uri='persona:ImAddresses3')
    business_addresses = PostalAddressAttributedValueField(field_uri='persona:BusinessAddresses')
    home_addresses = PostalAddressAttributedValueField(field_uri='persona:HomeAddresses')
    other_addresses = PostalAddressAttributedValueField(field_uri='persona:OtherAddresses')
    titles = StringAttributedValueField(field_uri='persona:Titles')
    departments = StringAttributedValueField(field_uri='persona:Departments')
    company_names = StringAttributedValueField(field_uri='persona:CompanyNames')
    managers = StringAttributedValueField(field_uri='persona:Managers')
    assistant_names = StringAttributedValueField(field_uri='persona:AssistantNames')
    professions = StringAttributedValueField(field_uri='persona:Professions')
    spouse_names = StringAttributedValueField(field_uri='persona:SpouseNames')
    children = StringAttributedValueField(field_uri='persona:Children')
    schools = StringAttributedValueField(field_uri='persona:Schools')
    hobbies = StringAttributedValueField(field_uri='persona:Hobbies')
    wedding_anniversaries = StringAttributedValueField(field_uri='persona:WeddingAnniversaries')
    birthdays = StringAttributedValueField(field_uri='persona:Birthdays')
    locations = StringAttributedValueField(field_uri='persona:Locations')
    # ExtendedPropertyAttributedValueField('extended_properties', field_uri='persona:ExtendedProperties')


class DistributionList(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distributionlist"""

    ELEMENT_NAME = 'DistributionList'

    display_name = CharField(field_uri='contacts:DisplayName', is_required=True)
    file_as = CharField(field_uri='contacts:FileAs', is_read_only=True)
    contact_source = ChoiceField(field_uri='contacts:ContactSource', choices={
        Choice('Store'), Choice('ActiveDirectory')
    }, is_read_only=True)
    members = MemberListField(field_uri='distributionlist:Members')

Classes

class Contact (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/contact

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class Contact(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/contact"""

    ELEMENT_NAME = 'Contact'

    file_as = TextField(field_uri='contacts:FileAs')
    file_as_mapping = ChoiceField(field_uri='contacts:FileAsMapping', choices={
        Choice('None'), Choice('LastCommaFirst'), Choice('FirstSpaceLast'), Choice('Company'),
        Choice('LastCommaFirstCompany'), Choice('CompanyLastFirst'), Choice('LastFirst'),
        Choice('LastFirstCompany'), Choice('CompanyLastCommaFirst'), Choice('LastFirstSuffix'),
        Choice('LastSpaceFirstCompany'), Choice('CompanyLastSpaceFirst'), Choice('LastSpaceFirst'),
        Choice('DisplayName'), Choice('FirstName'), Choice('LastFirstMiddleSuffix'), Choice('LastName'),
        Choice('Empty'),
    })
    display_name = TextField(field_uri='contacts:DisplayName', is_required=True)
    given_name = CharField(field_uri='contacts:GivenName')
    initials = TextField(field_uri='contacts:Initials')
    middle_name = CharField(field_uri='contacts:MiddleName')
    nickname = TextField(field_uri='contacts:Nickname')
    complete_name = EWSElementField(field_uri='contacts:CompleteName', value_cls=CompleteName, is_read_only=True)
    company_name = TextField(field_uri='contacts:CompanyName')
    email_addresses = EmailAddressesField(field_uri='contacts:EmailAddress')
    physical_addresses = PhysicalAddressField(field_uri='contacts:PhysicalAddress')
    phone_numbers = PhoneNumberField(field_uri='contacts:PhoneNumber')
    assistant_name = TextField(field_uri='contacts:AssistantName')
    birthday = DateTimeBackedDateField(field_uri='contacts:Birthday', default_time=datetime.time(11, 59))
    business_homepage = URIField(field_uri='contacts:BusinessHomePage')
    children = TextListField(field_uri='contacts:Children')
    companies = TextListField(field_uri='contacts:Companies', is_searchable=False)
    contact_source = ChoiceField(field_uri='contacts:ContactSource', choices={
        Choice('Store'), Choice('ActiveDirectory')
    }, is_read_only=True)
    department = TextField(field_uri='contacts:Department')
    generation = TextField(field_uri='contacts:Generation')
    im_addresses = CharField(field_uri='contacts:ImAddresses', is_read_only=True)
    job_title = TextField(field_uri='contacts:JobTitle')
    manager = TextField(field_uri='contacts:Manager')
    mileage = TextField(field_uri='contacts:Mileage')
    office = TextField(field_uri='contacts:OfficeLocation')
    postal_address_index = ChoiceField(field_uri='contacts:PostalAddressIndex', choices={
        Choice('Business'), Choice('Home'), Choice('Other'), Choice('None')
    }, default='None', is_required_after_save=True)
    profession = TextField(field_uri='contacts:Profession')
    spouse_name = TextField(field_uri='contacts:SpouseName')
    surname = CharField(field_uri='contacts:Surname')
    wedding_anniversary = DateTimeBackedDateField(field_uri='contacts:WeddingAnniversary',
                                                  default_time=datetime.time(11, 59))
    has_picture = BooleanField(field_uri='contacts:HasPicture', supported_from=EXCHANGE_2010, is_read_only=True)
    phonetic_full_name = TextField(field_uri='contacts:PhoneticFullName', supported_from=EXCHANGE_2010_SP2,
                                   is_read_only=True)
    phonetic_first_name = TextField(field_uri='contacts:PhoneticFirstName', supported_from=EXCHANGE_2010_SP2,
                                    is_read_only=True)
    phonetic_last_name = TextField(field_uri='contacts:PhoneticLastName', supported_from=EXCHANGE_2010_SP2,
                                   is_read_only=True)
    email_alias = EmailAddressField(field_uri='contacts:Alias', is_read_only=True,
                                    supported_from=EXCHANGE_2010_SP2)
    # 'notes' is documented in MSDN but apparently unused. Writing to it raises ErrorInvalidPropertyRequest. OWA
    # put entries into the 'notes' form field into the 'body' field.
    notes = CharField(field_uri='contacts:Notes', supported_from=EXCHANGE_2010_SP2, is_read_only=True)
    # 'photo' is documented in MSDN but apparently unused. Writing to it raises ErrorInvalidPropertyRequest. OWA
    # adds photos as FileAttachments on the contact item (with 'is_contact_photo=True'), which automatically flips
    # the 'has_picture' field.
    photo = Base64Field(field_uri='contacts:Photo', supported_from=EXCHANGE_2010_SP2, is_read_only=True)
    user_smime_certificate = Base64Field(field_uri='contacts:UserSMIMECertificate', supported_from=EXCHANGE_2010_SP2,
                                         is_read_only=True)
    ms_exchange_certificate = Base64Field(field_uri='contacts:MSExchangeCertificate', supported_from=EXCHANGE_2010_SP2,
                                          is_read_only=True)
    directory_id = TextField(field_uri='contacts:DirectoryId', supported_from=EXCHANGE_2010_SP2, is_read_only=True)
    manager_mailbox = MailboxField(field_uri='contacts:ManagerMailbox', supported_from=EXCHANGE_2010_SP2,
                                   is_read_only=True)
    direct_reports = MailboxListField(field_uri='contacts:DirectReports', supported_from=EXCHANGE_2010_SP2,
                                      is_read_only=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var assistant_name
var birthday
var business_homepage
var children
var companies
var company_name
var complete_name
var contact_source
var department
var direct_reports
var directory_id
var display_name
var email_addresses
var email_alias
var file_as
var file_as_mapping
var generation
var given_name
var has_picture
var im_addresses
var initials
var job_title
var manager
var manager_mailbox
var middle_name
var mileage
var ms_exchange_certificate
var nickname
var notes
var office
var phone_numbers
var phonetic_first_name
var phonetic_full_name
var phonetic_last_name
var photo
var physical_addresses
var postal_address_index
var profession
var spouse_name
var surname
var user_smime_certificate
var wedding_anniversary

Inherited members

class DistributionList (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distributionlist

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class DistributionList(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distributionlist"""

    ELEMENT_NAME = 'DistributionList'

    display_name = CharField(field_uri='contacts:DisplayName', is_required=True)
    file_as = CharField(field_uri='contacts:FileAs', is_read_only=True)
    contact_source = ChoiceField(field_uri='contacts:ContactSource', choices={
        Choice('Store'), Choice('ActiveDirectory')
    }, is_read_only=True)
    members = MemberListField(field_uri='distributionlist:Members')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var contact_source
var display_name
var file_as
var members

Inherited members

class Persona (**kwargs)
Expand source code
class Persona(IdChangeKeyMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/persona"""

    ELEMENT_NAME = 'Persona'
    ID_ELEMENT_CLS = PersonaId

    _id = IdElementField(field_uri='persona:PersonaId', value_cls=ID_ELEMENT_CLS, namespace=TNS)
    persona_type = CharField(field_uri='persona:PersonaType')
    persona_object_type = TextField(field_uri='persona:PersonaObjectStatus')
    creation_time = DateTimeField(field_uri='persona:CreationTime')
    bodies = BodyContentAttributedValueField(field_uri='persona:Bodies')
    display_name_first_last_sort_key = TextField(field_uri='persona:DisplayNameFirstLastSortKey')
    display_name_last_first_sort_key = TextField(field_uri='persona:DisplayNameLastFirstSortKey')
    company_sort_key = TextField(field_uri='persona:CompanyNameSortKey')
    home_sort_key = TextField(field_uri='persona:HomeCitySortKey')
    work_city_sort_key = TextField(field_uri='persona:WorkCitySortKey')
    display_name_first_last_header = CharField(field_uri='persona:DisplayNameFirstLastHeader')
    display_name_last_first_header = CharField(field_uri='persona:DisplayNameLastFirstHeader')
    file_as_header = TextField(field_uri='persona:FileAsHeader')
    display_name = CharField(field_uri='persona:DisplayName')
    display_name_first_last = CharField(field_uri='persona:DisplayNameFirstLast')
    display_name_last_first = CharField(field_uri='persona:DisplayNameLastFirst')
    file_as = CharField(field_uri='persona:FileAs')
    file_as_id = TextField(field_uri='persona:FileAsId')
    display_name_prefix = CharField(field_uri='persona:DisplayNamePrefix')
    given_name = CharField(field_uri='persona:GivenName')
    middle_name = CharField(field_uri='persona:MiddleName')
    surname = CharField(field_uri='persona:Surname')
    generation = CharField(field_uri='persona:Generation')
    nickname = TextField(field_uri='persona:Nickname')
    yomi_company_name = TextField(field_uri='persona:YomiCompanyName')
    yomi_first_name = TextField(field_uri='persona:YomiFirstName')
    yomi_last_name = TextField(field_uri='persona:YomiLastName')
    title = CharField(field_uri='persona:Title')
    department = TextField(field_uri='persona:Department')
    company_name = CharField(field_uri='persona:CompanyName')
    email_address = EWSElementField(field_uri='persona:EmailAddress', value_cls=EmailAddress)
    email_addresses = EWSElementListField(field_uri='persona:EmailAddresses', value_cls=Address)
    PhoneNumber = PersonaPhoneNumberField(field_uri='persona:PhoneNumber')
    im_address = CharField(field_uri='persona:ImAddress')
    home_city = CharField(field_uri='persona:HomeCity')
    work_city = CharField(field_uri='persona:WorkCity')
    relevance_score = CharField(field_uri='persona:RelevanceScore')
    folder_ids = EWSElementListField(field_uri='persona:FolderIds', value_cls=FolderId)
    attributions = EWSElementListField(field_uri='persona:Attributions', value_cls=Attribution)
    display_names = StringAttributedValueField(field_uri='persona:DisplayNames')
    file_ases = StringAttributedValueField(field_uri='persona:FileAses')
    file_as_ids = StringAttributedValueField(field_uri='persona:FileAsIds')
    display_name_prefixes = StringAttributedValueField(field_uri='persona:DisplayNamePrefixes')
    given_names = StringAttributedValueField(field_uri='persona:GivenNames')
    middle_names = StringAttributedValueField(field_uri='persona:MiddleNames')
    surnames = StringAttributedValueField(field_uri='persona:Surnames')
    generations = StringAttributedValueField(field_uri='persona:Generations')
    nicknames = StringAttributedValueField(field_uri='persona:Nicknames')
    initials = StringAttributedValueField(field_uri='persona:Initials')
    yomi_company_names = StringAttributedValueField(field_uri='persona:YomiCompanyNames')
    yomi_first_names = StringAttributedValueField(field_uri='persona:YomiFirstNames')
    yomi_last_names = StringAttributedValueField(field_uri='persona:YomiLastNames')
    business_phone_numbers = PhoneNumberAttributedValueField(field_uri='persona:BusinessPhoneNumbers')
    business_phone_numbers2 = PhoneNumberAttributedValueField(field_uri='persona:BusinessPhoneNumbers2')
    home_phones = PhoneNumberAttributedValueField(field_uri='persona:HomePhones')
    home_phones2 = PhoneNumberAttributedValueField(field_uri='persona:HomePhones2')
    mobile_phones = PhoneNumberAttributedValueField(field_uri='persona:MobilePhones')
    mobile_phones2 = PhoneNumberAttributedValueField(field_uri='persona:MobilePhones2')
    assistant_phone_numbers = PhoneNumberAttributedValueField(field_uri='persona:AssistantPhoneNumbers')
    callback_phones = PhoneNumberAttributedValueField(field_uri='persona:CallbackPhones')
    car_phones = PhoneNumberAttributedValueField(field_uri='persona:CarPhones')
    home_faxes = PhoneNumberAttributedValueField(field_uri='persona:HomeFaxes')
    orgnaization_main_phones = PhoneNumberAttributedValueField(field_uri='persona:OrganizationMainPhones')
    other_faxes = PhoneNumberAttributedValueField(field_uri='persona:OtherFaxes')
    other_telephones = PhoneNumberAttributedValueField(field_uri='persona:OtherTelephones')
    other_phones2 = PhoneNumberAttributedValueField(field_uri='persona:OtherPhones2')
    pagers = PhoneNumberAttributedValueField(field_uri='persona:Pagers')
    radio_phones = PhoneNumberAttributedValueField(field_uri='persona:RadioPhones')
    telex_numbers = PhoneNumberAttributedValueField(field_uri='persona:TelexNumbers')
    tty_tdd_phone_numbers = PhoneNumberAttributedValueField(field_uri='persona:TTYTDDPhoneNumbers')
    work_faxes = PhoneNumberAttributedValueField(field_uri='persona:WorkFaxes')
    emails1 = EmailAddressAttributedValueField(field_uri='persona:Emails1')
    emails2 = EmailAddressAttributedValueField(field_uri='persona:Emails2')
    emails3 = EmailAddressAttributedValueField(field_uri='persona:Emails3')
    business_home_pages = StringAttributedValueField(field_uri='persona:BusinessHomePages')
    personal_home_pages = StringAttributedValueField(field_uri='persona:PersonalHomePages')
    office_locations = StringAttributedValueField(field_uri='persona:OfficeLocations')
    im_addresses = StringAttributedValueField(field_uri='persona:ImAddresses')
    im_addresses2 = StringAttributedValueField(field_uri='persona:ImAddresses2')
    im_addresses3 = StringAttributedValueField(field_uri='persona:ImAddresses3')
    business_addresses = PostalAddressAttributedValueField(field_uri='persona:BusinessAddresses')
    home_addresses = PostalAddressAttributedValueField(field_uri='persona:HomeAddresses')
    other_addresses = PostalAddressAttributedValueField(field_uri='persona:OtherAddresses')
    titles = StringAttributedValueField(field_uri='persona:Titles')
    departments = StringAttributedValueField(field_uri='persona:Departments')
    company_names = StringAttributedValueField(field_uri='persona:CompanyNames')
    managers = StringAttributedValueField(field_uri='persona:Managers')
    assistant_names = StringAttributedValueField(field_uri='persona:AssistantNames')
    professions = StringAttributedValueField(field_uri='persona:Professions')
    spouse_names = StringAttributedValueField(field_uri='persona:SpouseNames')
    children = StringAttributedValueField(field_uri='persona:Children')
    schools = StringAttributedValueField(field_uri='persona:Schools')
    hobbies = StringAttributedValueField(field_uri='persona:Hobbies')
    wedding_anniversaries = StringAttributedValueField(field_uri='persona:WeddingAnniversaries')
    birthdays = StringAttributedValueField(field_uri='persona:Birthdays')
    locations = StringAttributedValueField(field_uri='persona:Locations')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var ID_ELEMENT_CLS

Instance variables

var PhoneNumber
var assistant_names
var assistant_phone_numbers
var attributions
var birthdays
var bodies
var business_addresses
var business_home_pages
var business_phone_numbers
var business_phone_numbers2
var callback_phones
var car_phones
var children
var company_name
var company_names
var company_sort_key
var creation_time
var department
var departments
var display_name
var display_name_first_last
var display_name_first_last_header
var display_name_first_last_sort_key
var display_name_last_first
var display_name_last_first_header
var display_name_last_first_sort_key
var display_name_prefix
var display_name_prefixes
var display_names
var email_address
var email_addresses
var emails1
var emails2
var emails3
var file_as
var file_as_header
var file_as_id
var file_as_ids
var file_ases
var folder_ids
var generation
var generations
var given_name
var given_names
var hobbies
var home_addresses
var home_city
var home_faxes
var home_phones
var home_phones2
var home_sort_key
var im_address
var im_addresses
var im_addresses2
var im_addresses3
var initials
var locations
var managers
var middle_name
var middle_names
var mobile_phones
var mobile_phones2
var nickname
var nicknames
var office_locations
var orgnaization_main_phones
var other_addresses
var other_faxes
var other_phones2
var other_telephones
var pagers
var persona_object_type
var persona_type
var personal_home_pages
var professions
var radio_phones
var relevance_score
var schools
var spouse_names
var surname
var surnames
var telex_numbers
var title
var titles
var tty_tdd_phone_numbers
var wedding_anniversaries
var work_city
var work_city_sort_key
var work_faxes
var yomi_company_name
var yomi_company_names
var yomi_first_name
var yomi_first_names
var yomi_last_name
var yomi_last_names

Inherited members

exchangelib-4.6.1/docs/exchangelib/items/index.html000066400000000000000000011322101414601472700223200ustar00rootroot00000000000000 exchangelib.items API documentation

Module exchangelib.items

Expand source code
from .base import RegisterMixIn, BulkCreateResult, MESSAGE_DISPOSITION_CHOICES, SAVE_ONLY, SEND_ONLY, \
    ID_ONLY, DEFAULT, ALL_PROPERTIES, SEND_MEETING_INVITATIONS_CHOICES, SEND_TO_NONE, SEND_ONLY_TO_ALL, \
    SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES, SEND_ONLY_TO_CHANGED, SEND_TO_CHANGED_AND_SAVE_COPY, \
    SEND_MEETING_CANCELLATIONS_CHOICES, AFFECTED_TASK_OCCURRENCES_CHOICES, ALL_OCCURRENCIES, \
    SPECIFIED_OCCURRENCE_ONLY, CONFLICT_RESOLUTION_CHOICES, NEVER_OVERWRITE, AUTO_RESOLVE, ALWAYS_OVERWRITE, \
    DELETE_TYPE_CHOICES, HARD_DELETE, SOFT_DELETE, MOVE_TO_DELETED_ITEMS, SEND_TO_ALL_AND_SAVE_COPY, \
    SEND_AND_SAVE_COPY, SHAPE_CHOICES
from .calendar_item import CalendarItem, AcceptItem, TentativelyAcceptItem, DeclineItem, CancelCalendarItem, \
    MeetingMessage, MeetingRequest, MeetingResponse, MeetingCancellation, CONFERENCE_TYPES
from .contact import Contact, Persona, DistributionList
from .item import BaseItem, Item
from .message import Message, ReplyToItem, ReplyAllToItem, ForwardItem
from .post import PostItem, PostReplyItem
from .task import Task

# Traversal enums
SHALLOW = 'Shallow'
SOFT_DELETED = 'SoftDeleted'
ASSOCIATED = 'Associated'
ITEM_TRAVERSAL_CHOICES = (SHALLOW, SOFT_DELETED, ASSOCIATED)

# Contacts search (ResolveNames) scope enums
ACTIVE_DIRECTORY = 'ActiveDirectory'
ACTIVE_DIRECTORY_CONTACTS = 'ActiveDirectoryContacts'
CONTACTS = 'Contacts'
CONTACTS_ACTIVE_DIRECTORY = 'ContactsActiveDirectory'
SEARCH_SCOPE_CHOICES = (ACTIVE_DIRECTORY, ACTIVE_DIRECTORY_CONTACTS, CONTACTS, CONTACTS_ACTIVE_DIRECTORY)


ITEM_CLASSES = (CalendarItem, Contact, DistributionList, Item, Message, MeetingMessage, MeetingRequest,
                MeetingResponse, MeetingCancellation, PostItem, Task)

__all__ = [
    'RegisterMixIn', 'MESSAGE_DISPOSITION_CHOICES', 'SAVE_ONLY', 'SEND_ONLY', 'SEND_AND_SAVE_COPY',
    'CalendarItem', 'AcceptItem', 'TentativelyAcceptItem', 'DeclineItem', 'CancelCalendarItem',
    'MeetingRequest', 'MeetingResponse', 'MeetingCancellation', 'CONFERENCE_TYPES',
    'Contact', 'Persona', 'DistributionList',
    'SEND_MEETING_INVITATIONS_CHOICES', 'SEND_TO_NONE', 'SEND_ONLY_TO_ALL', 'SEND_TO_ALL_AND_SAVE_COPY',
    'SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES', 'SEND_ONLY_TO_CHANGED', 'SEND_TO_CHANGED_AND_SAVE_COPY',
    'SEND_MEETING_CANCELLATIONS_CHOICES', 'AFFECTED_TASK_OCCURRENCES_CHOICES', 'ALL_OCCURRENCIES',
    'SPECIFIED_OCCURRENCE_ONLY', 'CONFLICT_RESOLUTION_CHOICES', 'NEVER_OVERWRITE', 'AUTO_RESOLVE', 'ALWAYS_OVERWRITE',
    'DELETE_TYPE_CHOICES', 'HARD_DELETE', 'SOFT_DELETE', 'MOVE_TO_DELETED_ITEMS', 'BaseItem', 'Item',
    'BulkCreateResult',
    'Message', 'ReplyToItem', 'ReplyAllToItem', 'ForwardItem',
    'PostItem', 'PostReplyItem',
    'Task',
    'ITEM_TRAVERSAL_CHOICES', 'SHALLOW', 'SOFT_DELETED', 'ASSOCIATED',
    'SHAPE_CHOICES', 'ID_ONLY', 'DEFAULT', 'ALL_PROPERTIES',
    'SEARCH_SCOPE_CHOICES', 'ACTIVE_DIRECTORY', 'ACTIVE_DIRECTORY_CONTACTS', 'CONTACTS', 'CONTACTS_ACTIVE_DIRECTORY',
    'ITEM_CLASSES',
]

Sub-modules

exchangelib.items.base
exchangelib.items.calendar_item
exchangelib.items.contact
exchangelib.items.item
exchangelib.items.message
exchangelib.items.post
exchangelib.items.task

Classes

class AcceptItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/acceptitem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class AcceptItem(BaseMeetingReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/acceptitem"""

    ELEMENT_NAME = 'AcceptItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class BaseItem (**kwargs)

Base class for all other classes that implement EWS items.

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class BaseItem(RegisterMixIn, metaclass=EWSMeta):
    """Base class for all other classes that implement EWS items."""

    ID_ELEMENT_CLS = ItemId
    _id = IdElementField(field_uri='item:ItemId', value_cls=ID_ELEMENT_CLS)

    __slots__ = 'account', 'folder'

    def __init__(self, **kwargs):
        """Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

        :param kwargs:
            'account' is optional but allows calling 'send()' and 'delete()'
            'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set,
                we use folder.account.
        """
        from ..folders import BaseFolder
        from ..account import Account
        self.account = kwargs.pop('account', None)
        if self.account is not None and not isinstance(self.account, Account):
            raise ValueError("'account' %r must be an Account instance" % self.account)
        self.folder = kwargs.pop('folder', None)
        if self.folder is not None:
            if not isinstance(self.folder, BaseFolder):
                raise ValueError("'folder' %r must be a Folder instance" % self.folder)
            if self.folder.account is not None:
                if self.account is not None:
                    # Make sure the account from kwargs matches the folder account
                    if self.account != self.folder.account:
                        raise ValueError("'account' does not match 'folder.account'")
                self.account = self.folder.account
        super().__init__(**kwargs)

    @classmethod
    def from_xml(cls, elem, account):
        item = super().from_xml(elem=elem, account=account)
        item.account = account
        return item

Ancestors

Subclasses

Class variables

var FIELDS
var ID_ELEMENT_CLS

'id' and 'changekey' are UUIDs generated by Exchange.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/itemid

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    item = super().from_xml(elem=elem, account=account)
    item.account = account
    return item

Instance variables

var account

Return an attribute of instance, which is of type owner.

var folder

Return an attribute of instance, which is of type owner.

Inherited members

class BulkCreateResult (**kwargs)

A dummy class to store return values from a CreateItem service call.

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class BulkCreateResult(BaseItem):
    """A dummy class to store return values from a CreateItem service call."""

    attachments = AttachmentField(field_uri='item:Attachments')  # ItemAttachment or FileAttachment

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if self.attachments is None:
            self.attachments = []

Ancestors

Class variables

var FIELDS

Instance variables

var attachments

Inherited members

class CalendarItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendaritem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class CalendarItem(Item, AcceptDeclineMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendaritem"""

    ELEMENT_NAME = 'CalendarItem'

    uid = TextField(field_uri='calendar:UID', is_required_after_save=True, is_searchable=False)
    recurrence_id = DateTimeField(field_uri='calendar:RecurrenceId', is_read_only=True)
    start = DateOrDateTimeField(field_uri='calendar:Start', is_required=True)
    end = DateOrDateTimeField(field_uri='calendar:End', is_required=True)
    original_start = DateTimeField(field_uri='calendar:OriginalStart', is_read_only=True)
    is_all_day = BooleanField(field_uri='calendar:IsAllDayEvent', is_required=True, default=False)
    legacy_free_busy_status = FreeBusyStatusField(field_uri='calendar:LegacyFreeBusyStatus', is_required=True,
                                                  default='Busy')
    location = TextField(field_uri='calendar:Location')
    when = TextField(field_uri='calendar:When')
    is_meeting = BooleanField(field_uri='calendar:IsMeeting', is_read_only=True)
    is_cancelled = BooleanField(field_uri='calendar:IsCancelled', is_read_only=True)
    is_recurring = BooleanField(field_uri='calendar:IsRecurring', is_read_only=True)
    meeting_request_was_sent = BooleanField(field_uri='calendar:MeetingRequestWasSent', is_read_only=True)
    is_response_requested = BooleanField(field_uri='calendar:IsResponseRequested', default=None,
                                         is_required_after_save=True, is_searchable=False)
    type = ChoiceField(field_uri='calendar:CalendarItemType', choices={Choice(c) for c in CALENDAR_ITEM_CHOICES},
                       is_read_only=True)
    my_response_type = ChoiceField(field_uri='calendar:MyResponseType', choices={
            Choice(c) for c in Attendee.RESPONSE_TYPES
    }, is_read_only=True)
    organizer = MailboxField(field_uri='calendar:Organizer', is_read_only=True)
    required_attendees = AttendeesField(field_uri='calendar:RequiredAttendees', is_searchable=False)
    optional_attendees = AttendeesField(field_uri='calendar:OptionalAttendees', is_searchable=False)
    resources = AttendeesField(field_uri='calendar:Resources', is_searchable=False)
    conflicting_meeting_count = IntegerField(field_uri='calendar:ConflictingMeetingCount', is_read_only=True)
    adjacent_meeting_count = IntegerField(field_uri='calendar:AdjacentMeetingCount', is_read_only=True)
    conflicting_meetings = EWSElementListField(field_uri='calendar:ConflictingMeetings', value_cls='CalendarItem',
                                               namespace=Item.NAMESPACE, is_read_only=True)
    adjacent_meetings = EWSElementListField(field_uri='calendar:AdjacentMeetings', value_cls='CalendarItem',
                                            namespace=Item.NAMESPACE, is_read_only=True)
    duration = CharField(field_uri='calendar:Duration', is_read_only=True)
    appointment_reply_time = DateTimeField(field_uri='calendar:AppointmentReplyTime', is_read_only=True)
    appointment_sequence_number = IntegerField(field_uri='calendar:AppointmentSequenceNumber', is_read_only=True)
    appointment_state = AppointmentStateField(field_uri='calendar:AppointmentState', is_read_only=True)
    recurrence = RecurrenceField(field_uri='calendar:Recurrence', is_searchable=False)
    first_occurrence = OccurrenceField(field_uri='calendar:FirstOccurrence', value_cls=FirstOccurrence,
                                       is_read_only=True)
    last_occurrence = OccurrenceField(field_uri='calendar:LastOccurrence', value_cls=LastOccurrence,
                                      is_read_only=True)
    modified_occurrences = OccurrenceListField(field_uri='calendar:ModifiedOccurrences', value_cls=Occurrence,
                                               is_read_only=True)
    deleted_occurrences = OccurrenceListField(field_uri='calendar:DeletedOccurrences', value_cls=DeletedOccurrence,
                                              is_read_only=True)
    _meeting_timezone = TimeZoneField(field_uri='calendar:MeetingTimeZone', deprecated_from=EXCHANGE_2010,
                                      is_searchable=False)
    _start_timezone = TimeZoneField(field_uri='calendar:StartTimeZone', supported_from=EXCHANGE_2010,
                                    is_searchable=False)
    _end_timezone = TimeZoneField(field_uri='calendar:EndTimeZone', supported_from=EXCHANGE_2010,
                                  is_searchable=False)
    conference_type = EnumAsIntField(field_uri='calendar:ConferenceType', enum=CONFERENCE_TYPES, min=0,
                                     default=None, is_required_after_save=True)
    allow_new_time_proposal = BooleanField(field_uri='calendar:AllowNewTimeProposal', default=None,
                                           is_required_after_save=True, is_searchable=False)
    is_online_meeting = BooleanField(field_uri='calendar:IsOnlineMeeting', default=None,
                                     is_read_only=True)
    meeting_workspace_url = URIField(field_uri='calendar:MeetingWorkspaceUrl')
    net_show_url = URIField(field_uri='calendar:NetShowUrl')

    def occurrence(self, index):
        """Get an occurrence of a recurring master by index. No query is sent to the server to actually fetch the item.
        Call refresh() on the item do do so.

        Only call this method on a recurring master.

        :param index: The index, which is 1-based

        :return The occurrence
        """
        return self.__class__(
            account=self.account,
            folder=self.folder,
            _id=OccurrenceItemId(id=self.id, changekey=self.changekey, instance_index=index),
        )

    def recurring_master(self):
        """Get the recurring master of an occurrence. No query is sent to the server to actually fetch the item.
        Call refresh() on the item do do so.

        Only call this method on an occurrence of a recurring master.

        :return: The master occurrence
        """
        return self.__class__(
            account=self.account,
            folder=self.folder,
            _id=RecurringMasterItemId(id=self.id, changekey=self.changekey),
        )

    @classmethod
    def timezone_fields(cls):
        return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)]

    def clean_timezone_fields(self, version):
        # Sets proper values on the timezone fields if they are not already set
        if self.start is None:
            start_tz = None
        elif type(self.start) in (EWSDate, datetime.date):
            start_tz = self.account.default_timezone
        else:
            start_tz = self.start.tzinfo
        if self.end is None:
            end_tz = None
        elif type(self.end) in (EWSDate, datetime.date):
            end_tz = self.account.default_timezone
        else:
            end_tz = self.end.tzinfo
        if version.build < EXCHANGE_2010:
            if self._meeting_timezone is None:
                self._meeting_timezone = start_tz
            self._start_timezone = None
            self._end_timezone = None
        else:
            self._meeting_timezone = None
            if self._start_timezone is None:
                self._start_timezone = start_tz
            if self._end_timezone is None:
                self._end_timezone = end_tz

    def clean(self, version=None):
        super().clean(version=version)
        if self.start and self.end and self.end < self.start:
            raise ValueError("'end' must be greater than 'start' (%s -> %s)" % (self.start, self.end))
        if version:
            self.clean_timezone_fields(version=version)

    def cancel(self, **kwargs):
        return CancelCalendarItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            **kwargs
        ).send()

    def _update_fieldnames(self):
        update_fields = super()._update_fieldnames()
        if self.type == OCCURRENCE:
            # Some CalendarItem fields cannot be updated when the item is an occurrence. The values are empty when we
            # receive them so would have been updated because they are set to None.
            update_fields.remove('recurrence')
            update_fields.remove('uid')
        return update_fields

    @classmethod
    def from_xml(cls, elem, account):
        item = super().from_xml(elem=elem, account=account)
        # EWS returns the start and end values as a datetime regardless of the is_all_day status. Convert to date if
        # applicable.
        if not item.is_all_day:
            return item
        for field_name in ('start', 'end'):
            val = getattr(item, field_name)
            if val is None:
                continue
            # Return just the date part of the value. Subtract 1 day from the date if this is the end field. This is
            # the inverse of what we do in .to_xml(). Convert to the local timezone before getting the date.
            if field_name == 'end':
                val -= datetime.timedelta(days=1)
            tz = getattr(item, '_%s_timezone' % field_name)
            setattr(item, field_name, val.astimezone(tz).date())
        return item

    def tz_field_for_field_name(self, field_name):
        meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
        if self.account.version.build < EXCHANGE_2010:
            return meeting_tz_field
        if field_name == 'start':
            return start_tz_field
        if field_name == 'end':
            return end_tz_field
        raise ValueError('Unsupported field_name')

    def date_to_datetime(self, field_name):
        # EWS always expects a datetime. If we have a date value, then convert it to datetime in the local
        # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both
        # start and end values and let EWS apply its logic, but that seems hacky.
        value = getattr(self, field_name)
        tz = getattr(self, self.tz_field_for_field_name(field_name).name)
        value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz)
        if field_name == 'end':
            value += datetime.timedelta(days=1)
        return value

    def to_xml(self, version):
        # EWS has some special logic related to all-day start and end values. Non-midnight start values are pushed to
        # the previous midnight. Non-midnight end values are pushed to the following midnight. Midnight in this context
        # refers to midnight in the local timezone. See
        #
        # https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-create-all-day-events-by-using-ews-in-exchange
        #
        elem = super().to_xml(version=version)
        if not self.is_all_day:
            return elem
        for field_name in ('start', 'end'):
            value = getattr(self, field_name)
            if value is None:
                continue
            if type(value) in (EWSDate, datetime.date):
                # EWS always expects a datetime
                value = self.date_to_datetime(field_name=field_name)
                # We already generated an XML element for this field, but it contains a plain date at this point, which
                # is invalid. Replace the value.
                field = self.get_field_by_fieldname(field_name)
                set_xml_value(elem=elem.find(field.response_tag()), value=value, version=version)
        return elem

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    item = super().from_xml(elem=elem, account=account)
    # EWS returns the start and end values as a datetime regardless of the is_all_day status. Convert to date if
    # applicable.
    if not item.is_all_day:
        return item
    for field_name in ('start', 'end'):
        val = getattr(item, field_name)
        if val is None:
            continue
        # Return just the date part of the value. Subtract 1 day from the date if this is the end field. This is
        # the inverse of what we do in .to_xml(). Convert to the local timezone before getting the date.
        if field_name == 'end':
            val -= datetime.timedelta(days=1)
        tz = getattr(item, '_%s_timezone' % field_name)
        setattr(item, field_name, val.astimezone(tz).date())
    return item
def timezone_fields()
Expand source code
@classmethod
def timezone_fields(cls):
    return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)]

Instance variables

var adjacent_meeting_count
var adjacent_meetings
var allow_new_time_proposal
var appointment_reply_time
var appointment_sequence_number
var appointment_state
var conference_type
var conflicting_meeting_count
var conflicting_meetings
var deleted_occurrences
var duration
var end
var first_occurrence
var is_all_day
var is_cancelled
var is_meeting
var is_online_meeting
var is_recurring
var is_response_requested
var last_occurrence
var legacy_free_busy_status
var location
var meeting_request_was_sent
var meeting_workspace_url
var modified_occurrences
var my_response_type
var net_show_url
var optional_attendees
var organizer
var original_start
var recurrence
var recurrence_id
var required_attendees
var resources
var start
var type
var uid
var when

Methods

def cancel(self, **kwargs)
Expand source code
def cancel(self, **kwargs):
    return CancelCalendarItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        **kwargs
    ).send()
def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)
    if self.start and self.end and self.end < self.start:
        raise ValueError("'end' must be greater than 'start' (%s -> %s)" % (self.start, self.end))
    if version:
        self.clean_timezone_fields(version=version)
def clean_timezone_fields(self, version)
Expand source code
def clean_timezone_fields(self, version):
    # Sets proper values on the timezone fields if they are not already set
    if self.start is None:
        start_tz = None
    elif type(self.start) in (EWSDate, datetime.date):
        start_tz = self.account.default_timezone
    else:
        start_tz = self.start.tzinfo
    if self.end is None:
        end_tz = None
    elif type(self.end) in (EWSDate, datetime.date):
        end_tz = self.account.default_timezone
    else:
        end_tz = self.end.tzinfo
    if version.build < EXCHANGE_2010:
        if self._meeting_timezone is None:
            self._meeting_timezone = start_tz
        self._start_timezone = None
        self._end_timezone = None
    else:
        self._meeting_timezone = None
        if self._start_timezone is None:
            self._start_timezone = start_tz
        if self._end_timezone is None:
            self._end_timezone = end_tz
def date_to_datetime(self, field_name)
Expand source code
def date_to_datetime(self, field_name):
    # EWS always expects a datetime. If we have a date value, then convert it to datetime in the local
    # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both
    # start and end values and let EWS apply its logic, but that seems hacky.
    value = getattr(self, field_name)
    tz = getattr(self, self.tz_field_for_field_name(field_name).name)
    value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz)
    if field_name == 'end':
        value += datetime.timedelta(days=1)
    return value
def occurrence(self, index)

Get an occurrence of a recurring master by index. No query is sent to the server to actually fetch the item. Call refresh() on the item do do so.

Only call this method on a recurring master.

:param index: The index, which is 1-based

:return The occurrence

Expand source code
def occurrence(self, index):
    """Get an occurrence of a recurring master by index. No query is sent to the server to actually fetch the item.
    Call refresh() on the item do do so.

    Only call this method on a recurring master.

    :param index: The index, which is 1-based

    :return The occurrence
    """
    return self.__class__(
        account=self.account,
        folder=self.folder,
        _id=OccurrenceItemId(id=self.id, changekey=self.changekey, instance_index=index),
    )
def recurring_master(self)

Get the recurring master of an occurrence. No query is sent to the server to actually fetch the item. Call refresh() on the item do do so.

Only call this method on an occurrence of a recurring master.

:return: The master occurrence

Expand source code
def recurring_master(self):
    """Get the recurring master of an occurrence. No query is sent to the server to actually fetch the item.
    Call refresh() on the item do do so.

    Only call this method on an occurrence of a recurring master.

    :return: The master occurrence
    """
    return self.__class__(
        account=self.account,
        folder=self.folder,
        _id=RecurringMasterItemId(id=self.id, changekey=self.changekey),
    )
def to_xml(self, version)
Expand source code
def to_xml(self, version):
    # EWS has some special logic related to all-day start and end values. Non-midnight start values are pushed to
    # the previous midnight. Non-midnight end values are pushed to the following midnight. Midnight in this context
    # refers to midnight in the local timezone. See
    #
    # https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-create-all-day-events-by-using-ews-in-exchange
    #
    elem = super().to_xml(version=version)
    if not self.is_all_day:
        return elem
    for field_name in ('start', 'end'):
        value = getattr(self, field_name)
        if value is None:
            continue
        if type(value) in (EWSDate, datetime.date):
            # EWS always expects a datetime
            value = self.date_to_datetime(field_name=field_name)
            # We already generated an XML element for this field, but it contains a plain date at this point, which
            # is invalid. Replace the value.
            field = self.get_field_by_fieldname(field_name)
            set_xml_value(elem=elem.find(field.response_tag()), value=value, version=version)
    return elem
def tz_field_for_field_name(self, field_name)
Expand source code
def tz_field_for_field_name(self, field_name):
    meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
    if self.account.version.build < EXCHANGE_2010:
        return meeting_tz_field
    if field_name == 'start':
        return start_tz_field
    if field_name == 'end':
        return end_tz_field
    raise ValueError('Unsupported field_name')

Inherited members

class CancelCalendarItem (**kwargs)
Expand source code
class CancelCalendarItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/cancelcalendaritem"""

    ELEMENT_NAME = 'CancelCalendarItem'
    author_idx = BaseReplyItem.FIELDS.index_by_name('author')
    FIELDS = BaseReplyItem.FIELDS[:author_idx] + BaseReplyItem.FIELDS[author_idx + 1:]

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var author_idx

Inherited members

class Contact (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/contact

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class Contact(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/contact"""

    ELEMENT_NAME = 'Contact'

    file_as = TextField(field_uri='contacts:FileAs')
    file_as_mapping = ChoiceField(field_uri='contacts:FileAsMapping', choices={
        Choice('None'), Choice('LastCommaFirst'), Choice('FirstSpaceLast'), Choice('Company'),
        Choice('LastCommaFirstCompany'), Choice('CompanyLastFirst'), Choice('LastFirst'),
        Choice('LastFirstCompany'), Choice('CompanyLastCommaFirst'), Choice('LastFirstSuffix'),
        Choice('LastSpaceFirstCompany'), Choice('CompanyLastSpaceFirst'), Choice('LastSpaceFirst'),
        Choice('DisplayName'), Choice('FirstName'), Choice('LastFirstMiddleSuffix'), Choice('LastName'),
        Choice('Empty'),
    })
    display_name = TextField(field_uri='contacts:DisplayName', is_required=True)
    given_name = CharField(field_uri='contacts:GivenName')
    initials = TextField(field_uri='contacts:Initials')
    middle_name = CharField(field_uri='contacts:MiddleName')
    nickname = TextField(field_uri='contacts:Nickname')
    complete_name = EWSElementField(field_uri='contacts:CompleteName', value_cls=CompleteName, is_read_only=True)
    company_name = TextField(field_uri='contacts:CompanyName')
    email_addresses = EmailAddressesField(field_uri='contacts:EmailAddress')
    physical_addresses = PhysicalAddressField(field_uri='contacts:PhysicalAddress')
    phone_numbers = PhoneNumberField(field_uri='contacts:PhoneNumber')
    assistant_name = TextField(field_uri='contacts:AssistantName')
    birthday = DateTimeBackedDateField(field_uri='contacts:Birthday', default_time=datetime.time(11, 59))
    business_homepage = URIField(field_uri='contacts:BusinessHomePage')
    children = TextListField(field_uri='contacts:Children')
    companies = TextListField(field_uri='contacts:Companies', is_searchable=False)
    contact_source = ChoiceField(field_uri='contacts:ContactSource', choices={
        Choice('Store'), Choice('ActiveDirectory')
    }, is_read_only=True)
    department = TextField(field_uri='contacts:Department')
    generation = TextField(field_uri='contacts:Generation')
    im_addresses = CharField(field_uri='contacts:ImAddresses', is_read_only=True)
    job_title = TextField(field_uri='contacts:JobTitle')
    manager = TextField(field_uri='contacts:Manager')
    mileage = TextField(field_uri='contacts:Mileage')
    office = TextField(field_uri='contacts:OfficeLocation')
    postal_address_index = ChoiceField(field_uri='contacts:PostalAddressIndex', choices={
        Choice('Business'), Choice('Home'), Choice('Other'), Choice('None')
    }, default='None', is_required_after_save=True)
    profession = TextField(field_uri='contacts:Profession')
    spouse_name = TextField(field_uri='contacts:SpouseName')
    surname = CharField(field_uri='contacts:Surname')
    wedding_anniversary = DateTimeBackedDateField(field_uri='contacts:WeddingAnniversary',
                                                  default_time=datetime.time(11, 59))
    has_picture = BooleanField(field_uri='contacts:HasPicture', supported_from=EXCHANGE_2010, is_read_only=True)
    phonetic_full_name = TextField(field_uri='contacts:PhoneticFullName', supported_from=EXCHANGE_2010_SP2,
                                   is_read_only=True)
    phonetic_first_name = TextField(field_uri='contacts:PhoneticFirstName', supported_from=EXCHANGE_2010_SP2,
                                    is_read_only=True)
    phonetic_last_name = TextField(field_uri='contacts:PhoneticLastName', supported_from=EXCHANGE_2010_SP2,
                                   is_read_only=True)
    email_alias = EmailAddressField(field_uri='contacts:Alias', is_read_only=True,
                                    supported_from=EXCHANGE_2010_SP2)
    # 'notes' is documented in MSDN but apparently unused. Writing to it raises ErrorInvalidPropertyRequest. OWA
    # put entries into the 'notes' form field into the 'body' field.
    notes = CharField(field_uri='contacts:Notes', supported_from=EXCHANGE_2010_SP2, is_read_only=True)
    # 'photo' is documented in MSDN but apparently unused. Writing to it raises ErrorInvalidPropertyRequest. OWA
    # adds photos as FileAttachments on the contact item (with 'is_contact_photo=True'), which automatically flips
    # the 'has_picture' field.
    photo = Base64Field(field_uri='contacts:Photo', supported_from=EXCHANGE_2010_SP2, is_read_only=True)
    user_smime_certificate = Base64Field(field_uri='contacts:UserSMIMECertificate', supported_from=EXCHANGE_2010_SP2,
                                         is_read_only=True)
    ms_exchange_certificate = Base64Field(field_uri='contacts:MSExchangeCertificate', supported_from=EXCHANGE_2010_SP2,
                                          is_read_only=True)
    directory_id = TextField(field_uri='contacts:DirectoryId', supported_from=EXCHANGE_2010_SP2, is_read_only=True)
    manager_mailbox = MailboxField(field_uri='contacts:ManagerMailbox', supported_from=EXCHANGE_2010_SP2,
                                   is_read_only=True)
    direct_reports = MailboxListField(field_uri='contacts:DirectReports', supported_from=EXCHANGE_2010_SP2,
                                      is_read_only=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var assistant_name
var birthday
var business_homepage
var children
var companies
var company_name
var complete_name
var contact_source
var department
var direct_reports
var directory_id
var display_name
var email_addresses
var email_alias
var file_as
var file_as_mapping
var generation
var given_name
var has_picture
var im_addresses
var initials
var job_title
var manager
var manager_mailbox
var middle_name
var mileage
var ms_exchange_certificate
var nickname
var notes
var office
var phone_numbers
var phonetic_first_name
var phonetic_full_name
var phonetic_last_name
var photo
var physical_addresses
var postal_address_index
var profession
var spouse_name
var surname
var user_smime_certificate
var wedding_anniversary

Inherited members

class DeclineItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/declineitem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class DeclineItem(BaseMeetingReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/declineitem"""

    ELEMENT_NAME = 'DeclineItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class DistributionList (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distributionlist

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class DistributionList(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distributionlist"""

    ELEMENT_NAME = 'DistributionList'

    display_name = CharField(field_uri='contacts:DisplayName', is_required=True)
    file_as = CharField(field_uri='contacts:FileAs', is_read_only=True)
    contact_source = ChoiceField(field_uri='contacts:ContactSource', choices={
        Choice('Store'), Choice('ActiveDirectory')
    }, is_read_only=True)
    members = MemberListField(field_uri='distributionlist:Members')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var contact_source
var display_name
var file_as
var members

Inherited members

class ForwardItem (**kwargs)
Expand source code
class ForwardItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/forwarditem"""

    ELEMENT_NAME = 'ForwardItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class Item (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class Item(BaseItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item"""

    ELEMENT_NAME = 'Item'

    mime_content = MimeContentField(field_uri='item:MimeContent', is_read_only_after_send=True)
    _id = BaseItem.FIELDS['_id']
    parent_folder_id = EWSElementField(field_uri='item:ParentFolderId', value_cls=ParentFolderId, is_read_only=True)
    item_class = CharField(field_uri='item:ItemClass', is_read_only=True)
    subject = CharField(field_uri='item:Subject')
    sensitivity = ChoiceField(field_uri='item:Sensitivity', choices={
        Choice('Normal'), Choice('Personal'), Choice('Private'), Choice('Confidential')
    }, is_required=True, default='Normal')
    text_body = TextField(field_uri='item:TextBody', is_read_only=True, supported_from=EXCHANGE_2013)
    body = BodyField(field_uri='item:Body')  # Accepts and returns Body or HTMLBody instances
    attachments = AttachmentField(field_uri='item:Attachments')  # ItemAttachment or FileAttachment
    datetime_received = DateTimeField(field_uri='item:DateTimeReceived', is_read_only=True)
    size = IntegerField(field_uri='item:Size', is_read_only=True)  # Item size in bytes
    categories = CharListField(field_uri='item:Categories')
    importance = ChoiceField(field_uri='item:Importance', choices={
        Choice('Low'), Choice('Normal'), Choice('High')
    }, is_required=True, default='Normal')
    in_reply_to = TextField(field_uri='item:InReplyTo')
    is_submitted = BooleanField(field_uri='item:IsSubmitted', is_read_only=True)
    is_draft = BooleanField(field_uri='item:IsDraft', is_read_only=True)
    is_from_me = BooleanField(field_uri='item:IsFromMe', is_read_only=True)
    is_resend = BooleanField(field_uri='item:IsResend', is_read_only=True)
    is_unmodified = BooleanField(field_uri='item:IsUnmodified', is_read_only=True)
    headers = MessageHeaderField(field_uri='item:InternetMessageHeaders', is_read_only=True)
    datetime_sent = DateTimeField(field_uri='item:DateTimeSent', is_read_only=True)
    datetime_created = DateTimeField(field_uri='item:DateTimeCreated', is_read_only=True)
    response_objects = EWSElementField(field_uri='item:ResponseObjects', value_cls=ResponseObjects,
                                       is_read_only=True,)
    # Placeholder for ResponseObjects
    reminder_due_by = DateTimeField(field_uri='item:ReminderDueBy', is_required_after_save=True, is_searchable=False)
    reminder_is_set = BooleanField(field_uri='item:ReminderIsSet', is_required=True, default=False)
    reminder_minutes_before_start = IntegerField(field_uri='item:ReminderMinutesBeforeStart',
                                                 is_required_after_save=True, min=0, default=0)
    display_cc = TextField(field_uri='item:DisplayCc', is_read_only=True)
    display_to = TextField(field_uri='item:DisplayTo', is_read_only=True)
    has_attachments = BooleanField(field_uri='item:HasAttachments', is_read_only=True)
    # ExtendedProperty fields go here
    culture = CultureField(field_uri='item:Culture', is_required_after_save=True, is_searchable=False)
    effective_rights = EffectiveRightsField(field_uri='item:EffectiveRights', is_read_only=True)
    last_modified_name = CharField(field_uri='item:LastModifiedName', is_read_only=True)
    last_modified_time = DateTimeField(field_uri='item:LastModifiedTime', is_read_only=True)
    is_associated = BooleanField(field_uri='item:IsAssociated', is_read_only=True, supported_from=EXCHANGE_2010)
    web_client_read_form_query_string = URIField(field_uri='item:WebClientReadFormQueryString', is_read_only=True,
                                                 supported_from=EXCHANGE_2010)
    web_client_edit_form_query_string = URIField(field_uri='item:WebClientEditFormQueryString', is_read_only=True,
                                                 supported_from=EXCHANGE_2010)
    conversation_id = EWSElementField(field_uri='item:ConversationId', value_cls=ConversationId,
                                      is_read_only=True, supported_from=EXCHANGE_2010)
    unique_body = BodyField(field_uri='item:UniqueBody', is_read_only=True, supported_from=EXCHANGE_2010)

    FIELDS = Fields()

    # Used to register extended properties
    INSERT_AFTER_FIELD = 'has_attachments'

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if self.attachments:
            for a in self.attachments:
                if a.parent_item:
                    if a.parent_item is not self:
                        raise ValueError("'parent_item' of attachment %s must point to this item" % a)
                else:
                    a.parent_item = self
                self.attach(self.attachments)
        else:
            self.attachments = []

    def save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE):
        from .task import Task
        if self.id:
            item_id, changekey = self._update(
                update_fieldnames=update_fields,
                message_disposition=SAVE_ONLY,
                conflict_resolution=conflict_resolution,
                send_meeting_invitations=send_meeting_invitations
            )
            if self.id != item_id \
                    and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)) \
                    and not isinstance(self, Task):
                # When we update an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so
                # the ID of this item changes.
                #
                # When we update certain fields on a task, the ID may change. A full description is available at
                # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation-task
                raise ValueError("'id' mismatch in returned update response")
            # Don't check that changekeys are different. No-op saves will sometimes leave the changekey intact
            self._id = self.ID_ELEMENT_CLS(item_id, changekey)
        else:
            if update_fields:
                raise ValueError("'update_fields' is only valid for updates")
            tmp_attachments = None
            if self.account and self.account.version.build < EXCHANGE_2013 and self.attachments:
                # At least some versions prior to Exchange 2013 can't save attachments immediately. You need to first
                # save, then attach. Store the attachment of this item temporarily and attach later.
                tmp_attachments, self.attachments = self.attachments, []
            item = self._create(message_disposition=SAVE_ONLY, send_meeting_invitations=send_meeting_invitations)
            self._id = self.ID_ELEMENT_CLS(item.id, item.changekey)
            for old_att, new_att in zip(self.attachments, item.attachments):
                if old_att.attachment_id is not None:
                    raise ValueError("Old 'attachment_id' is not empty")
                if new_att.attachment_id is None:
                    raise ValueError("New 'attachment_id' is empty")
                old_att.attachment_id = new_att.attachment_id
            if tmp_attachments:
                # Exchange 2007 workaround. See above
                self.attach(tmp_attachments)
        return self

    @require_account
    def _create(self, message_disposition, send_meeting_invitations):
        # Return a BulkCreateResult because we want to return the ID of both the main item *and* attachments. In send
        # and send-and-save-copy mode, the server does not return an ID, so we just return True.
        return CreateItem(account=self.account).get(
            items=[self],
            folder=self.folder,
            message_disposition=message_disposition,
            send_meeting_invitations=send_meeting_invitations,
        )

    def _update_fieldnames(self):
        from .contact import Contact, DistributionList
        # Return the list of fields we are allowed to update
        update_fieldnames = []
        for f in self.supported_fields(version=self.account.version):
            if f.name == 'attachments':
                # Attachments are handled separately after item creation
                continue
            if f.is_read_only:
                # These cannot be changed
                continue
            if (f.is_required or f.is_required_after_save) and (
                    getattr(self, f.name) is None or (f.is_list and not getattr(self, f.name))
            ):
                # These are required and cannot be deleted
                continue
            if not self.is_draft and f.is_read_only_after_send:
                # These cannot be changed when the item is no longer a draft
                continue
            if f.name == 'message_id' and f.is_read_only_after_send:
                # 'message_id' doesn't support updating, no matter the draft status
                continue
            if f.name == 'mime_content' and isinstance(self, (Contact, DistributionList)):
                # Contact and DistributionList don't support updating mime_content, no matter the draft status
                continue
            update_fieldnames.append(f.name)
        return update_fieldnames

    @require_account
    def _update(self, update_fieldnames, message_disposition, conflict_resolution, send_meeting_invitations):
        if not self.changekey:
            raise ValueError('%s must have changekey' % self.__class__.__name__)
        if not update_fieldnames:
            # The fields to update was not specified explicitly. Update all fields where update is possible
            update_fieldnames = self._update_fieldnames()
        return UpdateItem(account=self.account).get(
            items=[(self, update_fieldnames)],
            message_disposition=message_disposition,
            conflict_resolution=conflict_resolution,
            send_meeting_invitations_or_cancellations=send_meeting_invitations,
            suppress_read_receipts=True,
            expect_result=message_disposition != SEND_AND_SAVE_COPY,
        )

    @require_id
    def refresh(self):
        # Updates the item based on fresh data from EWS
        from ..folders import Folder
        additional_fields = {
            FieldPath(field=f) for f in Folder(root=self.account.root).allowed_item_fields(version=self.account.version)
        }
        res = GetItem(account=self.account).get(items=[self], additional_fields=additional_fields, shape=ID_ONLY)
        if self.id != res.id and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)):
            # When we refresh an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so
            # the ID of this item changes.
            raise ValueError("'id' mismatch in returned update response")
        for f in self.FIELDS:
            setattr(self, f.name, getattr(res, f.name))
        # 'parent_item' should point to 'self', not 'fresh_item'. That way, 'fresh_item' can be garbage collected.
        for a in self.attachments:
            a.parent_item = self
        return self

    @require_id
    def copy(self, to_folder):
        # If 'to_folder' is a public folder or a folder in a different mailbox then None is returned
        return CopyItem(account=self.account).get(
            items=[self],
            to_folder=to_folder,
            expect_result=None,
        )

    @require_id
    def move(self, to_folder):
        res = MoveItem(account=self.account).get(
            items=[self],
            to_folder=to_folder,
            expect_result=None,
        )
        if res is None:
            # Assume 'to_folder' is a public folder or a folder in a different mailbox
            self._id = None
            return
        self._id = self.ID_ELEMENT_CLS(*res)
        self.folder = to_folder

    def move_to_trash(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES,
                      suppress_read_receipts=True):
        # Delete and move to the trash folder.
        self._delete(delete_type=MOVE_TO_DELETED_ITEMS, send_meeting_cancellations=send_meeting_cancellations,
                     affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts)
        self._id = None
        self.folder = self.account.trash

    def soft_delete(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES,
                    suppress_read_receipts=True):
        # Delete and move to the dumpster, if it is enabled.
        self._delete(delete_type=SOFT_DELETE, send_meeting_cancellations=send_meeting_cancellations,
                     affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts)
        self._id = None
        self.folder = self.account.recoverable_items_deletions

    def delete(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES,
               suppress_read_receipts=True):
        # Remove the item permanently. No copies are stored anywhere.
        self._delete(delete_type=HARD_DELETE, send_meeting_cancellations=send_meeting_cancellations,
                     affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts)
        self._id, self.folder = None, None

    @require_id
    def _delete(self, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts):
        DeleteItem(account=self.account).get(
            items=[self],
            delete_type=delete_type,
            send_meeting_cancellations=send_meeting_cancellations,
            affected_task_occurrences=affected_task_occurrences,
            suppress_read_receipts=suppress_read_receipts,
        )

    @require_id
    def archive(self, to_folder):
        return ArchiveItem(account=self.account).get(items=[self], to_folder=to_folder, expect_result=True)

    def attach(self, attachments):
        """Add an attachment, or a list of attachments, to this item. If the item has already been saved, the
        attachments will be created on the server immediately. If the item has not yet been saved, the attachments will
        be created on the server when the item is saved.

        Adding attachments to an existing item will update the changekey of the item.

        :param attachments:
        """
        if not is_iterable(attachments, generators_allowed=True):
            attachments = [attachments]
        for a in attachments:
            if not a.parent_item:
                a.parent_item = self
            if self.id and not a.attachment_id:
                # Already saved object. Attach the attachment server-side now
                a.attach()
            if a not in self.attachments:
                self.attachments.append(a)

    def detach(self, attachments):
        """Remove an attachment, or a list of attachments, from this item. If the item has already been saved, the
        attachments will be deleted on the server immediately. If the item has not yet been saved, the attachments will
        simply not be created on the server the item is saved.

        Removing attachments from an existing item will update the changekey of the item.

        :param attachments:
        """
        if not is_iterable(attachments, generators_allowed=True):
            attachments = [attachments]
        if attachments is self.attachments:
            # Don't remove from the same list we are iterating
            attachments = list(attachments)
        for a in attachments:
            if a.parent_item is not self:
                raise ValueError('Attachment does not belong to this item')
            if self.id:
                # Item is already created. Detach  the attachment server-side now
                a.detach()
            if a in self.attachments:
                self.attachments.remove(a)

    @require_id
    def create_forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None):
        from .message import ForwardItem
        return ForwardItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            subject=subject,
            new_body=body,
            to_recipients=to_recipients,
            cc_recipients=cc_recipients,
            bcc_recipients=bcc_recipients,
        )

    def forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None):
        self.create_forward(
            subject,
            body,
            to_recipients,
            cc_recipients,
            bcc_recipients,
        ).send()

Ancestors

Subclasses

Class variables

var ELEMENT_NAME
var FIELDS
var INSERT_AFTER_FIELD

Instance variables

var attachments
var body
var categories
var conversation_id
var culture
var datetime_created
var datetime_received
var datetime_sent
var display_cc
var display_to
var effective_rights
var has_attachments
var headers
var importance
var in_reply_to
var is_associated
var is_draft
var is_from_me
var is_resend
var is_submitted
var is_unmodified
var item_class
var last_modified_name
var last_modified_time
var mime_content
var parent_folder_id
var reminder_due_by
var reminder_is_set
var reminder_minutes_before_start
var response_objects
var sensitivity
var size
var subject
var text_body
var unique_body
var web_client_edit_form_query_string
var web_client_read_form_query_string

Methods

def archive(self, to_folder)
Expand source code
@require_id
def archive(self, to_folder):
    return ArchiveItem(account=self.account).get(items=[self], to_folder=to_folder, expect_result=True)
def attach(self, attachments)

Add an attachment, or a list of attachments, to this item. If the item has already been saved, the attachments will be created on the server immediately. If the item has not yet been saved, the attachments will be created on the server when the item is saved.

Adding attachments to an existing item will update the changekey of the item.

:param attachments:

Expand source code
def attach(self, attachments):
    """Add an attachment, or a list of attachments, to this item. If the item has already been saved, the
    attachments will be created on the server immediately. If the item has not yet been saved, the attachments will
    be created on the server when the item is saved.

    Adding attachments to an existing item will update the changekey of the item.

    :param attachments:
    """
    if not is_iterable(attachments, generators_allowed=True):
        attachments = [attachments]
    for a in attachments:
        if not a.parent_item:
            a.parent_item = self
        if self.id and not a.attachment_id:
            # Already saved object. Attach the attachment server-side now
            a.attach()
        if a not in self.attachments:
            self.attachments.append(a)
def copy(self, to_folder)
Expand source code
@require_id
def copy(self, to_folder):
    # If 'to_folder' is a public folder or a folder in a different mailbox then None is returned
    return CopyItem(account=self.account).get(
        items=[self],
        to_folder=to_folder,
        expect_result=None,
    )
def create_forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None)
Expand source code
@require_id
def create_forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None):
    from .message import ForwardItem
    return ForwardItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        subject=subject,
        new_body=body,
        to_recipients=to_recipients,
        cc_recipients=cc_recipients,
        bcc_recipients=bcc_recipients,
    )
def delete(self, send_meeting_cancellations='SendToNone', affected_task_occurrences='AllOccurrences', suppress_read_receipts=True)
Expand source code
def delete(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES,
           suppress_read_receipts=True):
    # Remove the item permanently. No copies are stored anywhere.
    self._delete(delete_type=HARD_DELETE, send_meeting_cancellations=send_meeting_cancellations,
                 affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts)
    self._id, self.folder = None, None
def detach(self, attachments)

Remove an attachment, or a list of attachments, from this item. If the item has already been saved, the attachments will be deleted on the server immediately. If the item has not yet been saved, the attachments will simply not be created on the server the item is saved.

Removing attachments from an existing item will update the changekey of the item.

:param attachments:

Expand source code
def detach(self, attachments):
    """Remove an attachment, or a list of attachments, from this item. If the item has already been saved, the
    attachments will be deleted on the server immediately. If the item has not yet been saved, the attachments will
    simply not be created on the server the item is saved.

    Removing attachments from an existing item will update the changekey of the item.

    :param attachments:
    """
    if not is_iterable(attachments, generators_allowed=True):
        attachments = [attachments]
    if attachments is self.attachments:
        # Don't remove from the same list we are iterating
        attachments = list(attachments)
    for a in attachments:
        if a.parent_item is not self:
            raise ValueError('Attachment does not belong to this item')
        if self.id:
            # Item is already created. Detach  the attachment server-side now
            a.detach()
        if a in self.attachments:
            self.attachments.remove(a)
def forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None)
Expand source code
def forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None):
    self.create_forward(
        subject,
        body,
        to_recipients,
        cc_recipients,
        bcc_recipients,
    ).send()
def move(self, to_folder)
Expand source code
@require_id
def move(self, to_folder):
    res = MoveItem(account=self.account).get(
        items=[self],
        to_folder=to_folder,
        expect_result=None,
    )
    if res is None:
        # Assume 'to_folder' is a public folder or a folder in a different mailbox
        self._id = None
        return
    self._id = self.ID_ELEMENT_CLS(*res)
    self.folder = to_folder
def move_to_trash(self, send_meeting_cancellations='SendToNone', affected_task_occurrences='AllOccurrences', suppress_read_receipts=True)
Expand source code
def move_to_trash(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES,
                  suppress_read_receipts=True):
    # Delete and move to the trash folder.
    self._delete(delete_type=MOVE_TO_DELETED_ITEMS, send_meeting_cancellations=send_meeting_cancellations,
                 affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts)
    self._id = None
    self.folder = self.account.trash
def refresh(self)
Expand source code
@require_id
def refresh(self):
    # Updates the item based on fresh data from EWS
    from ..folders import Folder
    additional_fields = {
        FieldPath(field=f) for f in Folder(root=self.account.root).allowed_item_fields(version=self.account.version)
    }
    res = GetItem(account=self.account).get(items=[self], additional_fields=additional_fields, shape=ID_ONLY)
    if self.id != res.id and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)):
        # When we refresh an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so
        # the ID of this item changes.
        raise ValueError("'id' mismatch in returned update response")
    for f in self.FIELDS:
        setattr(self, f.name, getattr(res, f.name))
    # 'parent_item' should point to 'self', not 'fresh_item'. That way, 'fresh_item' can be garbage collected.
    for a in self.attachments:
        a.parent_item = self
    return self
def save(self, update_fields=None, conflict_resolution='AutoResolve', send_meeting_invitations='SendToNone')
Expand source code
def save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE):
    from .task import Task
    if self.id:
        item_id, changekey = self._update(
            update_fieldnames=update_fields,
            message_disposition=SAVE_ONLY,
            conflict_resolution=conflict_resolution,
            send_meeting_invitations=send_meeting_invitations
        )
        if self.id != item_id \
                and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)) \
                and not isinstance(self, Task):
            # When we update an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so
            # the ID of this item changes.
            #
            # When we update certain fields on a task, the ID may change. A full description is available at
            # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation-task
            raise ValueError("'id' mismatch in returned update response")
        # Don't check that changekeys are different. No-op saves will sometimes leave the changekey intact
        self._id = self.ID_ELEMENT_CLS(item_id, changekey)
    else:
        if update_fields:
            raise ValueError("'update_fields' is only valid for updates")
        tmp_attachments = None
        if self.account and self.account.version.build < EXCHANGE_2013 and self.attachments:
            # At least some versions prior to Exchange 2013 can't save attachments immediately. You need to first
            # save, then attach. Store the attachment of this item temporarily and attach later.
            tmp_attachments, self.attachments = self.attachments, []
        item = self._create(message_disposition=SAVE_ONLY, send_meeting_invitations=send_meeting_invitations)
        self._id = self.ID_ELEMENT_CLS(item.id, item.changekey)
        for old_att, new_att in zip(self.attachments, item.attachments):
            if old_att.attachment_id is not None:
                raise ValueError("Old 'attachment_id' is not empty")
            if new_att.attachment_id is None:
                raise ValueError("New 'attachment_id' is empty")
            old_att.attachment_id = new_att.attachment_id
        if tmp_attachments:
            # Exchange 2007 workaround. See above
            self.attach(tmp_attachments)
    return self
def soft_delete(self, send_meeting_cancellations='SendToNone', affected_task_occurrences='AllOccurrences', suppress_read_receipts=True)
Expand source code
def soft_delete(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES,
                suppress_read_receipts=True):
    # Delete and move to the dumpster, if it is enabled.
    self._delete(delete_type=SOFT_DELETE, send_meeting_cancellations=send_meeting_cancellations,
                 affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts)
    self._id = None
    self.folder = self.account.recoverable_items_deletions

Inherited members

class MeetingCancellation (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingcancellation

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class MeetingCancellation(BaseMeetingItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingcancellation"""

    ELEMENT_NAME = 'MeetingCancellation'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class MeetingRequest (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingrequest

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class MeetingRequest(BaseMeetingItem, AcceptDeclineMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingrequest"""

    ELEMENT_NAME = 'MeetingRequest'

    meeting_request_type = ChoiceField(field_uri='meetingRequest:MeetingRequestType', choices={
        Choice('FullUpdate'), Choice('InformationalUpdate'), Choice('NewMeetingRequest'), Choice('None'),
        Choice('Outdated'), Choice('PrincipalWantsCopy'), Choice('SilentUpdate')
    }, default='None')
    intended_free_busy_status = ChoiceField(field_uri='meetingRequest:IntendedFreeBusyStatus', choices={
            Choice('Free'), Choice('Tentative'), Choice('Busy'), Choice('OOF'), Choice('NoData')
    }, is_required=True, default='Busy')

    # This element also has some fields from CalendarItem
    start_idx = CalendarItem.FIELDS.index_by_name('start')
    is_response_requested_idx = CalendarItem.FIELDS.index_by_name('is_response_requested')
    FIELDS = BaseMeetingItem.FIELDS \
        + CalendarItem.FIELDS[start_idx:is_response_requested_idx]\
        + CalendarItem.FIELDS[is_response_requested_idx + 1:]

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var is_response_requested_idx
var start_idx

Instance variables

var adjacent_meeting_count
var adjacent_meetings
var allow_new_time_proposal
var appointment_reply_time
var appointment_sequence_number
var appointment_state
var conference_type
var conflicting_meeting_count
var conflicting_meetings
var deleted_occurrences
var duration
var end
var first_occurrence
var intended_free_busy_status
var is_all_day
var is_cancelled
var is_meeting
var is_online_meeting
var is_recurring
var last_occurrence
var legacy_free_busy_status
var location
var meeting_request_type
var meeting_request_was_sent
var meeting_workspace_url
var modified_occurrences
var my_response_type
var net_show_url
var optional_attendees
var organizer
var original_start
var recurrence
var required_attendees
var resources
var start
var type
var when

Inherited members

class MeetingResponse (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingresponse

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class MeetingResponse(BaseMeetingItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingresponse"""

    ELEMENT_NAME = 'MeetingResponse'

    received_by = MailboxField(field_uri='message:ReceivedBy', is_read_only=True)
    received_representing = MailboxField(field_uri='message:ReceivedRepresenting', is_read_only=True)
    proposed_start = DateTimeField(field_uri='meeting:ProposedStart', supported_from=EXCHANGE_2013)
    proposed_end = DateTimeField(field_uri='meeting:ProposedEnd', supported_from=EXCHANGE_2013)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var proposed_end
var proposed_start
var received_by
var received_representing

Inherited members

class Message (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/message-ex15websvcsotherref

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class Message(Item):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/message-ex15websvcsotherref
    """

    ELEMENT_NAME = 'Message'

    sender = MailboxField(field_uri='message:Sender', is_read_only=True, is_read_only_after_send=True)
    to_recipients = MailboxListField(field_uri='message:ToRecipients', is_read_only_after_send=True,
                                     is_searchable=False)
    cc_recipients = MailboxListField(field_uri='message:CcRecipients', is_read_only_after_send=True,
                                     is_searchable=False)
    bcc_recipients = MailboxListField(field_uri='message:BccRecipients', is_read_only_after_send=True,
                                      is_searchable=False)
    is_read_receipt_requested = BooleanField(field_uri='message:IsReadReceiptRequested',
                                             is_required=True, default=False, is_read_only_after_send=True)
    is_delivery_receipt_requested = BooleanField(field_uri='message:IsDeliveryReceiptRequested', is_required=True,
                                                 default=False, is_read_only_after_send=True)
    conversation_index = Base64Field(field_uri='message:ConversationIndex', is_read_only=True)
    conversation_topic = CharField(field_uri='message:ConversationTopic', is_read_only=True)
    # Rename 'From' to 'author'. We can't use fieldname 'from' since it's a Python keyword.
    author = MailboxField(field_uri='message:From', is_read_only_after_send=True)
    message_id = CharField(field_uri='message:InternetMessageId', is_read_only_after_send=True)
    is_read = BooleanField(field_uri='message:IsRead', is_required=True, default=False)
    is_response_requested = BooleanField(field_uri='message:IsResponseRequested', default=False, is_required=True)
    references = TextField(field_uri='message:References')
    reply_to = MailboxListField(field_uri='message:ReplyTo', is_read_only_after_send=True, is_searchable=False)
    received_by = MailboxField(field_uri='message:ReceivedBy', is_read_only=True)
    received_representing = MailboxField(field_uri='message:ReceivedRepresenting', is_read_only=True)
    reminder_message_data = EWSElementField(field_uri='message:ReminderMessageData', value_cls=ReminderMessageData,
                                            supported_from=EXCHANGE_2013_SP1, is_read_only=True)

    @require_account
    def send(self, save_copy=True, copy_to_folder=None, conflict_resolution=AUTO_RESOLVE,
             send_meeting_invitations=SEND_TO_NONE):
        # Only sends a message. The message can either be an existing draft stored in EWS or a new message that does
        # not yet exist in EWS.
        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.account.sent  # 'Sent' is default EWS behaviour
        if self.id:
            SendItem(account=self.account).get(items=[self], saved_item_folder=copy_to_folder)
            # The item will be deleted from the original folder
            self._id = None
            self.folder = copy_to_folder
            return None

        # New message
        if copy_to_folder:
            # This would better be done via send_and_save() but lets just support it here
            self.folder = copy_to_folder
            return self.send_and_save(conflict_resolution=conflict_resolution,
                                      send_meeting_invitations=send_meeting_invitations)

        if self.account.version.build < EXCHANGE_2013 and self.attachments:
            # At least some versions prior to Exchange 2013 can't send attachments immediately. You need to first save,
            # then attach, then send. This is done in send_and_save(). send() will delete the item again.
            self.send_and_save(conflict_resolution=conflict_resolution,
                               send_meeting_invitations=send_meeting_invitations)
            return None

        self._create(message_disposition=SEND_ONLY, send_meeting_invitations=send_meeting_invitations)
        return None

    def send_and_save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE,
                      send_meeting_invitations=SEND_TO_NONE):
        # Sends Message and saves a copy in the parent folder. Does not return an ItemId.
        if self.id:
            self._update(
                update_fieldnames=update_fields,
                message_disposition=SEND_AND_SAVE_COPY,
                conflict_resolution=conflict_resolution,
                send_meeting_invitations=send_meeting_invitations
            )
        else:
            if self.account.version.build < EXCHANGE_2013 and self.attachments:
                # At least some versions prior to Exchange 2013 can't send-and-save attachments immediately. You need
                # to first save, then attach, then send. This is done in save().
                self.save(update_fields=update_fields, conflict_resolution=conflict_resolution,
                          send_meeting_invitations=send_meeting_invitations)
                self.send(save_copy=False, conflict_resolution=conflict_resolution,
                          send_meeting_invitations=send_meeting_invitations)
            else:
                res = self._create(
                    message_disposition=SEND_AND_SAVE_COPY,
                    send_meeting_invitations=send_meeting_invitations
                )
                if res is not True:
                    raise ValueError('Unexpected response in send-only mode')

    @require_id
    def create_reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
        if to_recipients is None:
            if not self.author:
                raise ValueError("'to_recipients' must be set when message has no 'author'")
            to_recipients = [self.author]
        return ReplyToItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            subject=subject,
            new_body=body,
            to_recipients=to_recipients,
            cc_recipients=cc_recipients,
            bcc_recipients=bcc_recipients,
        )

    def reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
        self.create_reply(
            subject,
            body,
            to_recipients,
            cc_recipients,
            bcc_recipients
        ).send()

    @require_id
    def create_reply_all(self, subject, body):
        to_recipients = list(self.to_recipients) if self.to_recipients else []
        if self.author:
            to_recipients.append(self.author)
        return ReplyAllToItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            subject=subject,
            new_body=body,
            to_recipients=to_recipients,
            cc_recipients=self.cc_recipients,
            bcc_recipients=self.bcc_recipients,
        )

    def reply_all(self, subject, body):
        self.create_reply_all(subject, body).send()

    def mark_as_junk(self, is_junk=True, move_item=True):
        """Mark or un-marks items as junk email.

        :param is_junk: If True, the sender will be added from the blocked sender list. Otherwise, the sender will be
        removed.
        :param move_item: If true, the item will be moved to the junk folder.
        :return:
        """
        res = MarkAsJunk(account=self.account).get(
            items=[self], is_junk=is_junk, move_item=move_item, expect_result=move_item
        )
        if res is None:
            return
        self.folder = self.account.junk if is_junk else self.account.inbox
        self.id, self.changekey = res

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var author
var bcc_recipients
var cc_recipients
var conversation_index
var conversation_topic
var is_delivery_receipt_requested
var is_read
var is_read_receipt_requested
var is_response_requested
var message_id
var received_by
var received_representing
var references
var reminder_message_data
var reply_to
var sender
var to_recipients

Methods

def create_reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None)
Expand source code
@require_id
def create_reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
    if to_recipients is None:
        if not self.author:
            raise ValueError("'to_recipients' must be set when message has no 'author'")
        to_recipients = [self.author]
    return ReplyToItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        subject=subject,
        new_body=body,
        to_recipients=to_recipients,
        cc_recipients=cc_recipients,
        bcc_recipients=bcc_recipients,
    )
def create_reply_all(self, subject, body)
Expand source code
@require_id
def create_reply_all(self, subject, body):
    to_recipients = list(self.to_recipients) if self.to_recipients else []
    if self.author:
        to_recipients.append(self.author)
    return ReplyAllToItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        subject=subject,
        new_body=body,
        to_recipients=to_recipients,
        cc_recipients=self.cc_recipients,
        bcc_recipients=self.bcc_recipients,
    )
def mark_as_junk(self, is_junk=True, move_item=True)

Mark or un-marks items as junk email.

:param is_junk: If True, the sender will be added from the blocked sender list. Otherwise, the sender will be removed. :param move_item: If true, the item will be moved to the junk folder. :return:

Expand source code
def mark_as_junk(self, is_junk=True, move_item=True):
    """Mark or un-marks items as junk email.

    :param is_junk: If True, the sender will be added from the blocked sender list. Otherwise, the sender will be
    removed.
    :param move_item: If true, the item will be moved to the junk folder.
    :return:
    """
    res = MarkAsJunk(account=self.account).get(
        items=[self], is_junk=is_junk, move_item=move_item, expect_result=move_item
    )
    if res is None:
        return
    self.folder = self.account.junk if is_junk else self.account.inbox
    self.id, self.changekey = res
def reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None)
Expand source code
def reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
    self.create_reply(
        subject,
        body,
        to_recipients,
        cc_recipients,
        bcc_recipients
    ).send()
def reply_all(self, subject, body)
Expand source code
def reply_all(self, subject, body):
    self.create_reply_all(subject, body).send()
def send(self, save_copy=True, copy_to_folder=None, conflict_resolution='AutoResolve', send_meeting_invitations='SendToNone')
Expand source code
@require_account
def send(self, save_copy=True, copy_to_folder=None, conflict_resolution=AUTO_RESOLVE,
         send_meeting_invitations=SEND_TO_NONE):
    # Only sends a message. The message can either be an existing draft stored in EWS or a new message that does
    # not yet exist in EWS.
    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.account.sent  # 'Sent' is default EWS behaviour
    if self.id:
        SendItem(account=self.account).get(items=[self], saved_item_folder=copy_to_folder)
        # The item will be deleted from the original folder
        self._id = None
        self.folder = copy_to_folder
        return None

    # New message
    if copy_to_folder:
        # This would better be done via send_and_save() but lets just support it here
        self.folder = copy_to_folder
        return self.send_and_save(conflict_resolution=conflict_resolution,
                                  send_meeting_invitations=send_meeting_invitations)

    if self.account.version.build < EXCHANGE_2013 and self.attachments:
        # At least some versions prior to Exchange 2013 can't send attachments immediately. You need to first save,
        # then attach, then send. This is done in send_and_save(). send() will delete the item again.
        self.send_and_save(conflict_resolution=conflict_resolution,
                           send_meeting_invitations=send_meeting_invitations)
        return None

    self._create(message_disposition=SEND_ONLY, send_meeting_invitations=send_meeting_invitations)
    return None
def send_and_save(self, update_fields=None, conflict_resolution='AutoResolve', send_meeting_invitations='SendToNone')
Expand source code
def send_and_save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE,
                  send_meeting_invitations=SEND_TO_NONE):
    # Sends Message and saves a copy in the parent folder. Does not return an ItemId.
    if self.id:
        self._update(
            update_fieldnames=update_fields,
            message_disposition=SEND_AND_SAVE_COPY,
            conflict_resolution=conflict_resolution,
            send_meeting_invitations=send_meeting_invitations
        )
    else:
        if self.account.version.build < EXCHANGE_2013 and self.attachments:
            # At least some versions prior to Exchange 2013 can't send-and-save attachments immediately. You need
            # to first save, then attach, then send. This is done in save().
            self.save(update_fields=update_fields, conflict_resolution=conflict_resolution,
                      send_meeting_invitations=send_meeting_invitations)
            self.send(save_copy=False, conflict_resolution=conflict_resolution,
                      send_meeting_invitations=send_meeting_invitations)
        else:
            res = self._create(
                message_disposition=SEND_AND_SAVE_COPY,
                send_meeting_invitations=send_meeting_invitations
            )
            if res is not True:
                raise ValueError('Unexpected response in send-only mode')

Inherited members

class Persona (**kwargs)
Expand source code
class Persona(IdChangeKeyMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/persona"""

    ELEMENT_NAME = 'Persona'
    ID_ELEMENT_CLS = PersonaId

    _id = IdElementField(field_uri='persona:PersonaId', value_cls=ID_ELEMENT_CLS, namespace=TNS)
    persona_type = CharField(field_uri='persona:PersonaType')
    persona_object_type = TextField(field_uri='persona:PersonaObjectStatus')
    creation_time = DateTimeField(field_uri='persona:CreationTime')
    bodies = BodyContentAttributedValueField(field_uri='persona:Bodies')
    display_name_first_last_sort_key = TextField(field_uri='persona:DisplayNameFirstLastSortKey')
    display_name_last_first_sort_key = TextField(field_uri='persona:DisplayNameLastFirstSortKey')
    company_sort_key = TextField(field_uri='persona:CompanyNameSortKey')
    home_sort_key = TextField(field_uri='persona:HomeCitySortKey')
    work_city_sort_key = TextField(field_uri='persona:WorkCitySortKey')
    display_name_first_last_header = CharField(field_uri='persona:DisplayNameFirstLastHeader')
    display_name_last_first_header = CharField(field_uri='persona:DisplayNameLastFirstHeader')
    file_as_header = TextField(field_uri='persona:FileAsHeader')
    display_name = CharField(field_uri='persona:DisplayName')
    display_name_first_last = CharField(field_uri='persona:DisplayNameFirstLast')
    display_name_last_first = CharField(field_uri='persona:DisplayNameLastFirst')
    file_as = CharField(field_uri='persona:FileAs')
    file_as_id = TextField(field_uri='persona:FileAsId')
    display_name_prefix = CharField(field_uri='persona:DisplayNamePrefix')
    given_name = CharField(field_uri='persona:GivenName')
    middle_name = CharField(field_uri='persona:MiddleName')
    surname = CharField(field_uri='persona:Surname')
    generation = CharField(field_uri='persona:Generation')
    nickname = TextField(field_uri='persona:Nickname')
    yomi_company_name = TextField(field_uri='persona:YomiCompanyName')
    yomi_first_name = TextField(field_uri='persona:YomiFirstName')
    yomi_last_name = TextField(field_uri='persona:YomiLastName')
    title = CharField(field_uri='persona:Title')
    department = TextField(field_uri='persona:Department')
    company_name = CharField(field_uri='persona:CompanyName')
    email_address = EWSElementField(field_uri='persona:EmailAddress', value_cls=EmailAddress)
    email_addresses = EWSElementListField(field_uri='persona:EmailAddresses', value_cls=Address)
    PhoneNumber = PersonaPhoneNumberField(field_uri='persona:PhoneNumber')
    im_address = CharField(field_uri='persona:ImAddress')
    home_city = CharField(field_uri='persona:HomeCity')
    work_city = CharField(field_uri='persona:WorkCity')
    relevance_score = CharField(field_uri='persona:RelevanceScore')
    folder_ids = EWSElementListField(field_uri='persona:FolderIds', value_cls=FolderId)
    attributions = EWSElementListField(field_uri='persona:Attributions', value_cls=Attribution)
    display_names = StringAttributedValueField(field_uri='persona:DisplayNames')
    file_ases = StringAttributedValueField(field_uri='persona:FileAses')
    file_as_ids = StringAttributedValueField(field_uri='persona:FileAsIds')
    display_name_prefixes = StringAttributedValueField(field_uri='persona:DisplayNamePrefixes')
    given_names = StringAttributedValueField(field_uri='persona:GivenNames')
    middle_names = StringAttributedValueField(field_uri='persona:MiddleNames')
    surnames = StringAttributedValueField(field_uri='persona:Surnames')
    generations = StringAttributedValueField(field_uri='persona:Generations')
    nicknames = StringAttributedValueField(field_uri='persona:Nicknames')
    initials = StringAttributedValueField(field_uri='persona:Initials')
    yomi_company_names = StringAttributedValueField(field_uri='persona:YomiCompanyNames')
    yomi_first_names = StringAttributedValueField(field_uri='persona:YomiFirstNames')
    yomi_last_names = StringAttributedValueField(field_uri='persona:YomiLastNames')
    business_phone_numbers = PhoneNumberAttributedValueField(field_uri='persona:BusinessPhoneNumbers')
    business_phone_numbers2 = PhoneNumberAttributedValueField(field_uri='persona:BusinessPhoneNumbers2')
    home_phones = PhoneNumberAttributedValueField(field_uri='persona:HomePhones')
    home_phones2 = PhoneNumberAttributedValueField(field_uri='persona:HomePhones2')
    mobile_phones = PhoneNumberAttributedValueField(field_uri='persona:MobilePhones')
    mobile_phones2 = PhoneNumberAttributedValueField(field_uri='persona:MobilePhones2')
    assistant_phone_numbers = PhoneNumberAttributedValueField(field_uri='persona:AssistantPhoneNumbers')
    callback_phones = PhoneNumberAttributedValueField(field_uri='persona:CallbackPhones')
    car_phones = PhoneNumberAttributedValueField(field_uri='persona:CarPhones')
    home_faxes = PhoneNumberAttributedValueField(field_uri='persona:HomeFaxes')
    orgnaization_main_phones = PhoneNumberAttributedValueField(field_uri='persona:OrganizationMainPhones')
    other_faxes = PhoneNumberAttributedValueField(field_uri='persona:OtherFaxes')
    other_telephones = PhoneNumberAttributedValueField(field_uri='persona:OtherTelephones')
    other_phones2 = PhoneNumberAttributedValueField(field_uri='persona:OtherPhones2')
    pagers = PhoneNumberAttributedValueField(field_uri='persona:Pagers')
    radio_phones = PhoneNumberAttributedValueField(field_uri='persona:RadioPhones')
    telex_numbers = PhoneNumberAttributedValueField(field_uri='persona:TelexNumbers')
    tty_tdd_phone_numbers = PhoneNumberAttributedValueField(field_uri='persona:TTYTDDPhoneNumbers')
    work_faxes = PhoneNumberAttributedValueField(field_uri='persona:WorkFaxes')
    emails1 = EmailAddressAttributedValueField(field_uri='persona:Emails1')
    emails2 = EmailAddressAttributedValueField(field_uri='persona:Emails2')
    emails3 = EmailAddressAttributedValueField(field_uri='persona:Emails3')
    business_home_pages = StringAttributedValueField(field_uri='persona:BusinessHomePages')
    personal_home_pages = StringAttributedValueField(field_uri='persona:PersonalHomePages')
    office_locations = StringAttributedValueField(field_uri='persona:OfficeLocations')
    im_addresses = StringAttributedValueField(field_uri='persona:ImAddresses')
    im_addresses2 = StringAttributedValueField(field_uri='persona:ImAddresses2')
    im_addresses3 = StringAttributedValueField(field_uri='persona:ImAddresses3')
    business_addresses = PostalAddressAttributedValueField(field_uri='persona:BusinessAddresses')
    home_addresses = PostalAddressAttributedValueField(field_uri='persona:HomeAddresses')
    other_addresses = PostalAddressAttributedValueField(field_uri='persona:OtherAddresses')
    titles = StringAttributedValueField(field_uri='persona:Titles')
    departments = StringAttributedValueField(field_uri='persona:Departments')
    company_names = StringAttributedValueField(field_uri='persona:CompanyNames')
    managers = StringAttributedValueField(field_uri='persona:Managers')
    assistant_names = StringAttributedValueField(field_uri='persona:AssistantNames')
    professions = StringAttributedValueField(field_uri='persona:Professions')
    spouse_names = StringAttributedValueField(field_uri='persona:SpouseNames')
    children = StringAttributedValueField(field_uri='persona:Children')
    schools = StringAttributedValueField(field_uri='persona:Schools')
    hobbies = StringAttributedValueField(field_uri='persona:Hobbies')
    wedding_anniversaries = StringAttributedValueField(field_uri='persona:WeddingAnniversaries')
    birthdays = StringAttributedValueField(field_uri='persona:Birthdays')
    locations = StringAttributedValueField(field_uri='persona:Locations')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var ID_ELEMENT_CLS

Instance variables

var PhoneNumber
var assistant_names
var assistant_phone_numbers
var attributions
var birthdays
var bodies
var business_addresses
var business_home_pages
var business_phone_numbers
var business_phone_numbers2
var callback_phones
var car_phones
var children
var company_name
var company_names
var company_sort_key
var creation_time
var department
var departments
var display_name
var display_name_first_last
var display_name_first_last_header
var display_name_first_last_sort_key
var display_name_last_first
var display_name_last_first_header
var display_name_last_first_sort_key
var display_name_prefix
var display_name_prefixes
var display_names
var email_address
var email_addresses
var emails1
var emails2
var emails3
var file_as
var file_as_header
var file_as_id
var file_as_ids
var file_ases
var folder_ids
var generation
var generations
var given_name
var given_names
var hobbies
var home_addresses
var home_city
var home_faxes
var home_phones
var home_phones2
var home_sort_key
var im_address
var im_addresses
var im_addresses2
var im_addresses3
var initials
var locations
var managers
var middle_name
var middle_names
var mobile_phones
var mobile_phones2
var nickname
var nicknames
var office_locations
var orgnaization_main_phones
var other_addresses
var other_faxes
var other_phones2
var other_telephones
var pagers
var persona_object_type
var persona_type
var personal_home_pages
var professions
var radio_phones
var relevance_score
var schools
var spouse_names
var surname
var surnames
var telex_numbers
var title
var titles
var tty_tdd_phone_numbers
var wedding_anniversaries
var work_city
var work_city_sort_key
var work_faxes
var yomi_company_name
var yomi_company_names
var yomi_first_name
var yomi_first_names
var yomi_last_name
var yomi_last_names

Inherited members

class PostItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postitem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class PostItem(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postitem"""

    ELEMENT_NAME = 'PostItem'

    conversation_index = Message.FIELDS['conversation_index']
    conversation_topic = Message.FIELDS['conversation_topic']

    author = Message.FIELDS['author']
    message_id = Message.FIELDS['message_id']
    is_read = Message.FIELDS['is_read']

    posted_time = DateTimeField(field_uri='postitem:PostedTime', is_read_only=True)
    references = TextField(field_uri='message:References')
    sender = MailboxField(field_uri='message:Sender', is_read_only=True, is_read_only_after_send=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var author
var conversation_index
var conversation_topic
var is_read
var message_id
var posted_time
var references
var sender

Inherited members

class PostReplyItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postreplyitem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class PostReplyItem(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postreplyitem"""

    ELEMENT_NAME = 'PostReplyItem'

    # This element only has Item fields up to, and including, 'culture'
    # TDO: Plus all message fields
    new_body = BodyField(field_uri='NewBodyContent')  # Accepts and returns Body or HTMLBody instances

    culture_idx = Item.FIELDS.index_by_name('culture')
    sender_idx = Message.FIELDS.index_by_name('sender')
    FIELDS = Item.FIELDS[:culture_idx + 1] + Message.FIELDS[sender_idx:]

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var culture_idx
var sender_idx

Instance variables

var author
var bcc_recipients
var cc_recipients
var conversation_index
var conversation_topic
var is_delivery_receipt_requested
var is_read
var is_read_receipt_requested
var is_response_requested
var message_id
var new_body
var received_by
var received_representing
var references
var reminder_message_data
var reply_to
var sender
var to_recipients

Inherited members

class RegisterMixIn (**kwargs)

Base class for classes that can change their list of supported fields dynamically.

Expand source code
class RegisterMixIn(IdChangeKeyMixIn, metaclass=EWSMeta):
    """Base class for classes that can change their list of supported fields dynamically."""

    # This class implements dynamic fields on an element class, so we need to include __dict__ in __slots__
    __slots__ = '__dict__',

    INSERT_AFTER_FIELD = None

    @classmethod
    def register(cls, attr_name, attr_cls):
        """Register a custom extended property in this item class so they can be accessed just like any other attribute

        :param attr_name:
        :param attr_cls:
        :return:
        """
        if not cls.INSERT_AFTER_FIELD:
            raise ValueError('Class %s is missing INSERT_AFTER_FIELD value' % cls)
        try:
            cls.get_field_by_fieldname(attr_name)
        except InvalidField:
            pass
        else:
            raise ValueError("'%s' is already registered" % attr_name)
        if not issubclass(attr_cls, ExtendedProperty):
            raise ValueError("%r must be a subclass of ExtendedProperty" % attr_cls)
        # Check if class attributes are properly defined
        attr_cls.validate_cls()
        # ExtendedProperty is not a real field, but a placeholder in the fields list. See
        #   https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item
        #
        # Find the correct index for the new extended property, and insert.
        field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
        cls.add_field(field, insert_after=cls.INSERT_AFTER_FIELD)

    @classmethod
    def deregister(cls, attr_name):
        """De-register an extended property that has been registered with register().

        :param attr_name:
        :return:
        """
        try:
            field = cls.get_field_by_fieldname(attr_name)
        except InvalidField:
            raise ValueError("'%s' is not registered" % attr_name)
        if not isinstance(field, ExtendedPropertyField):
            raise ValueError("'%s' is not registered as an ExtendedProperty" % attr_name)
        cls.remove_field(field)

Ancestors

Subclasses

Class variables

var INSERT_AFTER_FIELD

Static methods

def deregister(attr_name)

De-register an extended property that has been registered with register().

:param attr_name: :return:

Expand source code
@classmethod
def deregister(cls, attr_name):
    """De-register an extended property that has been registered with register().

    :param attr_name:
    :return:
    """
    try:
        field = cls.get_field_by_fieldname(attr_name)
    except InvalidField:
        raise ValueError("'%s' is not registered" % attr_name)
    if not isinstance(field, ExtendedPropertyField):
        raise ValueError("'%s' is not registered as an ExtendedProperty" % attr_name)
    cls.remove_field(field)
def register(attr_name, attr_cls)

Register a custom extended property in this item class so they can be accessed just like any other attribute

:param attr_name: :param attr_cls: :return:

Expand source code
@classmethod
def register(cls, attr_name, attr_cls):
    """Register a custom extended property in this item class so they can be accessed just like any other attribute

    :param attr_name:
    :param attr_cls:
    :return:
    """
    if not cls.INSERT_AFTER_FIELD:
        raise ValueError('Class %s is missing INSERT_AFTER_FIELD value' % cls)
    try:
        cls.get_field_by_fieldname(attr_name)
    except InvalidField:
        pass
    else:
        raise ValueError("'%s' is already registered" % attr_name)
    if not issubclass(attr_cls, ExtendedProperty):
        raise ValueError("%r must be a subclass of ExtendedProperty" % attr_cls)
    # Check if class attributes are properly defined
    attr_cls.validate_cls()
    # ExtendedProperty is not a real field, but a placeholder in the fields list. See
    #   https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item
    #
    # Find the correct index for the new extended property, and insert.
    field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
    cls.add_field(field, insert_after=cls.INSERT_AFTER_FIELD)

Inherited members

class ReplyAllToItem (**kwargs)
Expand source code
class ReplyAllToItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/replyalltoitem"""

    ELEMENT_NAME = 'ReplyAllToItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class ReplyToItem (**kwargs)
Expand source code
class ReplyToItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/replytoitem"""

    ELEMENT_NAME = 'ReplyToItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class Task (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/task

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class Task(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/task"""

    ELEMENT_NAME = 'Task'
    NOT_STARTED = 'NotStarted'
    COMPLETED = 'Completed'

    actual_work = IntegerField(field_uri='task:ActualWork', min=0)
    assigned_time = DateTimeField(field_uri='task:AssignedTime', is_read_only=True)
    billing_information = TextField(field_uri='task:BillingInformation')
    change_count = IntegerField(field_uri='task:ChangeCount', is_read_only=True, min=0)
    companies = TextListField(field_uri='task:Companies')
    # 'complete_date' can be set, but is ignored by the server, which sets it to now()
    complete_date = DateTimeField(field_uri='task:CompleteDate', is_read_only=True)
    contacts = TextListField(field_uri='task:Contacts')
    delegation_state = ChoiceField(field_uri='task:DelegationState', choices={
        Choice('NoMatch'), Choice('OwnNew'), Choice('Owned'), Choice('Accepted'), Choice('Declined'), Choice('Max')
    }, is_read_only=True)
    delegator = CharField(field_uri='task:Delegator', is_read_only=True)
    due_date = DateTimeBackedDateField(field_uri='task:DueDate')
    is_editable = BooleanField(field_uri='task:IsAssignmentEditable', is_read_only=True)
    is_complete = BooleanField(field_uri='task:IsComplete', is_read_only=True)
    is_recurring = BooleanField(field_uri='task:IsRecurring', is_read_only=True)
    is_team_task = BooleanField(field_uri='task:IsTeamTask', is_read_only=True)
    mileage = TextField(field_uri='task:Mileage')
    owner = CharField(field_uri='task:Owner', is_read_only=True)
    percent_complete = DecimalField(field_uri='task:PercentComplete', is_required=True, default=Decimal(0.0),
                                    min=Decimal(0), max=Decimal(100), is_searchable=False)
    recurrence = TaskRecurrenceField(field_uri='task:Recurrence', is_searchable=False)
    start_date = DateTimeBackedDateField(field_uri='task:StartDate')
    status = ChoiceField(field_uri='task:Status', choices={
        Choice(NOT_STARTED), Choice('InProgress'), Choice(COMPLETED), Choice('WaitingOnOthers'), Choice('Deferred')
    }, is_required=True, is_searchable=False, default=NOT_STARTED)
    status_description = CharField(field_uri='task:StatusDescription', is_read_only=True)
    total_work = IntegerField(field_uri='task:TotalWork', min=0)

    def clean(self, version=None):
        super().clean(version=version)
        if self.due_date and self.start_date and self.due_date < self.start_date:
            log.warning("'due_date' must be greater than 'start_date' (%s vs %s). Resetting 'due_date'",
                        self.due_date, self.start_date)
            self.due_date = self.start_date
        if self.complete_date:
            if self.status != self.COMPLETED:
                log.warning("'status' must be '%s' when 'complete_date' is set (%s). Resetting",
                            self.COMPLETED, self.status)
                self.status = self.COMPLETED
            now = datetime.datetime.now(tz=UTC)
            if (self.complete_date - now).total_seconds() > 120:
                # Reset complete_date values that are in the future
                # 'complete_date' can be set automatically by the server. Allow some grace between local and server time
                log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now)
                self.complete_date = now
            if self.start_date and self.complete_date.date() < self.start_date:
                log.warning("'complete_date' must be greater than 'start_date' (%s vs %s). Resetting",
                            self.complete_date, self.start_date)
                self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC)
        if self.percent_complete is not None:
            if self.status == self.COMPLETED and self.percent_complete != Decimal(100):
                # percent_complete must be 100% if task is complete
                log.warning("'percent_complete' must be 100 when 'status' is '%s' (%s). Resetting",
                            self.COMPLETED, self.percent_complete)
                self.percent_complete = Decimal(100)
            elif self.status == self.NOT_STARTED and self.percent_complete != Decimal(0):
                # percent_complete must be 0% if task is not started
                log.warning("'percent_complete' must be 0 when 'status' is '%s' (%s). Resetting",
                            self.NOT_STARTED, self.percent_complete)
                self.percent_complete = Decimal(0)

    def complete(self):
        # A helper method to mark a task as complete on the server
        self.status = Task.COMPLETED
        self.percent_complete = Decimal(100)
        self.save()

Ancestors

Class variables

var COMPLETED
var ELEMENT_NAME
var FIELDS
var NOT_STARTED

Instance variables

var actual_work
var assigned_time
var billing_information
var change_count
var companies
var complete_date
var contacts
var delegation_state
var delegator
var due_date
var is_complete
var is_editable
var is_recurring
var is_team_task
var mileage
var owner
var percent_complete
var recurrence
var start_date
var status
var status_description
var total_work

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)
    if self.due_date and self.start_date and self.due_date < self.start_date:
        log.warning("'due_date' must be greater than 'start_date' (%s vs %s). Resetting 'due_date'",
                    self.due_date, self.start_date)
        self.due_date = self.start_date
    if self.complete_date:
        if self.status != self.COMPLETED:
            log.warning("'status' must be '%s' when 'complete_date' is set (%s). Resetting",
                        self.COMPLETED, self.status)
            self.status = self.COMPLETED
        now = datetime.datetime.now(tz=UTC)
        if (self.complete_date - now).total_seconds() > 120:
            # Reset complete_date values that are in the future
            # 'complete_date' can be set automatically by the server. Allow some grace between local and server time
            log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now)
            self.complete_date = now
        if self.start_date and self.complete_date.date() < self.start_date:
            log.warning("'complete_date' must be greater than 'start_date' (%s vs %s). Resetting",
                        self.complete_date, self.start_date)
            self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC)
    if self.percent_complete is not None:
        if self.status == self.COMPLETED and self.percent_complete != Decimal(100):
            # percent_complete must be 100% if task is complete
            log.warning("'percent_complete' must be 100 when 'status' is '%s' (%s). Resetting",
                        self.COMPLETED, self.percent_complete)
            self.percent_complete = Decimal(100)
        elif self.status == self.NOT_STARTED and self.percent_complete != Decimal(0):
            # percent_complete must be 0% if task is not started
            log.warning("'percent_complete' must be 0 when 'status' is '%s' (%s). Resetting",
                        self.NOT_STARTED, self.percent_complete)
            self.percent_complete = Decimal(0)
def complete(self)
Expand source code
def complete(self):
    # A helper method to mark a task as complete on the server
    self.status = Task.COMPLETED
    self.percent_complete = Decimal(100)
    self.save()

Inherited members

class TentativelyAcceptItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/tentativelyacceptitem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class TentativelyAcceptItem(BaseMeetingReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/tentativelyacceptitem"""

    ELEMENT_NAME = 'TentativelyAcceptItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

exchangelib-4.6.1/docs/exchangelib/items/item.html000066400000000000000000002202051414601472700221500ustar00rootroot00000000000000 exchangelib.items.item API documentation

Module exchangelib.items.item

Expand source code
import logging

from .base import BaseItem, SAVE_ONLY, SEND_AND_SAVE_COPY, ID_ONLY, SEND_TO_NONE, \
    AUTO_RESOLVE, SOFT_DELETE, HARD_DELETE, ALL_OCCURRENCIES, MOVE_TO_DELETED_ITEMS
from ..fields import BooleanField, IntegerField, TextField, CharListField, ChoiceField, URIField, BodyField, \
    DateTimeField, MessageHeaderField, AttachmentField, Choice, EWSElementField, EffectiveRightsField, CultureField, \
    CharField, MimeContentField, FieldPath
from ..properties import ConversationId, ParentFolderId, ReferenceItemId, OccurrenceItemId, RecurringMasterItemId, \
    ResponseObjects, Fields
from ..services import GetItem, CreateItem, UpdateItem, DeleteItem, MoveItem, CopyItem, ArchiveItem
from ..util import is_iterable, require_account, require_id
from ..version import EXCHANGE_2010, EXCHANGE_2013

log = logging.getLogger(__name__)


class Item(BaseItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item"""

    ELEMENT_NAME = 'Item'

    mime_content = MimeContentField(field_uri='item:MimeContent', is_read_only_after_send=True)
    _id = BaseItem.FIELDS['_id']
    parent_folder_id = EWSElementField(field_uri='item:ParentFolderId', value_cls=ParentFolderId, is_read_only=True)
    item_class = CharField(field_uri='item:ItemClass', is_read_only=True)
    subject = CharField(field_uri='item:Subject')
    sensitivity = ChoiceField(field_uri='item:Sensitivity', choices={
        Choice('Normal'), Choice('Personal'), Choice('Private'), Choice('Confidential')
    }, is_required=True, default='Normal')
    text_body = TextField(field_uri='item:TextBody', is_read_only=True, supported_from=EXCHANGE_2013)
    body = BodyField(field_uri='item:Body')  # Accepts and returns Body or HTMLBody instances
    attachments = AttachmentField(field_uri='item:Attachments')  # ItemAttachment or FileAttachment
    datetime_received = DateTimeField(field_uri='item:DateTimeReceived', is_read_only=True)
    size = IntegerField(field_uri='item:Size', is_read_only=True)  # Item size in bytes
    categories = CharListField(field_uri='item:Categories')
    importance = ChoiceField(field_uri='item:Importance', choices={
        Choice('Low'), Choice('Normal'), Choice('High')
    }, is_required=True, default='Normal')
    in_reply_to = TextField(field_uri='item:InReplyTo')
    is_submitted = BooleanField(field_uri='item:IsSubmitted', is_read_only=True)
    is_draft = BooleanField(field_uri='item:IsDraft', is_read_only=True)
    is_from_me = BooleanField(field_uri='item:IsFromMe', is_read_only=True)
    is_resend = BooleanField(field_uri='item:IsResend', is_read_only=True)
    is_unmodified = BooleanField(field_uri='item:IsUnmodified', is_read_only=True)
    headers = MessageHeaderField(field_uri='item:InternetMessageHeaders', is_read_only=True)
    datetime_sent = DateTimeField(field_uri='item:DateTimeSent', is_read_only=True)
    datetime_created = DateTimeField(field_uri='item:DateTimeCreated', is_read_only=True)
    response_objects = EWSElementField(field_uri='item:ResponseObjects', value_cls=ResponseObjects,
                                       is_read_only=True,)
    # Placeholder for ResponseObjects
    reminder_due_by = DateTimeField(field_uri='item:ReminderDueBy', is_required_after_save=True, is_searchable=False)
    reminder_is_set = BooleanField(field_uri='item:ReminderIsSet', is_required=True, default=False)
    reminder_minutes_before_start = IntegerField(field_uri='item:ReminderMinutesBeforeStart',
                                                 is_required_after_save=True, min=0, default=0)
    display_cc = TextField(field_uri='item:DisplayCc', is_read_only=True)
    display_to = TextField(field_uri='item:DisplayTo', is_read_only=True)
    has_attachments = BooleanField(field_uri='item:HasAttachments', is_read_only=True)
    # ExtendedProperty fields go here
    culture = CultureField(field_uri='item:Culture', is_required_after_save=True, is_searchable=False)
    effective_rights = EffectiveRightsField(field_uri='item:EffectiveRights', is_read_only=True)
    last_modified_name = CharField(field_uri='item:LastModifiedName', is_read_only=True)
    last_modified_time = DateTimeField(field_uri='item:LastModifiedTime', is_read_only=True)
    is_associated = BooleanField(field_uri='item:IsAssociated', is_read_only=True, supported_from=EXCHANGE_2010)
    web_client_read_form_query_string = URIField(field_uri='item:WebClientReadFormQueryString', is_read_only=True,
                                                 supported_from=EXCHANGE_2010)
    web_client_edit_form_query_string = URIField(field_uri='item:WebClientEditFormQueryString', is_read_only=True,
                                                 supported_from=EXCHANGE_2010)
    conversation_id = EWSElementField(field_uri='item:ConversationId', value_cls=ConversationId,
                                      is_read_only=True, supported_from=EXCHANGE_2010)
    unique_body = BodyField(field_uri='item:UniqueBody', is_read_only=True, supported_from=EXCHANGE_2010)

    FIELDS = Fields()

    # Used to register extended properties
    INSERT_AFTER_FIELD = 'has_attachments'

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if self.attachments:
            for a in self.attachments:
                if a.parent_item:
                    if a.parent_item is not self:
                        raise ValueError("'parent_item' of attachment %s must point to this item" % a)
                else:
                    a.parent_item = self
                self.attach(self.attachments)
        else:
            self.attachments = []

    def save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE):
        from .task import Task
        if self.id:
            item_id, changekey = self._update(
                update_fieldnames=update_fields,
                message_disposition=SAVE_ONLY,
                conflict_resolution=conflict_resolution,
                send_meeting_invitations=send_meeting_invitations
            )
            if self.id != item_id \
                    and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)) \
                    and not isinstance(self, Task):
                # When we update an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so
                # the ID of this item changes.
                #
                # When we update certain fields on a task, the ID may change. A full description is available at
                # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation-task
                raise ValueError("'id' mismatch in returned update response")
            # Don't check that changekeys are different. No-op saves will sometimes leave the changekey intact
            self._id = self.ID_ELEMENT_CLS(item_id, changekey)
        else:
            if update_fields:
                raise ValueError("'update_fields' is only valid for updates")
            tmp_attachments = None
            if self.account and self.account.version.build < EXCHANGE_2013 and self.attachments:
                # At least some versions prior to Exchange 2013 can't save attachments immediately. You need to first
                # save, then attach. Store the attachment of this item temporarily and attach later.
                tmp_attachments, self.attachments = self.attachments, []
            item = self._create(message_disposition=SAVE_ONLY, send_meeting_invitations=send_meeting_invitations)
            self._id = self.ID_ELEMENT_CLS(item.id, item.changekey)
            for old_att, new_att in zip(self.attachments, item.attachments):
                if old_att.attachment_id is not None:
                    raise ValueError("Old 'attachment_id' is not empty")
                if new_att.attachment_id is None:
                    raise ValueError("New 'attachment_id' is empty")
                old_att.attachment_id = new_att.attachment_id
            if tmp_attachments:
                # Exchange 2007 workaround. See above
                self.attach(tmp_attachments)
        return self

    @require_account
    def _create(self, message_disposition, send_meeting_invitations):
        # Return a BulkCreateResult because we want to return the ID of both the main item *and* attachments. In send
        # and send-and-save-copy mode, the server does not return an ID, so we just return True.
        return CreateItem(account=self.account).get(
            items=[self],
            folder=self.folder,
            message_disposition=message_disposition,
            send_meeting_invitations=send_meeting_invitations,
        )

    def _update_fieldnames(self):
        from .contact import Contact, DistributionList
        # Return the list of fields we are allowed to update
        update_fieldnames = []
        for f in self.supported_fields(version=self.account.version):
            if f.name == 'attachments':
                # Attachments are handled separately after item creation
                continue
            if f.is_read_only:
                # These cannot be changed
                continue
            if (f.is_required or f.is_required_after_save) and (
                    getattr(self, f.name) is None or (f.is_list and not getattr(self, f.name))
            ):
                # These are required and cannot be deleted
                continue
            if not self.is_draft and f.is_read_only_after_send:
                # These cannot be changed when the item is no longer a draft
                continue
            if f.name == 'message_id' and f.is_read_only_after_send:
                # 'message_id' doesn't support updating, no matter the draft status
                continue
            if f.name == 'mime_content' and isinstance(self, (Contact, DistributionList)):
                # Contact and DistributionList don't support updating mime_content, no matter the draft status
                continue
            update_fieldnames.append(f.name)
        return update_fieldnames

    @require_account
    def _update(self, update_fieldnames, message_disposition, conflict_resolution, send_meeting_invitations):
        if not self.changekey:
            raise ValueError('%s must have changekey' % self.__class__.__name__)
        if not update_fieldnames:
            # The fields to update was not specified explicitly. Update all fields where update is possible
            update_fieldnames = self._update_fieldnames()
        return UpdateItem(account=self.account).get(
            items=[(self, update_fieldnames)],
            message_disposition=message_disposition,
            conflict_resolution=conflict_resolution,
            send_meeting_invitations_or_cancellations=send_meeting_invitations,
            suppress_read_receipts=True,
            expect_result=message_disposition != SEND_AND_SAVE_COPY,
        )

    @require_id
    def refresh(self):
        # Updates the item based on fresh data from EWS
        from ..folders import Folder
        additional_fields = {
            FieldPath(field=f) for f in Folder(root=self.account.root).allowed_item_fields(version=self.account.version)
        }
        res = GetItem(account=self.account).get(items=[self], additional_fields=additional_fields, shape=ID_ONLY)
        if self.id != res.id and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)):
            # When we refresh an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so
            # the ID of this item changes.
            raise ValueError("'id' mismatch in returned update response")
        for f in self.FIELDS:
            setattr(self, f.name, getattr(res, f.name))
        # 'parent_item' should point to 'self', not 'fresh_item'. That way, 'fresh_item' can be garbage collected.
        for a in self.attachments:
            a.parent_item = self
        return self

    @require_id
    def copy(self, to_folder):
        # If 'to_folder' is a public folder or a folder in a different mailbox then None is returned
        return CopyItem(account=self.account).get(
            items=[self],
            to_folder=to_folder,
            expect_result=None,
        )

    @require_id
    def move(self, to_folder):
        res = MoveItem(account=self.account).get(
            items=[self],
            to_folder=to_folder,
            expect_result=None,
        )
        if res is None:
            # Assume 'to_folder' is a public folder or a folder in a different mailbox
            self._id = None
            return
        self._id = self.ID_ELEMENT_CLS(*res)
        self.folder = to_folder

    def move_to_trash(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES,
                      suppress_read_receipts=True):
        # Delete and move to the trash folder.
        self._delete(delete_type=MOVE_TO_DELETED_ITEMS, send_meeting_cancellations=send_meeting_cancellations,
                     affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts)
        self._id = None
        self.folder = self.account.trash

    def soft_delete(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES,
                    suppress_read_receipts=True):
        # Delete and move to the dumpster, if it is enabled.
        self._delete(delete_type=SOFT_DELETE, send_meeting_cancellations=send_meeting_cancellations,
                     affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts)
        self._id = None
        self.folder = self.account.recoverable_items_deletions

    def delete(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES,
               suppress_read_receipts=True):
        # Remove the item permanently. No copies are stored anywhere.
        self._delete(delete_type=HARD_DELETE, send_meeting_cancellations=send_meeting_cancellations,
                     affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts)
        self._id, self.folder = None, None

    @require_id
    def _delete(self, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts):
        DeleteItem(account=self.account).get(
            items=[self],
            delete_type=delete_type,
            send_meeting_cancellations=send_meeting_cancellations,
            affected_task_occurrences=affected_task_occurrences,
            suppress_read_receipts=suppress_read_receipts,
        )

    @require_id
    def archive(self, to_folder):
        return ArchiveItem(account=self.account).get(items=[self], to_folder=to_folder, expect_result=True)

    def attach(self, attachments):
        """Add an attachment, or a list of attachments, to this item. If the item has already been saved, the
        attachments will be created on the server immediately. If the item has not yet been saved, the attachments will
        be created on the server when the item is saved.

        Adding attachments to an existing item will update the changekey of the item.

        :param attachments:
        """
        if not is_iterable(attachments, generators_allowed=True):
            attachments = [attachments]
        for a in attachments:
            if not a.parent_item:
                a.parent_item = self
            if self.id and not a.attachment_id:
                # Already saved object. Attach the attachment server-side now
                a.attach()
            if a not in self.attachments:
                self.attachments.append(a)

    def detach(self, attachments):
        """Remove an attachment, or a list of attachments, from this item. If the item has already been saved, the
        attachments will be deleted on the server immediately. If the item has not yet been saved, the attachments will
        simply not be created on the server the item is saved.

        Removing attachments from an existing item will update the changekey of the item.

        :param attachments:
        """
        if not is_iterable(attachments, generators_allowed=True):
            attachments = [attachments]
        if attachments is self.attachments:
            # Don't remove from the same list we are iterating
            attachments = list(attachments)
        for a in attachments:
            if a.parent_item is not self:
                raise ValueError('Attachment does not belong to this item')
            if self.id:
                # Item is already created. Detach  the attachment server-side now
                a.detach()
            if a in self.attachments:
                self.attachments.remove(a)

    @require_id
    def create_forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None):
        from .message import ForwardItem
        return ForwardItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            subject=subject,
            new_body=body,
            to_recipients=to_recipients,
            cc_recipients=cc_recipients,
            bcc_recipients=bcc_recipients,
        )

    def forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None):
        self.create_forward(
            subject,
            body,
            to_recipients,
            cc_recipients,
            bcc_recipients,
        ).send()

Classes

class Item (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class Item(BaseItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item"""

    ELEMENT_NAME = 'Item'

    mime_content = MimeContentField(field_uri='item:MimeContent', is_read_only_after_send=True)
    _id = BaseItem.FIELDS['_id']
    parent_folder_id = EWSElementField(field_uri='item:ParentFolderId', value_cls=ParentFolderId, is_read_only=True)
    item_class = CharField(field_uri='item:ItemClass', is_read_only=True)
    subject = CharField(field_uri='item:Subject')
    sensitivity = ChoiceField(field_uri='item:Sensitivity', choices={
        Choice('Normal'), Choice('Personal'), Choice('Private'), Choice('Confidential')
    }, is_required=True, default='Normal')
    text_body = TextField(field_uri='item:TextBody', is_read_only=True, supported_from=EXCHANGE_2013)
    body = BodyField(field_uri='item:Body')  # Accepts and returns Body or HTMLBody instances
    attachments = AttachmentField(field_uri='item:Attachments')  # ItemAttachment or FileAttachment
    datetime_received = DateTimeField(field_uri='item:DateTimeReceived', is_read_only=True)
    size = IntegerField(field_uri='item:Size', is_read_only=True)  # Item size in bytes
    categories = CharListField(field_uri='item:Categories')
    importance = ChoiceField(field_uri='item:Importance', choices={
        Choice('Low'), Choice('Normal'), Choice('High')
    }, is_required=True, default='Normal')
    in_reply_to = TextField(field_uri='item:InReplyTo')
    is_submitted = BooleanField(field_uri='item:IsSubmitted', is_read_only=True)
    is_draft = BooleanField(field_uri='item:IsDraft', is_read_only=True)
    is_from_me = BooleanField(field_uri='item:IsFromMe', is_read_only=True)
    is_resend = BooleanField(field_uri='item:IsResend', is_read_only=True)
    is_unmodified = BooleanField(field_uri='item:IsUnmodified', is_read_only=True)
    headers = MessageHeaderField(field_uri='item:InternetMessageHeaders', is_read_only=True)
    datetime_sent = DateTimeField(field_uri='item:DateTimeSent', is_read_only=True)
    datetime_created = DateTimeField(field_uri='item:DateTimeCreated', is_read_only=True)
    response_objects = EWSElementField(field_uri='item:ResponseObjects', value_cls=ResponseObjects,
                                       is_read_only=True,)
    # Placeholder for ResponseObjects
    reminder_due_by = DateTimeField(field_uri='item:ReminderDueBy', is_required_after_save=True, is_searchable=False)
    reminder_is_set = BooleanField(field_uri='item:ReminderIsSet', is_required=True, default=False)
    reminder_minutes_before_start = IntegerField(field_uri='item:ReminderMinutesBeforeStart',
                                                 is_required_after_save=True, min=0, default=0)
    display_cc = TextField(field_uri='item:DisplayCc', is_read_only=True)
    display_to = TextField(field_uri='item:DisplayTo', is_read_only=True)
    has_attachments = BooleanField(field_uri='item:HasAttachments', is_read_only=True)
    # ExtendedProperty fields go here
    culture = CultureField(field_uri='item:Culture', is_required_after_save=True, is_searchable=False)
    effective_rights = EffectiveRightsField(field_uri='item:EffectiveRights', is_read_only=True)
    last_modified_name = CharField(field_uri='item:LastModifiedName', is_read_only=True)
    last_modified_time = DateTimeField(field_uri='item:LastModifiedTime', is_read_only=True)
    is_associated = BooleanField(field_uri='item:IsAssociated', is_read_only=True, supported_from=EXCHANGE_2010)
    web_client_read_form_query_string = URIField(field_uri='item:WebClientReadFormQueryString', is_read_only=True,
                                                 supported_from=EXCHANGE_2010)
    web_client_edit_form_query_string = URIField(field_uri='item:WebClientEditFormQueryString', is_read_only=True,
                                                 supported_from=EXCHANGE_2010)
    conversation_id = EWSElementField(field_uri='item:ConversationId', value_cls=ConversationId,
                                      is_read_only=True, supported_from=EXCHANGE_2010)
    unique_body = BodyField(field_uri='item:UniqueBody', is_read_only=True, supported_from=EXCHANGE_2010)

    FIELDS = Fields()

    # Used to register extended properties
    INSERT_AFTER_FIELD = 'has_attachments'

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if self.attachments:
            for a in self.attachments:
                if a.parent_item:
                    if a.parent_item is not self:
                        raise ValueError("'parent_item' of attachment %s must point to this item" % a)
                else:
                    a.parent_item = self
                self.attach(self.attachments)
        else:
            self.attachments = []

    def save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE):
        from .task import Task
        if self.id:
            item_id, changekey = self._update(
                update_fieldnames=update_fields,
                message_disposition=SAVE_ONLY,
                conflict_resolution=conflict_resolution,
                send_meeting_invitations=send_meeting_invitations
            )
            if self.id != item_id \
                    and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)) \
                    and not isinstance(self, Task):
                # When we update an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so
                # the ID of this item changes.
                #
                # When we update certain fields on a task, the ID may change. A full description is available at
                # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation-task
                raise ValueError("'id' mismatch in returned update response")
            # Don't check that changekeys are different. No-op saves will sometimes leave the changekey intact
            self._id = self.ID_ELEMENT_CLS(item_id, changekey)
        else:
            if update_fields:
                raise ValueError("'update_fields' is only valid for updates")
            tmp_attachments = None
            if self.account and self.account.version.build < EXCHANGE_2013 and self.attachments:
                # At least some versions prior to Exchange 2013 can't save attachments immediately. You need to first
                # save, then attach. Store the attachment of this item temporarily and attach later.
                tmp_attachments, self.attachments = self.attachments, []
            item = self._create(message_disposition=SAVE_ONLY, send_meeting_invitations=send_meeting_invitations)
            self._id = self.ID_ELEMENT_CLS(item.id, item.changekey)
            for old_att, new_att in zip(self.attachments, item.attachments):
                if old_att.attachment_id is not None:
                    raise ValueError("Old 'attachment_id' is not empty")
                if new_att.attachment_id is None:
                    raise ValueError("New 'attachment_id' is empty")
                old_att.attachment_id = new_att.attachment_id
            if tmp_attachments:
                # Exchange 2007 workaround. See above
                self.attach(tmp_attachments)
        return self

    @require_account
    def _create(self, message_disposition, send_meeting_invitations):
        # Return a BulkCreateResult because we want to return the ID of both the main item *and* attachments. In send
        # and send-and-save-copy mode, the server does not return an ID, so we just return True.
        return CreateItem(account=self.account).get(
            items=[self],
            folder=self.folder,
            message_disposition=message_disposition,
            send_meeting_invitations=send_meeting_invitations,
        )

    def _update_fieldnames(self):
        from .contact import Contact, DistributionList
        # Return the list of fields we are allowed to update
        update_fieldnames = []
        for f in self.supported_fields(version=self.account.version):
            if f.name == 'attachments':
                # Attachments are handled separately after item creation
                continue
            if f.is_read_only:
                # These cannot be changed
                continue
            if (f.is_required or f.is_required_after_save) and (
                    getattr(self, f.name) is None or (f.is_list and not getattr(self, f.name))
            ):
                # These are required and cannot be deleted
                continue
            if not self.is_draft and f.is_read_only_after_send:
                # These cannot be changed when the item is no longer a draft
                continue
            if f.name == 'message_id' and f.is_read_only_after_send:
                # 'message_id' doesn't support updating, no matter the draft status
                continue
            if f.name == 'mime_content' and isinstance(self, (Contact, DistributionList)):
                # Contact and DistributionList don't support updating mime_content, no matter the draft status
                continue
            update_fieldnames.append(f.name)
        return update_fieldnames

    @require_account
    def _update(self, update_fieldnames, message_disposition, conflict_resolution, send_meeting_invitations):
        if not self.changekey:
            raise ValueError('%s must have changekey' % self.__class__.__name__)
        if not update_fieldnames:
            # The fields to update was not specified explicitly. Update all fields where update is possible
            update_fieldnames = self._update_fieldnames()
        return UpdateItem(account=self.account).get(
            items=[(self, update_fieldnames)],
            message_disposition=message_disposition,
            conflict_resolution=conflict_resolution,
            send_meeting_invitations_or_cancellations=send_meeting_invitations,
            suppress_read_receipts=True,
            expect_result=message_disposition != SEND_AND_SAVE_COPY,
        )

    @require_id
    def refresh(self):
        # Updates the item based on fresh data from EWS
        from ..folders import Folder
        additional_fields = {
            FieldPath(field=f) for f in Folder(root=self.account.root).allowed_item_fields(version=self.account.version)
        }
        res = GetItem(account=self.account).get(items=[self], additional_fields=additional_fields, shape=ID_ONLY)
        if self.id != res.id and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)):
            # When we refresh an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so
            # the ID of this item changes.
            raise ValueError("'id' mismatch in returned update response")
        for f in self.FIELDS:
            setattr(self, f.name, getattr(res, f.name))
        # 'parent_item' should point to 'self', not 'fresh_item'. That way, 'fresh_item' can be garbage collected.
        for a in self.attachments:
            a.parent_item = self
        return self

    @require_id
    def copy(self, to_folder):
        # If 'to_folder' is a public folder or a folder in a different mailbox then None is returned
        return CopyItem(account=self.account).get(
            items=[self],
            to_folder=to_folder,
            expect_result=None,
        )

    @require_id
    def move(self, to_folder):
        res = MoveItem(account=self.account).get(
            items=[self],
            to_folder=to_folder,
            expect_result=None,
        )
        if res is None:
            # Assume 'to_folder' is a public folder or a folder in a different mailbox
            self._id = None
            return
        self._id = self.ID_ELEMENT_CLS(*res)
        self.folder = to_folder

    def move_to_trash(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES,
                      suppress_read_receipts=True):
        # Delete and move to the trash folder.
        self._delete(delete_type=MOVE_TO_DELETED_ITEMS, send_meeting_cancellations=send_meeting_cancellations,
                     affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts)
        self._id = None
        self.folder = self.account.trash

    def soft_delete(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES,
                    suppress_read_receipts=True):
        # Delete and move to the dumpster, if it is enabled.
        self._delete(delete_type=SOFT_DELETE, send_meeting_cancellations=send_meeting_cancellations,
                     affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts)
        self._id = None
        self.folder = self.account.recoverable_items_deletions

    def delete(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES,
               suppress_read_receipts=True):
        # Remove the item permanently. No copies are stored anywhere.
        self._delete(delete_type=HARD_DELETE, send_meeting_cancellations=send_meeting_cancellations,
                     affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts)
        self._id, self.folder = None, None

    @require_id
    def _delete(self, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts):
        DeleteItem(account=self.account).get(
            items=[self],
            delete_type=delete_type,
            send_meeting_cancellations=send_meeting_cancellations,
            affected_task_occurrences=affected_task_occurrences,
            suppress_read_receipts=suppress_read_receipts,
        )

    @require_id
    def archive(self, to_folder):
        return ArchiveItem(account=self.account).get(items=[self], to_folder=to_folder, expect_result=True)

    def attach(self, attachments):
        """Add an attachment, or a list of attachments, to this item. If the item has already been saved, the
        attachments will be created on the server immediately. If the item has not yet been saved, the attachments will
        be created on the server when the item is saved.

        Adding attachments to an existing item will update the changekey of the item.

        :param attachments:
        """
        if not is_iterable(attachments, generators_allowed=True):
            attachments = [attachments]
        for a in attachments:
            if not a.parent_item:
                a.parent_item = self
            if self.id and not a.attachment_id:
                # Already saved object. Attach the attachment server-side now
                a.attach()
            if a not in self.attachments:
                self.attachments.append(a)

    def detach(self, attachments):
        """Remove an attachment, or a list of attachments, from this item. If the item has already been saved, the
        attachments will be deleted on the server immediately. If the item has not yet been saved, the attachments will
        simply not be created on the server the item is saved.

        Removing attachments from an existing item will update the changekey of the item.

        :param attachments:
        """
        if not is_iterable(attachments, generators_allowed=True):
            attachments = [attachments]
        if attachments is self.attachments:
            # Don't remove from the same list we are iterating
            attachments = list(attachments)
        for a in attachments:
            if a.parent_item is not self:
                raise ValueError('Attachment does not belong to this item')
            if self.id:
                # Item is already created. Detach  the attachment server-side now
                a.detach()
            if a in self.attachments:
                self.attachments.remove(a)

    @require_id
    def create_forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None):
        from .message import ForwardItem
        return ForwardItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            subject=subject,
            new_body=body,
            to_recipients=to_recipients,
            cc_recipients=cc_recipients,
            bcc_recipients=bcc_recipients,
        )

    def forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None):
        self.create_forward(
            subject,
            body,
            to_recipients,
            cc_recipients,
            bcc_recipients,
        ).send()

Ancestors

Subclasses

Class variables

var ELEMENT_NAME
var FIELDS
var INSERT_AFTER_FIELD

Instance variables

var attachments
var body
var categories
var conversation_id
var culture
var datetime_created
var datetime_received
var datetime_sent
var display_cc
var display_to
var effective_rights
var has_attachments
var headers
var importance
var in_reply_to
var is_associated
var is_draft
var is_from_me
var is_resend
var is_submitted
var is_unmodified
var item_class
var last_modified_name
var last_modified_time
var mime_content
var parent_folder_id
var reminder_due_by
var reminder_is_set
var reminder_minutes_before_start
var response_objects
var sensitivity
var size
var subject
var text_body
var unique_body
var web_client_edit_form_query_string
var web_client_read_form_query_string

Methods

def archive(self, to_folder)
Expand source code
@require_id
def archive(self, to_folder):
    return ArchiveItem(account=self.account).get(items=[self], to_folder=to_folder, expect_result=True)
def attach(self, attachments)

Add an attachment, or a list of attachments, to this item. If the item has already been saved, the attachments will be created on the server immediately. If the item has not yet been saved, the attachments will be created on the server when the item is saved.

Adding attachments to an existing item will update the changekey of the item.

:param attachments:

Expand source code
def attach(self, attachments):
    """Add an attachment, or a list of attachments, to this item. If the item has already been saved, the
    attachments will be created on the server immediately. If the item has not yet been saved, the attachments will
    be created on the server when the item is saved.

    Adding attachments to an existing item will update the changekey of the item.

    :param attachments:
    """
    if not is_iterable(attachments, generators_allowed=True):
        attachments = [attachments]
    for a in attachments:
        if not a.parent_item:
            a.parent_item = self
        if self.id and not a.attachment_id:
            # Already saved object. Attach the attachment server-side now
            a.attach()
        if a not in self.attachments:
            self.attachments.append(a)
def copy(self, to_folder)
Expand source code
@require_id
def copy(self, to_folder):
    # If 'to_folder' is a public folder or a folder in a different mailbox then None is returned
    return CopyItem(account=self.account).get(
        items=[self],
        to_folder=to_folder,
        expect_result=None,
    )
def create_forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None)
Expand source code
@require_id
def create_forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None):
    from .message import ForwardItem
    return ForwardItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        subject=subject,
        new_body=body,
        to_recipients=to_recipients,
        cc_recipients=cc_recipients,
        bcc_recipients=bcc_recipients,
    )
def delete(self, send_meeting_cancellations='SendToNone', affected_task_occurrences='AllOccurrences', suppress_read_receipts=True)
Expand source code
def delete(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES,
           suppress_read_receipts=True):
    # Remove the item permanently. No copies are stored anywhere.
    self._delete(delete_type=HARD_DELETE, send_meeting_cancellations=send_meeting_cancellations,
                 affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts)
    self._id, self.folder = None, None
def detach(self, attachments)

Remove an attachment, or a list of attachments, from this item. If the item has already been saved, the attachments will be deleted on the server immediately. If the item has not yet been saved, the attachments will simply not be created on the server the item is saved.

Removing attachments from an existing item will update the changekey of the item.

:param attachments:

Expand source code
def detach(self, attachments):
    """Remove an attachment, or a list of attachments, from this item. If the item has already been saved, the
    attachments will be deleted on the server immediately. If the item has not yet been saved, the attachments will
    simply not be created on the server the item is saved.

    Removing attachments from an existing item will update the changekey of the item.

    :param attachments:
    """
    if not is_iterable(attachments, generators_allowed=True):
        attachments = [attachments]
    if attachments is self.attachments:
        # Don't remove from the same list we are iterating
        attachments = list(attachments)
    for a in attachments:
        if a.parent_item is not self:
            raise ValueError('Attachment does not belong to this item')
        if self.id:
            # Item is already created. Detach  the attachment server-side now
            a.detach()
        if a in self.attachments:
            self.attachments.remove(a)
def forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None)
Expand source code
def forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None):
    self.create_forward(
        subject,
        body,
        to_recipients,
        cc_recipients,
        bcc_recipients,
    ).send()
def move(self, to_folder)
Expand source code
@require_id
def move(self, to_folder):
    res = MoveItem(account=self.account).get(
        items=[self],
        to_folder=to_folder,
        expect_result=None,
    )
    if res is None:
        # Assume 'to_folder' is a public folder or a folder in a different mailbox
        self._id = None
        return
    self._id = self.ID_ELEMENT_CLS(*res)
    self.folder = to_folder
def move_to_trash(self, send_meeting_cancellations='SendToNone', affected_task_occurrences='AllOccurrences', suppress_read_receipts=True)
Expand source code
def move_to_trash(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES,
                  suppress_read_receipts=True):
    # Delete and move to the trash folder.
    self._delete(delete_type=MOVE_TO_DELETED_ITEMS, send_meeting_cancellations=send_meeting_cancellations,
                 affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts)
    self._id = None
    self.folder = self.account.trash
def refresh(self)
Expand source code
@require_id
def refresh(self):
    # Updates the item based on fresh data from EWS
    from ..folders import Folder
    additional_fields = {
        FieldPath(field=f) for f in Folder(root=self.account.root).allowed_item_fields(version=self.account.version)
    }
    res = GetItem(account=self.account).get(items=[self], additional_fields=additional_fields, shape=ID_ONLY)
    if self.id != res.id and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)):
        # When we refresh an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so
        # the ID of this item changes.
        raise ValueError("'id' mismatch in returned update response")
    for f in self.FIELDS:
        setattr(self, f.name, getattr(res, f.name))
    # 'parent_item' should point to 'self', not 'fresh_item'. That way, 'fresh_item' can be garbage collected.
    for a in self.attachments:
        a.parent_item = self
    return self
def save(self, update_fields=None, conflict_resolution='AutoResolve', send_meeting_invitations='SendToNone')
Expand source code
def save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE):
    from .task import Task
    if self.id:
        item_id, changekey = self._update(
            update_fieldnames=update_fields,
            message_disposition=SAVE_ONLY,
            conflict_resolution=conflict_resolution,
            send_meeting_invitations=send_meeting_invitations
        )
        if self.id != item_id \
                and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)) \
                and not isinstance(self, Task):
            # When we update an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so
            # the ID of this item changes.
            #
            # When we update certain fields on a task, the ID may change. A full description is available at
            # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation-task
            raise ValueError("'id' mismatch in returned update response")
        # Don't check that changekeys are different. No-op saves will sometimes leave the changekey intact
        self._id = self.ID_ELEMENT_CLS(item_id, changekey)
    else:
        if update_fields:
            raise ValueError("'update_fields' is only valid for updates")
        tmp_attachments = None
        if self.account and self.account.version.build < EXCHANGE_2013 and self.attachments:
            # At least some versions prior to Exchange 2013 can't save attachments immediately. You need to first
            # save, then attach. Store the attachment of this item temporarily and attach later.
            tmp_attachments, self.attachments = self.attachments, []
        item = self._create(message_disposition=SAVE_ONLY, send_meeting_invitations=send_meeting_invitations)
        self._id = self.ID_ELEMENT_CLS(item.id, item.changekey)
        for old_att, new_att in zip(self.attachments, item.attachments):
            if old_att.attachment_id is not None:
                raise ValueError("Old 'attachment_id' is not empty")
            if new_att.attachment_id is None:
                raise ValueError("New 'attachment_id' is empty")
            old_att.attachment_id = new_att.attachment_id
        if tmp_attachments:
            # Exchange 2007 workaround. See above
            self.attach(tmp_attachments)
    return self
def soft_delete(self, send_meeting_cancellations='SendToNone', affected_task_occurrences='AllOccurrences', suppress_read_receipts=True)
Expand source code
def soft_delete(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES,
                suppress_read_receipts=True):
    # Delete and move to the dumpster, if it is enabled.
    self._delete(delete_type=SOFT_DELETE, send_meeting_cancellations=send_meeting_cancellations,
                 affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts)
    self._id = None
    self.folder = self.account.recoverable_items_deletions

Inherited members

exchangelib-4.6.1/docs/exchangelib/items/message.html000066400000000000000000001446271414601472700226530ustar00rootroot00000000000000 exchangelib.items.message API documentation

Module exchangelib.items.message

Expand source code
import logging

from .base import BaseReplyItem, AUTO_RESOLVE, SEND_TO_NONE, SEND_ONLY, SEND_AND_SAVE_COPY
from .item import Item
from ..fields import BooleanField, Base64Field, TextField, MailboxField, MailboxListField, CharField, EWSElementField
from ..properties import ReferenceItemId, ReminderMessageData
from ..services import SendItem, MarkAsJunk
from ..util import require_account, require_id
from ..version import EXCHANGE_2013, EXCHANGE_2013_SP1

log = logging.getLogger(__name__)


class Message(Item):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/message-ex15websvcsotherref
    """

    ELEMENT_NAME = 'Message'

    sender = MailboxField(field_uri='message:Sender', is_read_only=True, is_read_only_after_send=True)
    to_recipients = MailboxListField(field_uri='message:ToRecipients', is_read_only_after_send=True,
                                     is_searchable=False)
    cc_recipients = MailboxListField(field_uri='message:CcRecipients', is_read_only_after_send=True,
                                     is_searchable=False)
    bcc_recipients = MailboxListField(field_uri='message:BccRecipients', is_read_only_after_send=True,
                                      is_searchable=False)
    is_read_receipt_requested = BooleanField(field_uri='message:IsReadReceiptRequested',
                                             is_required=True, default=False, is_read_only_after_send=True)
    is_delivery_receipt_requested = BooleanField(field_uri='message:IsDeliveryReceiptRequested', is_required=True,
                                                 default=False, is_read_only_after_send=True)
    conversation_index = Base64Field(field_uri='message:ConversationIndex', is_read_only=True)
    conversation_topic = CharField(field_uri='message:ConversationTopic', is_read_only=True)
    # Rename 'From' to 'author'. We can't use fieldname 'from' since it's a Python keyword.
    author = MailboxField(field_uri='message:From', is_read_only_after_send=True)
    message_id = CharField(field_uri='message:InternetMessageId', is_read_only_after_send=True)
    is_read = BooleanField(field_uri='message:IsRead', is_required=True, default=False)
    is_response_requested = BooleanField(field_uri='message:IsResponseRequested', default=False, is_required=True)
    references = TextField(field_uri='message:References')
    reply_to = MailboxListField(field_uri='message:ReplyTo', is_read_only_after_send=True, is_searchable=False)
    received_by = MailboxField(field_uri='message:ReceivedBy', is_read_only=True)
    received_representing = MailboxField(field_uri='message:ReceivedRepresenting', is_read_only=True)
    reminder_message_data = EWSElementField(field_uri='message:ReminderMessageData', value_cls=ReminderMessageData,
                                            supported_from=EXCHANGE_2013_SP1, is_read_only=True)

    @require_account
    def send(self, save_copy=True, copy_to_folder=None, conflict_resolution=AUTO_RESOLVE,
             send_meeting_invitations=SEND_TO_NONE):
        # Only sends a message. The message can either be an existing draft stored in EWS or a new message that does
        # not yet exist in EWS.
        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.account.sent  # 'Sent' is default EWS behaviour
        if self.id:
            SendItem(account=self.account).get(items=[self], saved_item_folder=copy_to_folder)
            # The item will be deleted from the original folder
            self._id = None
            self.folder = copy_to_folder
            return None

        # New message
        if copy_to_folder:
            # This would better be done via send_and_save() but lets just support it here
            self.folder = copy_to_folder
            return self.send_and_save(conflict_resolution=conflict_resolution,
                                      send_meeting_invitations=send_meeting_invitations)

        if self.account.version.build < EXCHANGE_2013 and self.attachments:
            # At least some versions prior to Exchange 2013 can't send attachments immediately. You need to first save,
            # then attach, then send. This is done in send_and_save(). send() will delete the item again.
            self.send_and_save(conflict_resolution=conflict_resolution,
                               send_meeting_invitations=send_meeting_invitations)
            return None

        self._create(message_disposition=SEND_ONLY, send_meeting_invitations=send_meeting_invitations)
        return None

    def send_and_save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE,
                      send_meeting_invitations=SEND_TO_NONE):
        # Sends Message and saves a copy in the parent folder. Does not return an ItemId.
        if self.id:
            self._update(
                update_fieldnames=update_fields,
                message_disposition=SEND_AND_SAVE_COPY,
                conflict_resolution=conflict_resolution,
                send_meeting_invitations=send_meeting_invitations
            )
        else:
            if self.account.version.build < EXCHANGE_2013 and self.attachments:
                # At least some versions prior to Exchange 2013 can't send-and-save attachments immediately. You need
                # to first save, then attach, then send. This is done in save().
                self.save(update_fields=update_fields, conflict_resolution=conflict_resolution,
                          send_meeting_invitations=send_meeting_invitations)
                self.send(save_copy=False, conflict_resolution=conflict_resolution,
                          send_meeting_invitations=send_meeting_invitations)
            else:
                res = self._create(
                    message_disposition=SEND_AND_SAVE_COPY,
                    send_meeting_invitations=send_meeting_invitations
                )
                if res is not True:
                    raise ValueError('Unexpected response in send-only mode')

    @require_id
    def create_reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
        if to_recipients is None:
            if not self.author:
                raise ValueError("'to_recipients' must be set when message has no 'author'")
            to_recipients = [self.author]
        return ReplyToItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            subject=subject,
            new_body=body,
            to_recipients=to_recipients,
            cc_recipients=cc_recipients,
            bcc_recipients=bcc_recipients,
        )

    def reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
        self.create_reply(
            subject,
            body,
            to_recipients,
            cc_recipients,
            bcc_recipients
        ).send()

    @require_id
    def create_reply_all(self, subject, body):
        to_recipients = list(self.to_recipients) if self.to_recipients else []
        if self.author:
            to_recipients.append(self.author)
        return ReplyAllToItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            subject=subject,
            new_body=body,
            to_recipients=to_recipients,
            cc_recipients=self.cc_recipients,
            bcc_recipients=self.bcc_recipients,
        )

    def reply_all(self, subject, body):
        self.create_reply_all(subject, body).send()

    def mark_as_junk(self, is_junk=True, move_item=True):
        """Mark or un-marks items as junk email.

        :param is_junk: If True, the sender will be added from the blocked sender list. Otherwise, the sender will be
        removed.
        :param move_item: If true, the item will be moved to the junk folder.
        :return:
        """
        res = MarkAsJunk(account=self.account).get(
            items=[self], is_junk=is_junk, move_item=move_item, expect_result=move_item
        )
        if res is None:
            return
        self.folder = self.account.junk if is_junk else self.account.inbox
        self.id, self.changekey = res


class ReplyToItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/replytoitem"""

    ELEMENT_NAME = 'ReplyToItem'


class ReplyAllToItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/replyalltoitem"""

    ELEMENT_NAME = 'ReplyAllToItem'


class ForwardItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/forwarditem"""

    ELEMENT_NAME = 'ForwardItem'

Classes

class ForwardItem (**kwargs)
Expand source code
class ForwardItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/forwarditem"""

    ELEMENT_NAME = 'ForwardItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class Message (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/message-ex15websvcsotherref

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class Message(Item):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/message-ex15websvcsotherref
    """

    ELEMENT_NAME = 'Message'

    sender = MailboxField(field_uri='message:Sender', is_read_only=True, is_read_only_after_send=True)
    to_recipients = MailboxListField(field_uri='message:ToRecipients', is_read_only_after_send=True,
                                     is_searchable=False)
    cc_recipients = MailboxListField(field_uri='message:CcRecipients', is_read_only_after_send=True,
                                     is_searchable=False)
    bcc_recipients = MailboxListField(field_uri='message:BccRecipients', is_read_only_after_send=True,
                                      is_searchable=False)
    is_read_receipt_requested = BooleanField(field_uri='message:IsReadReceiptRequested',
                                             is_required=True, default=False, is_read_only_after_send=True)
    is_delivery_receipt_requested = BooleanField(field_uri='message:IsDeliveryReceiptRequested', is_required=True,
                                                 default=False, is_read_only_after_send=True)
    conversation_index = Base64Field(field_uri='message:ConversationIndex', is_read_only=True)
    conversation_topic = CharField(field_uri='message:ConversationTopic', is_read_only=True)
    # Rename 'From' to 'author'. We can't use fieldname 'from' since it's a Python keyword.
    author = MailboxField(field_uri='message:From', is_read_only_after_send=True)
    message_id = CharField(field_uri='message:InternetMessageId', is_read_only_after_send=True)
    is_read = BooleanField(field_uri='message:IsRead', is_required=True, default=False)
    is_response_requested = BooleanField(field_uri='message:IsResponseRequested', default=False, is_required=True)
    references = TextField(field_uri='message:References')
    reply_to = MailboxListField(field_uri='message:ReplyTo', is_read_only_after_send=True, is_searchable=False)
    received_by = MailboxField(field_uri='message:ReceivedBy', is_read_only=True)
    received_representing = MailboxField(field_uri='message:ReceivedRepresenting', is_read_only=True)
    reminder_message_data = EWSElementField(field_uri='message:ReminderMessageData', value_cls=ReminderMessageData,
                                            supported_from=EXCHANGE_2013_SP1, is_read_only=True)

    @require_account
    def send(self, save_copy=True, copy_to_folder=None, conflict_resolution=AUTO_RESOLVE,
             send_meeting_invitations=SEND_TO_NONE):
        # Only sends a message. The message can either be an existing draft stored in EWS or a new message that does
        # not yet exist in EWS.
        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.account.sent  # 'Sent' is default EWS behaviour
        if self.id:
            SendItem(account=self.account).get(items=[self], saved_item_folder=copy_to_folder)
            # The item will be deleted from the original folder
            self._id = None
            self.folder = copy_to_folder
            return None

        # New message
        if copy_to_folder:
            # This would better be done via send_and_save() but lets just support it here
            self.folder = copy_to_folder
            return self.send_and_save(conflict_resolution=conflict_resolution,
                                      send_meeting_invitations=send_meeting_invitations)

        if self.account.version.build < EXCHANGE_2013 and self.attachments:
            # At least some versions prior to Exchange 2013 can't send attachments immediately. You need to first save,
            # then attach, then send. This is done in send_and_save(). send() will delete the item again.
            self.send_and_save(conflict_resolution=conflict_resolution,
                               send_meeting_invitations=send_meeting_invitations)
            return None

        self._create(message_disposition=SEND_ONLY, send_meeting_invitations=send_meeting_invitations)
        return None

    def send_and_save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE,
                      send_meeting_invitations=SEND_TO_NONE):
        # Sends Message and saves a copy in the parent folder. Does not return an ItemId.
        if self.id:
            self._update(
                update_fieldnames=update_fields,
                message_disposition=SEND_AND_SAVE_COPY,
                conflict_resolution=conflict_resolution,
                send_meeting_invitations=send_meeting_invitations
            )
        else:
            if self.account.version.build < EXCHANGE_2013 and self.attachments:
                # At least some versions prior to Exchange 2013 can't send-and-save attachments immediately. You need
                # to first save, then attach, then send. This is done in save().
                self.save(update_fields=update_fields, conflict_resolution=conflict_resolution,
                          send_meeting_invitations=send_meeting_invitations)
                self.send(save_copy=False, conflict_resolution=conflict_resolution,
                          send_meeting_invitations=send_meeting_invitations)
            else:
                res = self._create(
                    message_disposition=SEND_AND_SAVE_COPY,
                    send_meeting_invitations=send_meeting_invitations
                )
                if res is not True:
                    raise ValueError('Unexpected response in send-only mode')

    @require_id
    def create_reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
        if to_recipients is None:
            if not self.author:
                raise ValueError("'to_recipients' must be set when message has no 'author'")
            to_recipients = [self.author]
        return ReplyToItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            subject=subject,
            new_body=body,
            to_recipients=to_recipients,
            cc_recipients=cc_recipients,
            bcc_recipients=bcc_recipients,
        )

    def reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
        self.create_reply(
            subject,
            body,
            to_recipients,
            cc_recipients,
            bcc_recipients
        ).send()

    @require_id
    def create_reply_all(self, subject, body):
        to_recipients = list(self.to_recipients) if self.to_recipients else []
        if self.author:
            to_recipients.append(self.author)
        return ReplyAllToItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            subject=subject,
            new_body=body,
            to_recipients=to_recipients,
            cc_recipients=self.cc_recipients,
            bcc_recipients=self.bcc_recipients,
        )

    def reply_all(self, subject, body):
        self.create_reply_all(subject, body).send()

    def mark_as_junk(self, is_junk=True, move_item=True):
        """Mark or un-marks items as junk email.

        :param is_junk: If True, the sender will be added from the blocked sender list. Otherwise, the sender will be
        removed.
        :param move_item: If true, the item will be moved to the junk folder.
        :return:
        """
        res = MarkAsJunk(account=self.account).get(
            items=[self], is_junk=is_junk, move_item=move_item, expect_result=move_item
        )
        if res is None:
            return
        self.folder = self.account.junk if is_junk else self.account.inbox
        self.id, self.changekey = res

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var author
var bcc_recipients
var cc_recipients
var conversation_index
var conversation_topic
var is_delivery_receipt_requested
var is_read
var is_read_receipt_requested
var is_response_requested
var message_id
var received_by
var received_representing
var references
var reminder_message_data
var reply_to
var sender
var to_recipients

Methods

def create_reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None)
Expand source code
@require_id
def create_reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
    if to_recipients is None:
        if not self.author:
            raise ValueError("'to_recipients' must be set when message has no 'author'")
        to_recipients = [self.author]
    return ReplyToItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        subject=subject,
        new_body=body,
        to_recipients=to_recipients,
        cc_recipients=cc_recipients,
        bcc_recipients=bcc_recipients,
    )
def create_reply_all(self, subject, body)
Expand source code
@require_id
def create_reply_all(self, subject, body):
    to_recipients = list(self.to_recipients) if self.to_recipients else []
    if self.author:
        to_recipients.append(self.author)
    return ReplyAllToItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        subject=subject,
        new_body=body,
        to_recipients=to_recipients,
        cc_recipients=self.cc_recipients,
        bcc_recipients=self.bcc_recipients,
    )
def mark_as_junk(self, is_junk=True, move_item=True)

Mark or un-marks items as junk email.

:param is_junk: If True, the sender will be added from the blocked sender list. Otherwise, the sender will be removed. :param move_item: If true, the item will be moved to the junk folder. :return:

Expand source code
def mark_as_junk(self, is_junk=True, move_item=True):
    """Mark or un-marks items as junk email.

    :param is_junk: If True, the sender will be added from the blocked sender list. Otherwise, the sender will be
    removed.
    :param move_item: If true, the item will be moved to the junk folder.
    :return:
    """
    res = MarkAsJunk(account=self.account).get(
        items=[self], is_junk=is_junk, move_item=move_item, expect_result=move_item
    )
    if res is None:
        return
    self.folder = self.account.junk if is_junk else self.account.inbox
    self.id, self.changekey = res
def reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None)
Expand source code
def reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
    self.create_reply(
        subject,
        body,
        to_recipients,
        cc_recipients,
        bcc_recipients
    ).send()
def reply_all(self, subject, body)
Expand source code
def reply_all(self, subject, body):
    self.create_reply_all(subject, body).send()
def send(self, save_copy=True, copy_to_folder=None, conflict_resolution='AutoResolve', send_meeting_invitations='SendToNone')
Expand source code
@require_account
def send(self, save_copy=True, copy_to_folder=None, conflict_resolution=AUTO_RESOLVE,
         send_meeting_invitations=SEND_TO_NONE):
    # Only sends a message. The message can either be an existing draft stored in EWS or a new message that does
    # not yet exist in EWS.
    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.account.sent  # 'Sent' is default EWS behaviour
    if self.id:
        SendItem(account=self.account).get(items=[self], saved_item_folder=copy_to_folder)
        # The item will be deleted from the original folder
        self._id = None
        self.folder = copy_to_folder
        return None

    # New message
    if copy_to_folder:
        # This would better be done via send_and_save() but lets just support it here
        self.folder = copy_to_folder
        return self.send_and_save(conflict_resolution=conflict_resolution,
                                  send_meeting_invitations=send_meeting_invitations)

    if self.account.version.build < EXCHANGE_2013 and self.attachments:
        # At least some versions prior to Exchange 2013 can't send attachments immediately. You need to first save,
        # then attach, then send. This is done in send_and_save(). send() will delete the item again.
        self.send_and_save(conflict_resolution=conflict_resolution,
                           send_meeting_invitations=send_meeting_invitations)
        return None

    self._create(message_disposition=SEND_ONLY, send_meeting_invitations=send_meeting_invitations)
    return None
def send_and_save(self, update_fields=None, conflict_resolution='AutoResolve', send_meeting_invitations='SendToNone')
Expand source code
def send_and_save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE,
                  send_meeting_invitations=SEND_TO_NONE):
    # Sends Message and saves a copy in the parent folder. Does not return an ItemId.
    if self.id:
        self._update(
            update_fieldnames=update_fields,
            message_disposition=SEND_AND_SAVE_COPY,
            conflict_resolution=conflict_resolution,
            send_meeting_invitations=send_meeting_invitations
        )
    else:
        if self.account.version.build < EXCHANGE_2013 and self.attachments:
            # At least some versions prior to Exchange 2013 can't send-and-save attachments immediately. You need
            # to first save, then attach, then send. This is done in save().
            self.save(update_fields=update_fields, conflict_resolution=conflict_resolution,
                      send_meeting_invitations=send_meeting_invitations)
            self.send(save_copy=False, conflict_resolution=conflict_resolution,
                      send_meeting_invitations=send_meeting_invitations)
        else:
            res = self._create(
                message_disposition=SEND_AND_SAVE_COPY,
                send_meeting_invitations=send_meeting_invitations
            )
            if res is not True:
                raise ValueError('Unexpected response in send-only mode')

Inherited members

class ReplyAllToItem (**kwargs)
Expand source code
class ReplyAllToItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/replyalltoitem"""

    ELEMENT_NAME = 'ReplyAllToItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class ReplyToItem (**kwargs)
Expand source code
class ReplyToItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/replytoitem"""

    ELEMENT_NAME = 'ReplyToItem'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

exchangelib-4.6.1/docs/exchangelib/items/post.html000066400000000000000000000646471414601472700222170ustar00rootroot00000000000000 exchangelib.items.post API documentation

Module exchangelib.items.post

Expand source code
import logging

from .item import Item
from .message import Message
from ..fields import TextField, BodyField, DateTimeField, MailboxField

log = logging.getLogger(__name__)


class PostItem(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postitem"""

    ELEMENT_NAME = 'PostItem'

    conversation_index = Message.FIELDS['conversation_index']
    conversation_topic = Message.FIELDS['conversation_topic']

    author = Message.FIELDS['author']
    message_id = Message.FIELDS['message_id']
    is_read = Message.FIELDS['is_read']

    posted_time = DateTimeField(field_uri='postitem:PostedTime', is_read_only=True)
    references = TextField(field_uri='message:References')
    sender = MailboxField(field_uri='message:Sender', is_read_only=True, is_read_only_after_send=True)


class PostReplyItem(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postreplyitem"""

    ELEMENT_NAME = 'PostReplyItem'

    # This element only has Item fields up to, and including, 'culture'
    # TDO: Plus all message fields
    new_body = BodyField(field_uri='NewBodyContent')  # Accepts and returns Body or HTMLBody instances

    culture_idx = Item.FIELDS.index_by_name('culture')
    sender_idx = Message.FIELDS.index_by_name('sender')
    FIELDS = Item.FIELDS[:culture_idx + 1] + Message.FIELDS[sender_idx:]

Classes

class PostItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postitem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class PostItem(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postitem"""

    ELEMENT_NAME = 'PostItem'

    conversation_index = Message.FIELDS['conversation_index']
    conversation_topic = Message.FIELDS['conversation_topic']

    author = Message.FIELDS['author']
    message_id = Message.FIELDS['message_id']
    is_read = Message.FIELDS['is_read']

    posted_time = DateTimeField(field_uri='postitem:PostedTime', is_read_only=True)
    references = TextField(field_uri='message:References')
    sender = MailboxField(field_uri='message:Sender', is_read_only=True, is_read_only_after_send=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var author
var conversation_index
var conversation_topic
var is_read
var message_id
var posted_time
var references
var sender

Inherited members

class PostReplyItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postreplyitem

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class PostReplyItem(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postreplyitem"""

    ELEMENT_NAME = 'PostReplyItem'

    # This element only has Item fields up to, and including, 'culture'
    # TDO: Plus all message fields
    new_body = BodyField(field_uri='NewBodyContent')  # Accepts and returns Body or HTMLBody instances

    culture_idx = Item.FIELDS.index_by_name('culture')
    sender_idx = Message.FIELDS.index_by_name('sender')
    FIELDS = Item.FIELDS[:culture_idx + 1] + Message.FIELDS[sender_idx:]

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var culture_idx
var sender_idx

Instance variables

var author
var bcc_recipients
var cc_recipients
var conversation_index
var conversation_topic
var is_delivery_receipt_requested
var is_read
var is_read_receipt_requested
var is_response_requested
var message_id
var new_body
var received_by
var received_representing
var references
var reminder_message_data
var reply_to
var sender
var to_recipients

Inherited members

exchangelib-4.6.1/docs/exchangelib/items/task.html000066400000000000000000000764541414601472700221730ustar00rootroot00000000000000 exchangelib.items.task API documentation

Module exchangelib.items.task

Expand source code
import datetime
import logging
from decimal import Decimal

from .item import Item
from ..ewsdatetime import EWSDateTime, UTC
from ..fields import BooleanField, IntegerField, DecimalField, TextField, ChoiceField, DateTimeField, Choice, \
    CharField, TextListField, TaskRecurrenceField, DateTimeBackedDateField

log = logging.getLogger(__name__)


class Task(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/task"""

    ELEMENT_NAME = 'Task'
    NOT_STARTED = 'NotStarted'
    COMPLETED = 'Completed'

    actual_work = IntegerField(field_uri='task:ActualWork', min=0)
    assigned_time = DateTimeField(field_uri='task:AssignedTime', is_read_only=True)
    billing_information = TextField(field_uri='task:BillingInformation')
    change_count = IntegerField(field_uri='task:ChangeCount', is_read_only=True, min=0)
    companies = TextListField(field_uri='task:Companies')
    # 'complete_date' can be set, but is ignored by the server, which sets it to now()
    complete_date = DateTimeField(field_uri='task:CompleteDate', is_read_only=True)
    contacts = TextListField(field_uri='task:Contacts')
    delegation_state = ChoiceField(field_uri='task:DelegationState', choices={
        Choice('NoMatch'), Choice('OwnNew'), Choice('Owned'), Choice('Accepted'), Choice('Declined'), Choice('Max')
    }, is_read_only=True)
    delegator = CharField(field_uri='task:Delegator', is_read_only=True)
    due_date = DateTimeBackedDateField(field_uri='task:DueDate')
    is_editable = BooleanField(field_uri='task:IsAssignmentEditable', is_read_only=True)
    is_complete = BooleanField(field_uri='task:IsComplete', is_read_only=True)
    is_recurring = BooleanField(field_uri='task:IsRecurring', is_read_only=True)
    is_team_task = BooleanField(field_uri='task:IsTeamTask', is_read_only=True)
    mileage = TextField(field_uri='task:Mileage')
    owner = CharField(field_uri='task:Owner', is_read_only=True)
    percent_complete = DecimalField(field_uri='task:PercentComplete', is_required=True, default=Decimal(0.0),
                                    min=Decimal(0), max=Decimal(100), is_searchable=False)
    recurrence = TaskRecurrenceField(field_uri='task:Recurrence', is_searchable=False)
    start_date = DateTimeBackedDateField(field_uri='task:StartDate')
    status = ChoiceField(field_uri='task:Status', choices={
        Choice(NOT_STARTED), Choice('InProgress'), Choice(COMPLETED), Choice('WaitingOnOthers'), Choice('Deferred')
    }, is_required=True, is_searchable=False, default=NOT_STARTED)
    status_description = CharField(field_uri='task:StatusDescription', is_read_only=True)
    total_work = IntegerField(field_uri='task:TotalWork', min=0)

    def clean(self, version=None):
        super().clean(version=version)
        if self.due_date and self.start_date and self.due_date < self.start_date:
            log.warning("'due_date' must be greater than 'start_date' (%s vs %s). Resetting 'due_date'",
                        self.due_date, self.start_date)
            self.due_date = self.start_date
        if self.complete_date:
            if self.status != self.COMPLETED:
                log.warning("'status' must be '%s' when 'complete_date' is set (%s). Resetting",
                            self.COMPLETED, self.status)
                self.status = self.COMPLETED
            now = datetime.datetime.now(tz=UTC)
            if (self.complete_date - now).total_seconds() > 120:
                # Reset complete_date values that are in the future
                # 'complete_date' can be set automatically by the server. Allow some grace between local and server time
                log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now)
                self.complete_date = now
            if self.start_date and self.complete_date.date() < self.start_date:
                log.warning("'complete_date' must be greater than 'start_date' (%s vs %s). Resetting",
                            self.complete_date, self.start_date)
                self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC)
        if self.percent_complete is not None:
            if self.status == self.COMPLETED and self.percent_complete != Decimal(100):
                # percent_complete must be 100% if task is complete
                log.warning("'percent_complete' must be 100 when 'status' is '%s' (%s). Resetting",
                            self.COMPLETED, self.percent_complete)
                self.percent_complete = Decimal(100)
            elif self.status == self.NOT_STARTED and self.percent_complete != Decimal(0):
                # percent_complete must be 0% if task is not started
                log.warning("'percent_complete' must be 0 when 'status' is '%s' (%s). Resetting",
                            self.NOT_STARTED, self.percent_complete)
                self.percent_complete = Decimal(0)

    def complete(self):
        # A helper method to mark a task as complete on the server
        self.status = Task.COMPLETED
        self.percent_complete = Decimal(100)
        self.save()

Classes

class Task (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/task

Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.

:param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account.

Expand source code
class Task(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/task"""

    ELEMENT_NAME = 'Task'
    NOT_STARTED = 'NotStarted'
    COMPLETED = 'Completed'

    actual_work = IntegerField(field_uri='task:ActualWork', min=0)
    assigned_time = DateTimeField(field_uri='task:AssignedTime', is_read_only=True)
    billing_information = TextField(field_uri='task:BillingInformation')
    change_count = IntegerField(field_uri='task:ChangeCount', is_read_only=True, min=0)
    companies = TextListField(field_uri='task:Companies')
    # 'complete_date' can be set, but is ignored by the server, which sets it to now()
    complete_date = DateTimeField(field_uri='task:CompleteDate', is_read_only=True)
    contacts = TextListField(field_uri='task:Contacts')
    delegation_state = ChoiceField(field_uri='task:DelegationState', choices={
        Choice('NoMatch'), Choice('OwnNew'), Choice('Owned'), Choice('Accepted'), Choice('Declined'), Choice('Max')
    }, is_read_only=True)
    delegator = CharField(field_uri='task:Delegator', is_read_only=True)
    due_date = DateTimeBackedDateField(field_uri='task:DueDate')
    is_editable = BooleanField(field_uri='task:IsAssignmentEditable', is_read_only=True)
    is_complete = BooleanField(field_uri='task:IsComplete', is_read_only=True)
    is_recurring = BooleanField(field_uri='task:IsRecurring', is_read_only=True)
    is_team_task = BooleanField(field_uri='task:IsTeamTask', is_read_only=True)
    mileage = TextField(field_uri='task:Mileage')
    owner = CharField(field_uri='task:Owner', is_read_only=True)
    percent_complete = DecimalField(field_uri='task:PercentComplete', is_required=True, default=Decimal(0.0),
                                    min=Decimal(0), max=Decimal(100), is_searchable=False)
    recurrence = TaskRecurrenceField(field_uri='task:Recurrence', is_searchable=False)
    start_date = DateTimeBackedDateField(field_uri='task:StartDate')
    status = ChoiceField(field_uri='task:Status', choices={
        Choice(NOT_STARTED), Choice('InProgress'), Choice(COMPLETED), Choice('WaitingOnOthers'), Choice('Deferred')
    }, is_required=True, is_searchable=False, default=NOT_STARTED)
    status_description = CharField(field_uri='task:StatusDescription', is_read_only=True)
    total_work = IntegerField(field_uri='task:TotalWork', min=0)

    def clean(self, version=None):
        super().clean(version=version)
        if self.due_date and self.start_date and self.due_date < self.start_date:
            log.warning("'due_date' must be greater than 'start_date' (%s vs %s). Resetting 'due_date'",
                        self.due_date, self.start_date)
            self.due_date = self.start_date
        if self.complete_date:
            if self.status != self.COMPLETED:
                log.warning("'status' must be '%s' when 'complete_date' is set (%s). Resetting",
                            self.COMPLETED, self.status)
                self.status = self.COMPLETED
            now = datetime.datetime.now(tz=UTC)
            if (self.complete_date - now).total_seconds() > 120:
                # Reset complete_date values that are in the future
                # 'complete_date' can be set automatically by the server. Allow some grace between local and server time
                log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now)
                self.complete_date = now
            if self.start_date and self.complete_date.date() < self.start_date:
                log.warning("'complete_date' must be greater than 'start_date' (%s vs %s). Resetting",
                            self.complete_date, self.start_date)
                self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC)
        if self.percent_complete is not None:
            if self.status == self.COMPLETED and self.percent_complete != Decimal(100):
                # percent_complete must be 100% if task is complete
                log.warning("'percent_complete' must be 100 when 'status' is '%s' (%s). Resetting",
                            self.COMPLETED, self.percent_complete)
                self.percent_complete = Decimal(100)
            elif self.status == self.NOT_STARTED and self.percent_complete != Decimal(0):
                # percent_complete must be 0% if task is not started
                log.warning("'percent_complete' must be 0 when 'status' is '%s' (%s). Resetting",
                            self.NOT_STARTED, self.percent_complete)
                self.percent_complete = Decimal(0)

    def complete(self):
        # A helper method to mark a task as complete on the server
        self.status = Task.COMPLETED
        self.percent_complete = Decimal(100)
        self.save()

Ancestors

Class variables

var COMPLETED
var ELEMENT_NAME
var FIELDS
var NOT_STARTED

Instance variables

var actual_work
var assigned_time
var billing_information
var change_count
var companies
var complete_date
var contacts
var delegation_state
var delegator
var due_date
var is_complete
var is_editable
var is_recurring
var is_team_task
var mileage
var owner
var percent_complete
var recurrence
var start_date
var status
var status_description
var total_work

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)
    if self.due_date and self.start_date and self.due_date < self.start_date:
        log.warning("'due_date' must be greater than 'start_date' (%s vs %s). Resetting 'due_date'",
                    self.due_date, self.start_date)
        self.due_date = self.start_date
    if self.complete_date:
        if self.status != self.COMPLETED:
            log.warning("'status' must be '%s' when 'complete_date' is set (%s). Resetting",
                        self.COMPLETED, self.status)
            self.status = self.COMPLETED
        now = datetime.datetime.now(tz=UTC)
        if (self.complete_date - now).total_seconds() > 120:
            # Reset complete_date values that are in the future
            # 'complete_date' can be set automatically by the server. Allow some grace between local and server time
            log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now)
            self.complete_date = now
        if self.start_date and self.complete_date.date() < self.start_date:
            log.warning("'complete_date' must be greater than 'start_date' (%s vs %s). Resetting",
                        self.complete_date, self.start_date)
            self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC)
    if self.percent_complete is not None:
        if self.status == self.COMPLETED and self.percent_complete != Decimal(100):
            # percent_complete must be 100% if task is complete
            log.warning("'percent_complete' must be 100 when 'status' is '%s' (%s). Resetting",
                        self.COMPLETED, self.percent_complete)
            self.percent_complete = Decimal(100)
        elif self.status == self.NOT_STARTED and self.percent_complete != Decimal(0):
            # percent_complete must be 0% if task is not started
            log.warning("'percent_complete' must be 0 when 'status' is '%s' (%s). Resetting",
                        self.NOT_STARTED, self.percent_complete)
            self.percent_complete = Decimal(0)
def complete(self)
Expand source code
def complete(self):
    # A helper method to mark a task as complete on the server
    self.status = Task.COMPLETED
    self.percent_complete = Decimal(100)
    self.save()

Inherited members

exchangelib-4.6.1/docs/exchangelib/properties.html000066400000000000000000020071121414601472700222670ustar00rootroot00000000000000 exchangelib.properties API documentation

Module exchangelib.properties

Expand source code
import abc
import binascii
import codecs
import datetime
import logging
import struct
from inspect import getmro
from threading import Lock

from .errors import TimezoneDefinitionInvalidForYear
from .fields import SubField, TextField, EmailAddressField, ChoiceField, DateTimeField, EWSElementField, MailboxField, \
    Choice, BooleanField, IdField, ExtendedPropertyField, IntegerField, TimeField, EnumField, CharField, EmailField, \
    EWSElementListField, EnumListField, FreeBusyStatusField, UnknownEntriesField, MessageField, RecipientAddressField, \
    RoutingTypeField, WEEKDAY_NAMES, FieldPath, Field, AssociatedCalendarItemIdField, ReferenceItemIdField, \
    Base64Field, TypeValueField, DictionaryField, IdElementField, CharListField, GenericEventListField, \
    InvalidField, InvalidFieldForVersion
from .util import get_xml_attr, create_element, set_xml_value, value_to_xml_text, MNS, TNS
from .version import Version, EXCHANGE_2013, Build

log = logging.getLogger(__name__)


class Fields(list):
    """A collection type for the FIELDS class attribute. Works like a list but supports fast lookup by name."""

    def __init__(self, *fields):
        super().__init__(fields)
        self._dict = {}
        for f in fields:
            # Check for duplicate field names
            if f.name in self._dict:
                raise ValueError('Field %r is a duplicate' % f)
            self._dict[f.name] = f

    def __getitem__(self, idx_or_slice):
        # Support fast lookup by name. Make sure slicing returns an instance of this class
        if isinstance(idx_or_slice, str):
            return self._dict[idx_or_slice]
        if isinstance(idx_or_slice, int):
            return super().__getitem__(idx_or_slice)
        res = super().__getitem__(idx_or_slice)
        return self.__class__(*res)

    def __add__(self, other):
        # Make sure addition returns an instance of this class
        res = super().__add__(other)
        return self.__class__(*res)

    def __iadd__(self, other):
        for f in other:
            self.append(f)
        return self

    def __contains__(self, item):
        if isinstance(item, str):
            return item in self._dict
        return super().__contains__(item)

    def copy(self):
        return self.__class__(*self)

    def index_by_name(self, field_name):
        for i, f in enumerate(self):
            if f.name == field_name:
                return i
        raise ValueError('Unknown field name %r' % field_name)

    def insert(self, index, field):
        if field.name in self._dict:
            raise ValueError('Field %r is a duplicate' % field)
        super().insert(index, field)
        self._dict[field.name] = field

    def remove(self, field):
        super().remove(field)
        del self._dict[field.name]

    def append(self, field):
        super().append(field)
        self._dict[field.name] = field


class Body(str):
    """Helper to mark the 'body' field as a complex attribute.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/body
    """

    body_type = 'Text'

    def __add__(self, other):
        # Make sure Body('') + 'foo' returns a Body type
        return self.__class__(super().__add__(other))

    def __mod__(self, other):
        # Make sure Body('%s') % 'foo' returns a Body type
        return self.__class__(super().__mod__(other))

    def format(self, *args, **kwargs):
        # Make sure Body('{}').format('foo') returns a Body type
        return self.__class__(super().format(*args, **kwargs))


class HTMLBody(Body):
    """Helper to mark the 'body' field as a complex attribute.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/body
    """

    body_type = 'HTML'


class UID(bytes):
    """Helper class to encode Calendar UIDs. See issue #453. Example:

    class GlobalObjectId(ExtendedProperty):
        distinguished_property_set_id = 'Meeting'
        property_id = 3
        property_type = 'Binary'

    CalendarItem.register('global_object_id', GlobalObjectId)
    account.calendar.filter(global_object_id=UID('261cbc18-1f65-5a0a-bd11-23b1e224cc2f'))
    """

    _HEADER = binascii.hexlify(bytearray((
        0x04, 0x00, 0x00, 0x00,
        0x82, 0x00, 0xE0, 0x00,
        0x74, 0xC5, 0xB7, 0x10,
        0x1A, 0x82, 0xE0, 0x08)))

    _EXCEPTION_REPLACEMENT_TIME = binascii.hexlify(bytearray((
        0, 0, 0, 0)))

    _CREATION_TIME = binascii.hexlify(bytearray((
        0, 0, 0, 0,
        0, 0, 0, 0)))

    _RESERVED = binascii.hexlify(bytearray((
        0, 0, 0, 0,
        0, 0, 0, 0)))

    # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxocal/1d3aac05-a7b9-45cc-a213-47f0a0a2c5c1
    # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-asemail/e7424ddc-dd10-431e-a0b7-5c794863370e
    # https://stackoverflow.com/questions/42259122
    # https://stackoverflow.com/questions/33757805

    def __new__(cls, uid):
        payload = binascii.hexlify(bytearray('vCal-Uid\x01\x00\x00\x00{}\x00'.format(uid).encode('ascii')))
        length = binascii.hexlify(bytearray(struct.pack('<I', int(len(payload)/2))))
        encoding = b''.join([
            cls._HEADER, cls._EXCEPTION_REPLACEMENT_TIME, cls._CREATION_TIME, cls._RESERVED, length, payload
        ])
        return super().__new__(cls, codecs.decode(encoding, 'hex'))

    @classmethod
    def to_global_object_id(cls, uid):
        """Converts a UID as returned by EWS to GlobalObjectId format"""
        return binascii.unhexlify(uid)


def _mangle(field_name):
    return '__%s' % field_name


class EWSMeta(type, metaclass=abc.ABCMeta):
    def __new__(mcs, name, bases, kwargs):
        # Collect fields defined directly on the class
        local_fields = Fields()
        for k in tuple(kwargs.keys()):
            v = kwargs[k]
            if isinstance(v, Field):
                v.name = k
                local_fields.append(v)
                del kwargs[k]

        # Build a list of fields defined on this and all base classes
        base_fields = Fields()
        for base in bases:
            if hasattr(base, 'FIELDS'):
                base_fields += base.FIELDS

        # FIELDS defined on a model overrides the base class fields
        fields = kwargs.get('FIELDS', base_fields) + local_fields

        # Include all fields as class attributes so we can use them as instance attributes
        kwargs.update({_mangle(f.name): f for f in fields})

        # Calculate __slots__ so we don't have to hard-code it on the model
        kwargs['__slots__'] = tuple(f.name for f in fields if f.name not in base_fields) + kwargs.get('__slots__', ())

        # FIELDS is mentioned in docs and expected by internal code. Add it here, but only if the class has its own
        # fields. Otherwise, we want the implicit FIELDS from the base class (used for injecting custom fields on the
        # Folder class, making the custom field available for subclasses).
        if local_fields:
            kwargs['FIELDS'] = fields
        cls = super().__new__(mcs, name, bases, kwargs)
        cls._slots_keys = mcs._get_slots_keys(cls)
        return cls

    @staticmethod
    def _get_slots_keys(cls):
        seen = set()
        keys = []
        for c in reversed(getmro(cls)):
            if not hasattr(c, '__slots__'):
                continue
            for k in c.__slots__:
                if k in seen:
                    # We allow duplicate keys because we don't want to require subclasses of e.g.
                    # ExtendedProperty to define an empty __slots__ class attribute.
                    continue
                keys.append(k)
                seen.add(k)
        return keys

    def __getattribute__(cls, k):
        """Return Field instances via their mangled class attribute"""
        try:
            return super().__getattribute__('__dict__')[_mangle(k)]
        except KeyError:
            return super().__getattribute__(k)


class EWSElement(metaclass=EWSMeta):
    """Base class for all XML element implementations."""

    ELEMENT_NAME = None  # The name of the XML tag
    FIELDS = Fields()  # A list of attributes supported by this item class, ordered the same way as in EWS documentation
    NAMESPACE = TNS  # The XML tag namespace. Either TNS or MNS

    _fields_lock = Lock()

    def __init__(self, **kwargs):
        for f in self.FIELDS:
            setattr(self, f.name, kwargs.pop(f.name, None))
        if kwargs:
            raise AttributeError("%s are invalid kwargs for this class" % ', '.join("'%s'" % k for k in kwargs))

    def __setattr__(self, key, value):
        # Avoid silently accepting spelling errors to field names that are not set via __init__. We need to be able to
        # set values for predefined and registered fields, whatever non-field attributes this class defines, and
        # property setters.
        if key in self.FIELDS:
            return super().__setattr__(key, value)
        if key in self._slots_keys:
            return super().__setattr__(key, value)
        if hasattr(self, key):
            # Property setters
            return super().__setattr__(key, value)
        raise AttributeError('%r is not a valid attribute. See %s.FIELDS for valid field names' % (
            key, self.__class__.__name__))

    def clean(self, version=None):
        # Validate attribute values using the field validator
        for f in self.FIELDS:
            if version and not f.supports_version(version):
                continue
            if isinstance(f, ExtendedPropertyField) and not hasattr(self, f.name):
                # The extended field may have been registered after this item was created. Set default values.
                setattr(self, f.name, f.clean(None, version=version))
                continue
            val = getattr(self, f.name)
            setattr(self, f.name, f.clean(val, version=version))

    @staticmethod
    def _clear(elem):
        # Clears an XML element to reduce memory consumption
        elem.clear()
        # Don't attempt to clean up previous siblings. We may not have parsed them yet.
        parent = elem.getparent()
        if parent is None:
            return
        parent.remove(elem)

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
        cls._clear(elem)
        return cls(**kwargs)

    def to_xml(self, version):
        self.clean(version=version)
        # WARNING: The order of addition of XML elements is VERY important. Exchange expects XML elements in a
        # specific, non-documented order and will fail with meaningless errors if the order is wrong.

        # Call create_element() without args, to not fill up the cache with unique attribute values.
        elem = create_element(self.request_tag())

        # Add attributes
        for f in self.attribute_fields():
            if f.is_read_only:
                continue
            value = getattr(self, f.name)
            if value is None or (f.is_list and not value):
                continue
            elem.set(f.field_uri, value_to_xml_text(getattr(self, f.name)))

        # Add elements and values
        for f in self.supported_fields(version=version):
            if f.is_read_only:
                continue
            value = getattr(self, f.name)
            if value is None or (f.is_list and not value):
                continue
            set_xml_value(elem, f.to_xml(value, version=version), version)
        return elem

    @classmethod
    def request_tag(cls):
        if not cls.ELEMENT_NAME:
            raise ValueError('Class %s is missing the ELEMENT_NAME attribute' % cls)
        return {
            TNS: 't:%s' % cls.ELEMENT_NAME,
            MNS: 'm:%s' % cls.ELEMENT_NAME,
        }[cls.NAMESPACE]

    @classmethod
    def response_tag(cls):
        if not cls.NAMESPACE:
            raise ValueError('Class %s is missing the NAMESPACE attribute' % cls)
        if not cls.ELEMENT_NAME:
            raise ValueError('Class %s is missing the ELEMENT_NAME attribute' % cls)
        return '{%s}%s' % (cls.NAMESPACE, cls.ELEMENT_NAME)

    @classmethod
    def attribute_fields(cls):
        return tuple(f for f in cls.FIELDS if f.is_attribute)

    @classmethod
    def supported_fields(cls, version):
        """Return the fields supported by the given server version."""

        return tuple(f for f in cls.FIELDS if not f.is_attribute and f.supports_version(version))

    @classmethod
    def get_field_by_fieldname(cls, fieldname):
        try:
            return cls.FIELDS[fieldname]
        except KeyError:
            raise InvalidField("'%s' is not a valid field name on '%s'" % (fieldname, cls.__name__))

    @classmethod
    def validate_field(cls, field, version):
        """Take a list of fieldnames, Field or FieldPath objects pointing to item fields, and check that they are
        valid for the given version.

        :param field:
        :param version:
        """
        if not isinstance(version, Version):
            raise ValueError("'version' %r must be a Version instance" % version)
        # Allow both Field and FieldPath instances and string field paths as input
        if isinstance(field, str):
            field = cls.get_field_by_fieldname(fieldname=field)
        elif isinstance(field, FieldPath):
            field = field.field
        if not isinstance(field, Field):
            raise ValueError("Field %r must be a string, Field or FieldPath instance" % field)
        cls.get_field_by_fieldname(fieldname=field.name)  # Will raise if field name is invalid
        if not field.supports_version(version):
            # The field exists but is not valid for this version
            raise InvalidFieldForVersion(
                "Field '%s' is not supported on server version %s (supported from: %s, deprecated from: %s)"
                % (field.name, version, field.supported_from, field.deprecated_from))

    @classmethod
    def add_field(cls, field, insert_after):
        """Insert a new field at the preferred place in the tuple and update the slots cache.

        :param field:
        :param insert_after:
        """
        with cls._fields_lock:
            idx = cls.FIELDS.index_by_name(insert_after) + 1
            # This class may not have its own FIELDS attribute. Make sure not to edit an attribute belonging to a parent
            # class.
            cls.FIELDS.insert(idx, field)
            setattr(cls, _mangle(field.name), field)

    @classmethod
    def remove_field(cls, field):
        """Remove the given field and and update the slots cache.

        :param field:
        """
        with cls._fields_lock:
            # This class may not have its own FIELDS attribute. Make sure not to edit an attribute belonging to a parent
            # class.
            cls.FIELDS.remove(field)
            delattr(cls, _mangle(field.name))

    def __eq__(self, other):
        return hash(self) == hash(other)

    def __hash__(self):
        return hash(
            tuple(tuple(getattr(self, f.name) or ()) if f.is_list else getattr(self, f.name) for f in self.FIELDS)
        )

    def _field_vals(self):
        field_vals = []  # Keep sorting
        for f in self.FIELDS:
            val = getattr(self, f.name)
            if isinstance(f, EnumField) and isinstance(val, int):
                val = f.as_string(val)
            field_vals.append((f.name, val))
        return field_vals

    def __str__(self):
        return self.__class__.__name__ + '(%s)' % ', '.join(
            '%s=%r' % (name, val) for name, val in self._field_vals() if val is not None
        )

    def __repr__(self):
        return self.__class__.__name__ + '(%s)' % ', '.join(
            '%s=%r' % (name, val) for name, val in self._field_vals()
        )


class MessageHeader(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/internetmessageheader"""

    ELEMENT_NAME = 'InternetMessageHeader'

    name = TextField(field_uri='HeaderName', is_attribute=True)
    value = SubField()


class BaseItemId(EWSElement):
    """Base class for ItemId elements."""

    ID_ATTR = None
    CHANGEKEY_ATTR = None

    def __init__(self, *args, **kwargs):
        if not kwargs:
            # Allow to set attributes without keyword
            kwargs = dict(zip(self._slots_keys, args))
        super().__init__(**kwargs)


class ItemId(BaseItemId):
    """'id' and 'changekey' are UUIDs generated by Exchange.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/itemid
    """

    ELEMENT_NAME = 'ItemId'
    ID_ATTR = 'Id'
    CHANGEKEY_ATTR = 'ChangeKey'

    id = IdField(field_uri=ID_ATTR, is_required=True)
    changekey = IdField(field_uri=CHANGEKEY_ATTR, is_required=False)


class ParentItemId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/parentitemid"""

    ELEMENT_NAME = 'ParentItemId'
    NAMESPACE = MNS


class RootItemId(BaseItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/rootitemid"""

    ELEMENT_NAME = 'RootItemId'
    NAMESPACE = MNS
    ID_ATTR = 'RootItemId'
    CHANGEKEY_ATTR = 'RootItemChangeKey'

    id = IdField(field_uri=ID_ATTR, is_required=True)
    changekey = IdField(field_uri=CHANGEKEY_ATTR, is_required=True)


class AssociatedCalendarItemId(ItemId):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/associatedcalendaritemid
    """

    ELEMENT_NAME = 'AssociatedCalendarItemId'


class ConversationId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/conversationid"""

    ELEMENT_NAME = 'ConversationId'

    # ChangeKey attribute is sometimes required, see MSDN link


class ParentFolderId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/parentfolderid"""

    ELEMENT_NAME = 'ParentFolderId'


class ReferenceItemId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/referenceitemid"""

    ELEMENT_NAME = 'ReferenceItemId'


class PersonaId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/personaid"""

    ELEMENT_NAME = 'PersonaId'
    NAMESPACE = MNS

    @classmethod
    def response_tag(cls):
        # This element is in MNS in the request and TNS in the response...
        return '{%s}%s' % (TNS, cls.ELEMENT_NAME)


class SourceId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/sourceid"""

    ELEMENT_NAME = 'SourceId'


class FolderId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/folderid"""

    ELEMENT_NAME = 'FolderId'


class RecurringMasterItemId(BaseItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/recurringmasteritemid"""

    ELEMENT_NAME = 'RecurringMasterItemId'
    ID_ATTR = 'OccurrenceId'
    CHANGEKEY_ATTR = 'ChangeKey'

    id = IdField(field_uri=ID_ATTR, is_required=True)
    changekey = IdField(field_uri=CHANGEKEY_ATTR, is_required=False)


class OccurrenceItemId(BaseItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/occurrenceitemid"""

    ELEMENT_NAME = 'OccurrenceItemId'
    ID_ATTR = 'RecurringMasterId'
    CHANGEKEY_ATTR = 'ChangeKey'

    id = IdField(field_uri=ID_ATTR, is_required=True)
    changekey = IdField(field_uri=CHANGEKEY_ATTR, is_required=False)
    instance_index = IntegerField(field_uri='InstanceIndex', is_attribute=True, is_required=True, min=1)


class MovedItemId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/moveditemid"""

    ELEMENT_NAME = 'MovedItemId'
    NAMESPACE = MNS

    @classmethod
    def id_from_xml(cls, elem):
        item = cls.from_xml(elem=elem, account=None)
        return item.id, item.changekey


class Mailbox(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailbox"""

    ELEMENT_NAME = 'Mailbox'
    MAILBOX = 'Mailbox'
    ONE_OFF = 'OneOff'
    MAILBOX_TYPE_CHOICES = {
            Choice(MAILBOX), Choice('PublicDL'), Choice('PrivateDL'), Choice('Contact'), Choice('PublicFolder'),
            Choice('Unknown'), Choice(ONE_OFF), Choice('GroupMailbox', supported_from=EXCHANGE_2013)
        }

    name = TextField(field_uri='Name')
    email_address = EmailAddressField(field_uri='EmailAddress')
    # RoutingType values are not restricted:
    # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/routingtype-emailaddresstype
    routing_type = TextField(field_uri='RoutingType', default='SMTP')
    mailbox_type = ChoiceField(field_uri='MailboxType', choices=MAILBOX_TYPE_CHOICES, default=MAILBOX)
    item_id = EWSElementField(value_cls=ItemId, is_read_only=True)

    def clean(self, version=None):
        super().clean(version=version)

        if self.mailbox_type != self.ONE_OFF and not self.email_address and not self.item_id:
            # A OneOff Mailbox (a one-off member of a personal distribution list) may lack these fields, but other
            # Mailboxes require at least one. See also "Remarks" section of
            # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailbox
            raise ValueError("Mailbox type %r must have either 'email_address' or 'item_id' set" % self.mailbox_type)

    def __hash__(self):
        # Exchange may add 'mailbox_type' and 'name' on insert. We're satisfied if the item_id or email address matches.
        if self.item_id:
            return hash(self.item_id)
        if self.email_address:
            return hash(self.email_address.lower())
        return super().__hash__()


class DLMailbox(Mailbox):
    """Like Mailbox, but creates elements in the 'messages' namespace when sending requests."""

    NAMESPACE = MNS


class SendingAs(Mailbox):
    """Like Mailbox, but creates elements in the 'messages' namespace when sending requests.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/sendingas
    """

    ELEMENT_NAME = 'SendingAs'
    NAMESPACE = MNS


class RecipientAddress(Mailbox):
    """Like Mailbox, but with a different tag name.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/recipientaddress
    """

    ELEMENT_NAME = 'RecipientAddress'


class EmailAddress(Mailbox):
    """Like Mailbox, but with a different tag name.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/emailaddress-emailaddresstype
    """

    ELEMENT_NAME = 'EmailAddress'


class Address(Mailbox):
    """Like Mailbox, but with a different tag name.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/address-emailaddresstype
    """

    ELEMENT_NAME = 'Address'


class AvailabilityMailbox(EWSElement):
    """Like Mailbox, but with slightly different attributes.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailbox-availability
    """

    ELEMENT_NAME = 'Mailbox'

    name = TextField(field_uri='Name')
    email_address = EmailAddressField(field_uri='Address', is_required=True)
    # RoutingType values restricted to EX and SMTP:
    # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/routingtype-emailaddress
    routing_type = RoutingTypeField(field_uri='RoutingType')

    def __hash__(self):
        # Exchange may add 'name' on insert. We're satisfied if the email address matches.
        if self.email_address:
            return hash(self.email_address.lower())
        return super().__hash__()

    @classmethod
    def from_mailbox(cls, mailbox):
        if not isinstance(mailbox, Mailbox):
            raise ValueError("'mailbox' %r must be a Mailbox instance" % mailbox)
        return cls(name=mailbox.name, email_address=mailbox.email_address, routing_type=mailbox.routing_type)


class Email(AvailabilityMailbox):
    """Like AvailabilityMailbox, but with a different tag name.
    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/email-emailaddresstype
    """

    ELEMENT_NAME = 'Email'


class MailboxData(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailboxdata"""

    ELEMENT_NAME = 'MailboxData'
    ATTENDEE_TYPES = {'Optional', 'Organizer', 'Required', 'Resource', 'Room'}

    email = EmailField()
    attendee_type = ChoiceField(field_uri='AttendeeType', choices={Choice(c) for c in ATTENDEE_TYPES})
    exclude_conflicts = BooleanField(field_uri='ExcludeConflicts')


class DistinguishedFolderId(FolderId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid"""

    ELEMENT_NAME = 'DistinguishedFolderId'

    mailbox = MailboxField()

    def clean(self, version=None):
        from .folders import PublicFoldersRoot
        super().clean(version=version)
        if self.id == PublicFoldersRoot.DISTINGUISHED_FOLDER_ID:
            # Avoid "ErrorInvalidOperation: It is not valid to specify a mailbox with the public folder root" from EWS
            self.mailbox = None


class TimeWindow(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/timewindow"""

    ELEMENT_NAME = 'TimeWindow'

    start = DateTimeField(field_uri='StartTime', is_required=True)
    end = DateTimeField(field_uri='EndTime', is_required=True)


class FreeBusyViewOptions(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/freebusyviewoptions"""

    ELEMENT_NAME = 'FreeBusyViewOptions'
    REQUESTED_VIEWS = {'MergedOnly', 'FreeBusy', 'FreeBusyMerged', 'Detailed', 'DetailedMerged'}

    time_window = EWSElementField(value_cls=TimeWindow, is_required=True)
    # Interval value is in minutes
    merged_free_busy_interval = IntegerField(field_uri='MergedFreeBusyIntervalInMinutes', min=5, max=1440, default=30,
                                             is_required=True)
    requested_view = ChoiceField(field_uri='RequestedView', choices={Choice(c) for c in REQUESTED_VIEWS},
                                 is_required=True)  # Choice('None') is also valid, but only for responses


class Attendee(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/attendee"""

    ELEMENT_NAME = 'Attendee'
    RESPONSE_TYPES = {'Unknown', 'Organizer', 'Tentative', 'Accept', 'Decline', 'NoResponseReceived'}

    mailbox = MailboxField(is_required=True)
    response_type = ChoiceField(field_uri='ResponseType', choices={Choice(c) for c in RESPONSE_TYPES},
                                default='Unknown')
    last_response_time = DateTimeField(field_uri='LastResponseTime')

    def __hash__(self):
        return hash(self.mailbox)


class TimeZoneTransition(EWSElement, metaclass=EWSMeta):
    """Base class for StandardTime and DaylightTime classes."""

    bias = IntegerField(field_uri='Bias', is_required=True)  # Offset from the default bias, in minutes
    time = TimeField(field_uri='Time', is_required=True)
    occurrence = IntegerField(field_uri='DayOrder', is_required=True)  # n'th occurrence of weekday in iso_month
    iso_month = IntegerField(field_uri='Month', is_required=True)
    weekday = EnumField(field_uri='DayOfWeek', enum=WEEKDAY_NAMES, is_required=True)
    # 'Year' is not implemented yet

    @classmethod
    def from_xml(cls, elem, account):
        res = super().from_xml(elem, account)
        # Some parts of EWS use '5' to mean 'last occurrence in month', others use '-1'. Let's settle on '5' because
        # only '5' is accepted in requests.
        if res.occurrence == -1:
            res.occurrence = 5
        return res

    def clean(self, version=None):
        super().clean(version=version)
        if self.occurrence == -1:
            # See from_xml()
            self.occurrence = 5


class StandardTime(TimeZoneTransition):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/standardtime"""

    ELEMENT_NAME = 'StandardTime'


class DaylightTime(TimeZoneTransition):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/daylighttime"""

    ELEMENT_NAME = 'DaylightTime'


class TimeZone(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/timezone-availability"""

    ELEMENT_NAME = 'TimeZone'

    bias = IntegerField(field_uri='Bias', is_required=True)  # Standard (non-DST) offset from UTC, in minutes
    standard_time = EWSElementField(value_cls=StandardTime)
    daylight_time = EWSElementField(value_cls=DaylightTime)

    def to_server_timezone(self, timezones, for_year):
        """Return the Microsoft timezone ID corresponding to this timezone. There may not be a match at all, and there
        may be multiple matches. If so, return a random timezone ID.

        :param timezones: A list of server timezones, as returned by
          Protocol.get_timezones(return_full_timezone_data=True)
        :param for_year: return: A Microsoft timezone ID, as a string

        :return: A Microsoft timezone ID, as a string
        """
        candidates = set()
        for tz_id, tz_name, tz_periods, tz_transitions, tz_transitions_groups in timezones:
            candidate = self.from_server_timezone(tz_periods, tz_transitions, tz_transitions_groups, for_year)
            if candidate == self:
                log.debug('Found exact candidate: %s (%s)', tz_id, tz_name)
                # We prefer this timezone over anything else. Return immediately.
                return tz_id
            # Reduce list based on base bias and standard / daylight bias values
            if candidate.bias != self.bias:
                continue
            if candidate.standard_time is None:
                if self.standard_time is not None:
                    continue
            else:
                if self.standard_time is None:
                    continue
                if candidate.standard_time.bias != self.standard_time.bias:
                    continue
            if candidate.daylight_time is None:
                if self.daylight_time is not None:
                    continue
            else:
                if self.daylight_time is None:
                    continue
                if candidate.daylight_time.bias != self.daylight_time.bias:
                    continue
            log.debug('Found candidate with matching biases: %s (%s)', tz_id, tz_name)
            candidates.add(tz_id)
        if not candidates:
            raise ValueError('No server timezones match this timezone definition')
        if len(candidates) == 1:
            log.info('Could not find an exact timezone match for %s. Selecting the best candidate', self)
        else:
            log.warning('Could not find an exact timezone match for %s. Selecting a random candidate', self)
        return candidates.pop()

    @classmethod
    def from_server_timezone(cls, periods, transitions, transitionsgroups, for_year):
        # Creates a TimeZone object from the result of a GetServerTimeZones call with full timezone data

        # Get the default bias
        bias = cls._get_bias(periods=periods, for_year=for_year)

        # Get a relevant transition ID
        valid_tg_id = cls._get_valid_transition_id(transitions=transitions, for_year=for_year)
        transitiongroup = transitionsgroups[valid_tg_id]
        if not 0 <= len(transitiongroup) <= 2:
            raise ValueError('Expected 0-2 transitions in transitionsgroup %s' % transitiongroup)

        standard_time, daylight_time = cls._get_std_and_dst(transitiongroup=transitiongroup, periods=periods, bias=bias)
        return cls(bias=bias, standard_time=standard_time, daylight_time=daylight_time)

    @staticmethod
    def _get_bias(periods, for_year):
        # Set a default bias
        valid_period = None
        for (year, period_type), period in sorted(periods.items()):
            if year > for_year:
                break
            if period_type != 'Standard':
                continue
            valid_period = period
        if valid_period is None:
            raise TimezoneDefinitionInvalidForYear('Year %s not included in periods %s' % (for_year, periods))
        return int(valid_period['bias'].total_seconds()) // 60  # Convert to minutes

    @staticmethod
    def _get_valid_transition_id(transitions, for_year):
        # Look through the transitions, and pick the relevant one according to the 'for_year' value
        valid_tg_id = None
        for tg_id, from_date in sorted(transitions.items()):
            if from_date and from_date.year > for_year:
                break
            valid_tg_id = tg_id
        if valid_tg_id is None:
            raise ValueError('No valid transition for year %s: %s' % (for_year, transitions))
        return valid_tg_id

    @staticmethod
    def _get_std_and_dst(transitiongroup, periods, bias):
        # Return 'standard_time' and 'daylight_time' objects. We do unnecessary work here, but it keeps code simple.
        standard_time, daylight_time = None, None
        for transition in transitiongroup:
            period = periods[transition['to']]
            if len(transition) == 1:
                # This is a simple transition representing a timezone with no DST. Some servers don't accept TimeZone
                # elements without a STD and DST element (see issue #488). Return StandardTime and DaylightTime objects
                # with dummy values and 0 bias - this satisfies the broken servers and hopefully doesn't break the
                # well-behaving servers.
                standard_time = StandardTime(bias=0, time=datetime.time(0), occurrence=1, iso_month=1, weekday=1)
                daylight_time = DaylightTime(bias=0, time=datetime.time(0), occurrence=5, iso_month=12, weekday=7)
                continue
            # 'offset' is the time of day to transition, as timedelta since midnight. Must be a reasonable value
            if not datetime.timedelta(0) <= transition['offset'] < datetime.timedelta(days=1):
                raise ValueError("'offset' value %s must be be between 0 and 24 hours" % transition['offset'])
            transition_kwargs = dict(
                time=(datetime.datetime(2000, 1, 1) + transition['offset']).time(),
                occurrence=transition['occurrence'],
                iso_month=transition['iso_month'],
                weekday=transition['iso_weekday'],
            )
            if period['name'] == 'Standard':
                transition_kwargs['bias'] = 0
                standard_time = StandardTime(**transition_kwargs)
                continue
            if period['name'] == 'Daylight':
                dst_bias = int(period['bias'].total_seconds()) // 60  # Convert to minutes
                transition_kwargs['bias'] = dst_bias - bias
                daylight_time = DaylightTime(**transition_kwargs)
                continue
            raise ValueError('Unknown transition: %s' % transition)
        return standard_time, daylight_time


class CalendarView(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendarview"""

    ELEMENT_NAME = 'CalendarView'
    NAMESPACE = MNS

    start = DateTimeField(field_uri='StartDate', is_required=True, is_attribute=True)
    end = DateTimeField(field_uri='EndDate', is_required=True, is_attribute=True)
    max_items = IntegerField(field_uri='MaxEntriesReturned', min=1, is_attribute=True)

    def clean(self, version=None):
        super().clean(version=version)
        if self.end < self.start:
            raise ValueError("'start' must be before 'end'")


class CalendarEventDetails(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendareventdetails"""

    ELEMENT_NAME = 'CalendarEventDetails'

    id = CharField(field_uri='ID')
    subject = CharField(field_uri='Subject')
    location = CharField(field_uri='Location')
    is_meeting = BooleanField(field_uri='IsMeeting')
    is_recurring = BooleanField(field_uri='IsRecurring')
    is_exception = BooleanField(field_uri='IsException')
    is_reminder_set = BooleanField(field_uri='IsReminderSet')
    is_private = BooleanField(field_uri='IsPrivate')


class CalendarEvent(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendarevent"""

    ELEMENT_NAME = 'CalendarEvent'

    start = DateTimeField(field_uri='StartTime')
    end = DateTimeField(field_uri='EndTime')
    busy_type = FreeBusyStatusField(field_uri='BusyType', is_required=True, default='Busy')
    details = EWSElementField(value_cls=CalendarEventDetails)


class WorkingPeriod(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/workingperiod"""

    ELEMENT_NAME = 'WorkingPeriod'

    weekdays = EnumListField(field_uri='DayOfWeek', enum=WEEKDAY_NAMES, is_required=True)
    start = TimeField(field_uri='StartTimeInMinutes', is_required=True)
    end = TimeField(field_uri='EndTimeInMinutes', is_required=True)


class FreeBusyView(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/freebusyview"""

    ELEMENT_NAME = 'FreeBusyView'
    NAMESPACE = MNS
    view_type = ChoiceField(field_uri='FreeBusyViewType', choices={
        Choice('None'), Choice('MergedOnly'), Choice('FreeBusy'), Choice('FreeBusyMerged'), Choice('Detailed'),
        Choice('DetailedMerged'),
    }, is_required=True)
    # A string of digits. Each digit points to a position in .fields.FREE_BUSY_CHOICES
    merged = CharField(field_uri='MergedFreeBusy')
    calendar_events = EWSElementListField(field_uri='CalendarEventArray', value_cls=CalendarEvent)
    # WorkingPeriod is located inside the WorkingPeriodArray element which is inside the WorkingHours element
    working_hours = EWSElementListField(field_uri='WorkingPeriodArray', value_cls=WorkingPeriod)
    # TimeZone is also inside the WorkingHours element. It contains information about the timezone which the
    # account is located in.
    working_hours_timezone = EWSElementField(value_cls=TimeZone)

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {}
        working_hours_elem = elem.find('{%s}WorkingHours' % TNS)
        for f in cls.FIELDS:
            if f.name in ['working_hours', 'working_hours_timezone']:
                if working_hours_elem is None:
                    continue
                kwargs[f.name] = f.from_xml(elem=working_hours_elem, account=account)
                continue
            kwargs[f.name] = f.from_xml(elem=elem, account=account)
        cls._clear(elem)
        return cls(**kwargs)


class RoomList(Mailbox):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/roomlist"""

    ELEMENT_NAME = 'RoomList'
    NAMESPACE = MNS

    @classmethod
    def response_tag(cls):
        # In a GetRoomLists response, room lists are delivered as Address elements. See
        # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/address-emailaddresstype
        return '{%s}Address' % TNS


class Room(Mailbox):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/room"""

    ELEMENT_NAME = 'Room'

    @classmethod
    def from_xml(cls, elem, account):
        id_elem = elem.find('{%s}Id' % TNS)
        item_id_elem = id_elem.find(ItemId.response_tag())
        kwargs = dict(
            name=get_xml_attr(id_elem, '{%s}Name' % TNS),
            email_address=get_xml_attr(id_elem, '{%s}EmailAddress' % TNS),
            mailbox_type=get_xml_attr(id_elem, '{%s}MailboxType' % TNS),
            item_id=ItemId.from_xml(elem=item_id_elem, account=account) if item_id_elem else None,
        )
        cls._clear(elem)
        return cls(**kwargs)


class Member(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/member-ex15websvcsotherref
    """

    ELEMENT_NAME = 'Member'

    mailbox = MailboxField(is_required=True)
    status = ChoiceField(field_uri='Status', choices={
            Choice('Unrecognized'), Choice('Normal'), Choice('Demoted')
        }, default='Normal')

    def __hash__(self):
        return hash(self.mailbox)


class UserId(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/userid"""

    ELEMENT_NAME = 'UserId'

    sid = CharField(field_uri='SID')
    primary_smtp_address = EmailAddressField(field_uri='PrimarySmtpAddress')
    display_name = CharField(field_uri='DisplayName')
    distinguished_user = ChoiceField(field_uri='DistinguishedUser', choices={
        Choice('Default'), Choice('Anonymous')
    })
    external_user_identity = CharField(field_uri='ExternalUserIdentity')


class BasePermission(EWSElement, metaclass=EWSMeta):
    """Base class for the Permission and CalendarPermission classes"""

    PERMISSION_ENUM = {Choice('None'), Choice('Owned'), Choice('All')}

    can_create_items = BooleanField(field_uri='CanCreateItems', default=False)
    can_create_subfolders = BooleanField(field_uri='CanCreateSubfolders', default=False)
    is_folder_owner = BooleanField(field_uri='IsFolderOwner', default=False)
    is_folder_visible = BooleanField(field_uri='IsFolderVisible', default=False)
    is_folder_contact = BooleanField(field_uri='IsFolderContact', default=False)
    edit_items = ChoiceField(field_uri='EditItems', choices=PERMISSION_ENUM, default='None')
    delete_items = ChoiceField(field_uri='DeleteItems', choices=PERMISSION_ENUM, default='None')
    read_items = ChoiceField(field_uri='ReadItems', choices={Choice('None'), Choice('FullDetails')}, default='None')
    user_id = EWSElementField(value_cls=UserId, is_required=True)


class Permission(BasePermission):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/permission"""

    ELEMENT_NAME = 'Permission'
    LEVEL_CHOICES = (
        'None', 'Owner', 'PublishingEditor', 'Editor', 'PublishingAuthor', 'Author', 'NoneditingAuthor', 'Reviewer',
        'Contributor', 'Custom',
    )

    permission_level = ChoiceField(
        field_uri='CalendarPermissionLevel', choices={Choice(c) for c in LEVEL_CHOICES}, default=LEVEL_CHOICES[0]
    )


class CalendarPermission(BasePermission):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendarpermission"""

    ELEMENT_NAME = 'CalendarPermission'
    LEVEL_CHOICES = (
        'None', 'Owner', 'PublishingEditor', 'Editor', 'PublishingAuthor', 'Author', 'NoneditingAuthor', 'Reviewer',
        'Contributor', 'FreeBusyTimeOnly', 'FreeBusyTimeAndSubjectAndLocation', 'Custom',
    )

    calendar_permission_level = ChoiceField(
        field_uri='CalendarPermissionLevel', choices={Choice(c) for c in LEVEL_CHOICES}, default=LEVEL_CHOICES[0]
    )


class PermissionSet(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/permissionset-permissionsettype
    and
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/permissionset-calendarpermissionsettype
    """

    # For simplicity, we implement the two distinct but equally names elements as one class.
    ELEMENT_NAME = 'PermissionSet'

    permissions = EWSElementListField(field_uri='Permissions', value_cls=Permission)
    calendar_permissions = EWSElementListField(field_uri='CalendarPermissions', value_cls=CalendarPermission)
    unknown_entries = UnknownEntriesField(field_uri='UnknownEntries')


class EffectiveRights(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/effectiverights"""

    ELEMENT_NAME = 'EffectiveRights'

    create_associated = BooleanField(field_uri='CreateAssociated', default=False)
    create_contents = BooleanField(field_uri='CreateContents', default=False)
    create_hierarchy = BooleanField(field_uri='CreateHierarchy', default=False)
    delete = BooleanField(field_uri='Delete', default=False)
    modify = BooleanField(field_uri='Modify', default=False)
    read = BooleanField(field_uri='Read', default=False)
    view_private_items = BooleanField(field_uri='ViewPrivateItems', default=False)

    def __contains__(self, item):
        return getattr(self, item, False)


class DelegatePermissions(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/delegatepermissions"""

    ELEMENT_NAME = 'DelegatePermissions'
    PERMISSION_LEVEL_CHOICES = {
            Choice('None'), Choice('Editor'), Choice('Reviewer'), Choice('Author'), Choice('Custom'),
        }

    calendar_folder_permission_level = ChoiceField(field_uri='CalendarFolderPermissionLevel',
                                                   choices=PERMISSION_LEVEL_CHOICES, default='None')
    tasks_folder_permission_level = ChoiceField(field_uri='TasksFolderPermissionLevel',
                                                choices=PERMISSION_LEVEL_CHOICES, default='None')
    inbox_folder_permission_level = ChoiceField(field_uri='InboxFolderPermissionLevel',
                                                choices=PERMISSION_LEVEL_CHOICES, default='None')
    contacts_folder_permission_level = ChoiceField(field_uri='ContactsFolderPermissionLevel',
                                                   choices=PERMISSION_LEVEL_CHOICES, default='None')
    notes_folder_permission_level = ChoiceField(field_uri='NotesFolderPermissionLevel',
                                                choices=PERMISSION_LEVEL_CHOICES, default='None')
    journal_folder_permission_level = ChoiceField(field_uri='JournalFolderPermissionLevel',
                                                  choices=PERMISSION_LEVEL_CHOICES, default='None')


class DelegateUser(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/delegateuser"""

    ELEMENT_NAME = 'DelegateUser'
    NAMESPACE = MNS

    user_id = EWSElementField(value_cls=UserId)
    delegate_permissions = EWSElementField(value_cls=DelegatePermissions)
    receive_copies_of_meeting_messages = BooleanField(field_uri='ReceiveCopiesOfMeetingMessages', default=False)
    view_private_items = BooleanField(field_uri='ViewPrivateItems', default=False)


class SearchableMailbox(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/searchablemailbox"""

    ELEMENT_NAME = 'SearchableMailbox'

    guid = CharField(field_uri='Guid')
    primary_smtp_address = EmailAddressField(field_uri='PrimarySmtpAddress')
    is_external = BooleanField(field_uri='IsExternalMailbox')
    external_email = EmailAddressField(field_uri='ExternalEmailAddress')
    display_name = CharField(field_uri='DisplayName')
    is_membership_group = BooleanField(field_uri='IsMembershipGroup')
    reference_id = CharField(field_uri='ReferenceId')


class FailedMailbox(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/failedmailbox"""

    ELEMENT_NAME = 'FailedMailbox'

    mailbox = CharField(field_uri='Mailbox')
    error_code = IntegerField(field_uri='ErrorCode')
    error_message = CharField(field_uri='ErrorMessage')
    is_archive = BooleanField(field_uri='IsArchive')


# MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailtipsrequested
MAIL_TIPS_TYPES = (
    'All',
    'OutOfOfficeMessage',
    'MailboxFullStatus',
    'CustomMailTip',
    'ExternalMemberCount',
    'TotalMemberCount',
    'MaxMessageSize',
    'DeliveryRestriction',
    'ModerationStatus',
    'InvalidRecipient',
)


class OutOfOffice(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/outofoffice"""

    ELEMENT_NAME = 'OutOfOffice'

    reply_body = MessageField(field_uri='ReplyBody')
    start = DateTimeField(field_uri='StartTime', is_required=False)
    end = DateTimeField(field_uri='EndTime', is_required=False)

    @classmethod
    def duration_to_start_end(cls, elem, account):
        kwargs = {}
        duration = elem.find('{%s}Duration' % TNS)
        if duration is not None:
            for attr in ('start', 'end'):
                f = cls.get_field_by_fieldname(attr)
                kwargs[attr] = f.from_xml(elem=duration, account=account)
        return kwargs

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {}
        for attr in ('reply_body',):
            f = cls.get_field_by_fieldname(attr)
            kwargs[attr] = f.from_xml(elem=elem, account=account)
        kwargs.update(cls.duration_to_start_end(elem=elem, account=account))
        cls._clear(elem)
        return cls(**kwargs)


class MailTips(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailtips"""

    ELEMENT_NAME = 'MailTips'
    NAMESPACE = MNS

    recipient_address = RecipientAddressField()
    pending_mail_tips = ChoiceField(field_uri='PendingMailTips', choices={Choice(c) for c in MAIL_TIPS_TYPES})
    out_of_office = EWSElementField(value_cls=OutOfOffice)
    mailbox_full = BooleanField(field_uri='MailboxFull')
    custom_mail_tip = TextField(field_uri='CustomMailTip')
    total_member_count = IntegerField(field_uri='TotalMemberCount')
    external_member_count = IntegerField(field_uri='ExternalMemberCount')
    max_message_size = IntegerField(field_uri='MaxMessageSize')
    delivery_restricted = BooleanField(field_uri='DeliveryRestricted')
    is_moderated = BooleanField(field_uri='IsModerated')
    invalid_recipient = BooleanField(field_uri='InvalidRecipient')


ENTRY_ID = 'EntryId'  # The base64-encoded PR_ENTRYID property
EWS_ID = 'EwsId'  # The EWS format used in Exchange 2007 SP1 and later
EWS_LEGACY_ID = 'EwsLegacyId'  # The EWS format used in Exchange 2007 before SP1
HEX_ENTRY_ID = 'HexEntryId'  # The hexadecimal representation of the PR_ENTRYID property
OWA_ID = 'OwaId'  # The OWA format for Exchange 2007 and 2010
STORE_ID = 'StoreId'  # The Exchange Store format
# IdFormat enum
ID_FORMATS = (ENTRY_ID, EWS_ID, EWS_LEGACY_ID, HEX_ENTRY_ID, OWA_ID, STORE_ID)


class AlternateId(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/alternateid"""

    ELEMENT_NAME = 'AlternateId'

    id = CharField(field_uri='Id', is_required=True, is_attribute=True)
    format = ChoiceField(field_uri='Format', is_required=True, is_attribute=True,
                         choices={Choice(c) for c in ID_FORMATS})
    mailbox = EmailAddressField(field_uri='Mailbox', is_required=True, is_attribute=True)
    is_archive = BooleanField(field_uri='IsArchive', is_required=False, is_attribute=True)

    @classmethod
    def response_tag(cls):
        # This element is in TNS in the request and MNS in the response...
        return '{%s}%s' % (MNS, cls.ELEMENT_NAME)


class AlternatePublicFolderId(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/alternatepublicfolderid"""

    ELEMENT_NAME = 'AlternatePublicFolderId'

    folder_id = CharField(field_uri='FolderId', is_required=True, is_attribute=True)
    format = ChoiceField(field_uri='Format', is_required=True, is_attribute=True,
                         choices={Choice(c) for c in ID_FORMATS})


class AlternatePublicFolderItemId(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/alternatepublicfolderitemid
    """

    ELEMENT_NAME = 'AlternatePublicFolderItemId'

    folder_id = CharField(field_uri='FolderId', is_required=True, is_attribute=True)
    format = ChoiceField(field_uri='Format', is_required=True, is_attribute=True,
                         choices={Choice(c) for c in ID_FORMATS})
    item_id = CharField(field_uri='ItemId', is_required=True, is_attribute=True)


class FieldURI(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/fielduri"""

    ELEMENT_NAME = 'FieldURI'

    field_uri = CharField(field_uri='FieldURI', is_attribute=True, is_required=True)


class IndexedFieldURI(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/indexedfielduri"""

    ELEMENT_NAME = 'IndexedFieldURI'

    field_uri = CharField(field_uri='FieldURI', is_attribute=True, is_required=True)
    field_index = CharField(field_uri='FieldIndex', is_attribute=True, is_required=True)


class ExtendedFieldURI(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedfielduri"""

    ELEMENT_NAME = 'ExtendedFieldURI'

    distinguished_property_set_id = CharField(field_uri='DistinguishedPropertySetId', is_attribute=True)
    property_set_id = CharField(field_uri='PropertySetId', is_attribute=True)
    property_tag = CharField(field_uri='PropertyTag', is_attribute=True)
    property_name = CharField(field_uri='PropertyName', is_attribute=True)
    property_id = CharField(field_uri='PropertyId', is_attribute=True)
    property_type = CharField(field_uri='PropertyType', is_attribute=True)


class ExceptionFieldURI(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/exceptionfielduri"""

    ELEMENT_NAME = 'ExceptionFieldURI'

    field_uri = CharField(field_uri='FieldURI', is_attribute=True, is_required=True)


class CompleteName(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/completename"""

    ELEMENT_NAME = 'CompleteName'

    title = CharField(field_uri='Title')
    first_name = CharField(field_uri='FirstName')
    middle_name = CharField(field_uri='MiddleName')
    last_name = CharField(field_uri='LastName')
    suffix = CharField(field_uri='Suffix')
    initials = CharField(field_uri='Initials')
    full_name = CharField(field_uri='FullName')
    nickname = CharField(field_uri='Nickname')
    yomi_first_name = CharField(field_uri='YomiFirstName')
    yomi_last_name = CharField(field_uri='YomiLastName')


class ReminderMessageData(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/remindermessagedata"""

    ELEMENT_NAME = 'ReminderMessageData'

    reminder_text = CharField(field_uri='ReminderText')
    location = CharField(field_uri='Location')
    start_time = TimeField(field_uri='StartTime')
    end_time = TimeField(field_uri='EndTime')
    associated_calendar_item_id = AssociatedCalendarItemIdField(field_uri='AssociatedCalendarItemId',
                                                                supported_from=Build(15, 0, 913, 9))


class AcceptSharingInvitation(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/acceptsharinginvitation"""

    ELEMENT_NAME = 'AcceptSharingInvitation'

    reference_item_id = ReferenceItemIdField(field_uri='item:ReferenceItemId')


class SuppressReadReceipt(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/suppressreadreceipt"""

    ELEMENT_NAME = 'SuppressReadReceipt'

    reference_item_id = ReferenceItemIdField(field_uri='item:ReferenceItemId')


class RemoveItem(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/removeitem"""

    ELEMENT_NAME = 'RemoveItem'

    reference_item_id = ReferenceItemIdField(field_uri='item:ReferenceItemId')


class ResponseObjects(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/responseobjects"""

    ELEMENT_NAME = 'ResponseObjects'
    NAMESPACE = EWSElement.NAMESPACE

    accept_item = EWSElementField(field_uri='AcceptItem', value_cls='AcceptItem', namespace=NAMESPACE)
    tentatively_accept_item = EWSElementField(field_uri='TentativelyAcceptItem', value_cls='TentativelyAcceptItem',
                                              namespace=NAMESPACE)
    decline_item = EWSElementField(field_uri='DeclineItem', value_cls='DeclineItem', namespace=NAMESPACE)
    reply_to_item = EWSElementField(field_uri='ReplyToItem', value_cls='ReplyToItem', namespace=NAMESPACE)
    forward_item = EWSElementField(field_uri='ForwardItem', value_cls='ForwardItem', namespace=NAMESPACE)
    reply_all_to_item = EWSElementField(field_uri='ReplyAllToItem', value_cls='ReplyAllToItem', namespace=NAMESPACE)
    cancel_calendar_item = EWSElementField(field_uri='CancelCalendarItem', value_cls='CancelCalendarItem',
                                           namespace=NAMESPACE)
    remove_item = EWSElementField(field_uri='RemoveItem', value_cls=RemoveItem)
    post_reply_item = EWSElementField(field_uri='PostReplyItem', value_cls='PostReplyItem',
                                      namespace=EWSElement.NAMESPACE)
    success_read_receipt = EWSElementField(field_uri='SuppressReadReceipt', value_cls=SuppressReadReceipt)
    accept_sharing_invitation = EWSElementField(field_uri='AcceptSharingInvitation',
                                                value_cls=AcceptSharingInvitation)


class PhoneNumber(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/phonenumber"""

    ELEMENT_NAME = 'PhoneNumber'

    number = CharField(field_uri='Number')
    type = CharField(field_uri='Type')


class IdChangeKeyMixIn(EWSElement, metaclass=EWSMeta):
    """Base class for classes that have a concept of 'id' and 'changekey' values. The values are actually stored on
    a separate element but we add convenience methods to hide that fact.
    """

    ID_ELEMENT_CLS = None

    def __init__(self, **kwargs):
        _id = self.ID_ELEMENT_CLS(kwargs.pop('id', None), kwargs.pop('changekey', None))
        if _id.id or _id.changekey:
            kwargs['_id'] = _id
        super().__init__(**kwargs)

    @classmethod
    def get_field_by_fieldname(cls, fieldname):
        if fieldname in ('id', 'changekey'):
            return cls.ID_ELEMENT_CLS.get_field_by_fieldname(fieldname=fieldname)
        return super().get_field_by_fieldname(fieldname=fieldname)

    @property
    def id(self):
        if self._id is None:
            return None
        return self._id.id

    @id.setter
    def id(self, value):
        if self._id is None:
            self._id = self.ID_ELEMENT_CLS()
        self._id.id = value

    @property
    def changekey(self):
        if self._id is None:
            return None
        return self._id.changekey

    @changekey.setter
    def changekey(self, value):
        if self._id is None:
            self._id = self.ID_ELEMENT_CLS()
        self._id.changekey = value

    @classmethod
    def id_from_xml(cls, elem):
        # This method must be reasonably fast
        id_elem = elem.find(cls.ID_ELEMENT_CLS.response_tag())
        if id_elem is None:
            return None, None
        return id_elem.get(cls.ID_ELEMENT_CLS.ID_ATTR), id_elem.get(cls.ID_ELEMENT_CLS.CHANGEKEY_ATTR)

    def to_id_xml(self, version):
        return self._id.to_xml(version=version)

    def __eq__(self, other):
        if isinstance(other, tuple):
            return hash((self.id, self.changekey)) == hash(other)
        return super().__eq__(other)

    def __hash__(self):
        # If we have an ID and changekey, use that as key. Else return a hash of all attributes
        if self.id:
            return hash((self.id, self.changekey))
        return super().__hash__()


class DictionaryEntry(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/dictionaryentry"""

    ELEMENT_NAME = 'DictionaryEntry'

    key = TypeValueField(field_uri='DictionaryKey')
    value = TypeValueField(field_uri='DictionaryValue')


class UserConfigurationName(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/userconfigurationname"""

    ELEMENT_NAME = 'UserConfigurationName'
    NAMESPACE = TNS

    name = CharField(field_uri='Name', is_attribute=True)
    folder = EWSElementField(value_cls=FolderId)

    def clean(self, version=None):
        from .folders import BaseFolder
        if isinstance(self.folder, BaseFolder):
            self.folder = self.folder.to_folder_id()
        super().clean(version=version)

    @classmethod
    def from_xml(cls, elem, account):
        # We also accept distinguished folders
        f = EWSElementField(value_cls=DistinguishedFolderId)
        distinguished_folder_id = f.from_xml(elem=elem, account=account)
        res = super().from_xml(elem=elem, account=account)
        if distinguished_folder_id:
            res.folder = distinguished_folder_id
        return res


class UserConfigurationNameMNS(UserConfigurationName):
    """Like UserConfigurationName, but in the MNS namespace.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/userconfigurationname
    """

    NAMESPACE = MNS


class UserConfiguration(IdChangeKeyMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/userconfiguration"""

    ELEMENT_NAME = 'UserConfiguration'
    NAMESPACE = MNS
    ID_ELEMENT_CLS = ItemId

    _id = IdElementField(field_uri='ItemId', value_cls=ID_ELEMENT_CLS)
    user_configuration_name = EWSElementField(value_cls=UserConfigurationName)
    dictionary = DictionaryField(field_uri='Dictionary')
    xml_data = Base64Field(field_uri='XmlData')
    binary_data = Base64Field(field_uri='BinaryData')


class Attribution(IdChangeKeyMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/phonenumber"""

    ELEMENT_NAME = 'Attribution'
    ID_ELEMENT_CLS = SourceId

    ID = CharField(field_uri='Id')
    _id = IdElementField(field_uri='SourceId', value_cls=ID_ELEMENT_CLS)
    display_name = CharField(field_uri='DisplayName')
    is_writable = BooleanField(field_uri='IsWritable')
    is_quick_contact = BooleanField(field_uri='IsQuickContact')
    is_hidden = BooleanField(field_uri='IsHidden')
    folder_id = EWSElementField(value_cls=FolderId)


class BodyContentValue(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/value-bodycontenttype
    """

    ELEMENT_NAME = 'Value'

    value = CharField(field_uri='Value')
    body_type = CharField(field_uri='BodyType')


class BodyContentAttributedValue(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/bodycontentattributedvalue
    """

    ELEMENT_NAME = 'BodyContentAttributedValue'

    value = EWSElementField(value_cls=BodyContentValue)
    attributions = EWSElementListField(field_uri='Attributions', value_cls=Attribution)


class StringAttributedValue(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/stringattributedvalue
    """

    ELEMENT_NAME = 'StringAttributedValue'

    value = CharField(field_uri='Value')
    attributions = CharListField(field_uri='Attributions', list_elem_name='Attribution')


class PersonaPhoneNumberTypeValue(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/value-personaphonenumbertype
    """

    ELEMENT_NAME = 'Value'

    number = CharField(field_uri='Number')
    type = CharField(field_uri='Type')


class PhoneNumberAttributedValue(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/phonenumberattributedvalue
    """

    ELEMENT_NAME = 'PhoneNumberAttributedValue'

    value = EWSElementField(value_cls=PersonaPhoneNumberTypeValue)
    attributions = CharListField(field_uri='Attributions', list_elem_name='Attribution')


class EmailAddressTypeValue(Mailbox):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/value-emailaddresstype
    """

    ELEMENT_NAME = 'Value'

    original_display_name = TextField(field_uri='OriginalDisplayName')


class EmailAddressAttributedValue(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/emailaddressattributedvalue
    """

    ELEMENT_NAME = 'EmailAddressAttributedValue'

    value = EWSElementField(value_cls=EmailAddressTypeValue)
    attributions = EWSElementListField(field_uri='Attributions', value_cls=Attribution)


class PersonaPostalAddressTypeValue(Mailbox):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/value-personapostaladdresstype
    """

    ELEMENT_NAME = 'Value'

    street = TextField(field_uri='Street')
    city = TextField(field_uri='City')
    state = TextField(field_uri='State')
    country = TextField(field_uri='Country')
    postal_code = TextField(field_uri='PostalCode')
    post_office_box = TextField(field_uri='PostOfficeBox')
    type = TextField(field_uri='Type')
    latitude = TextField(field_uri='Latitude')
    longitude = TextField(field_uri='Longitude')
    accuracy = TextField(field_uri='Accuracy')
    altitude = TextField(field_uri='Altitude')
    altitude_accuracy = TextField(field_uri='AltitudeAccuracy')
    formatted_address = TextField(field_uri='FormattedAddress')
    location_uri = TextField(field_uri='LocationUri')
    location_source = TextField(field_uri='LocationSource')


class PostalAddressAttributedValue(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postaladdressattributedvalue
    """

    ELEMENT_NAME = 'PostalAddressAttributedValue'

    value = EWSElementField(value_cls=PersonaPostalAddressTypeValue)
    attributions = EWSElementListField(field_uri='Attributions', value_cls=Attribution)


class Event(EWSElement, metaclass=EWSMeta):
    """Base class for all event types."""

    watermark = CharField(field_uri='Watermark')


class TimestampEvent(Event, metaclass=EWSMeta):
    """Base class for both item and folder events with a timestamp."""

    FOLDER = 'folder'
    ITEM = 'item'

    timestamp = DateTimeField(field_uri='TimeStamp')
    item_id = EWSElementField(field_uri='ItemId', value_cls=ItemId)
    folder_id = EWSElementField(field_uri='FolderId', value_cls=FolderId)
    parent_folder_id = EWSElementField(field_uri='ParentFolderId', value_cls=ParentFolderId)

    @property
    def event_type(self):
        if self.item_id is not None:
            return self.ITEM
        if self.folder_id is not None:
            return self.FOLDER
        return None  # Empty object


class OldTimestampEvent(TimestampEvent, metaclass=EWSMeta):
    """Base class for both item and folder copy/move events."""

    old_item_id = EWSElementField(field_uri='OldItemId', value_cls=ItemId)
    old_folder_id = EWSElementField(field_uri='OldFolderId', value_cls=FolderId)
    old_parent_folder_id = EWSElementField(field_uri='OldParentFolderId', value_cls=ParentFolderId)


class CopiedEvent(OldTimestampEvent):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/copiedevent"""

    ELEMENT_NAME = 'CopiedEvent'


class CreatedEvent(TimestampEvent):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createdevent"""

    ELEMENT_NAME = 'CreatedEvent'


class DeletedEvent(TimestampEvent):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deletedevent"""

    ELEMENT_NAME = 'DeletedEvent'


class ModifiedEvent(TimestampEvent):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/modifiedevent"""

    ELEMENT_NAME = 'ModifiedEvent'

    unread_count = IntegerField(field_uri='UnreadCount')


class MovedEvent(OldTimestampEvent):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/movedevent"""

    ELEMENT_NAME = 'MovedEvent'


class NewMailEvent(TimestampEvent):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/newmailevent"""

    ELEMENT_NAME = 'NewMailEvent'


class StatusEvent(Event):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/statusevent"""

    ELEMENT_NAME = 'StatusEvent'


class FreeBusyChangedEvent(TimestampEvent):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/freebusychangedevent"""

    ELEMENT_NAME = 'FreeBusyChangedEvent'


class Notification(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/notification-ex15websvcsotherref
    """

    ELEMENT_NAME = 'Notification'
    NAMESPACE = MNS

    subscription_id = CharField(field_uri='SubscriptionId')
    previous_watermark = CharField(field_uri='PreviousWatermark')
    more_events = BooleanField(field_uri='MoreEvents')
    events = GenericEventListField('')

Classes

class AcceptSharingInvitation (**kwargs)
Expand source code
class AcceptSharingInvitation(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/acceptsharinginvitation"""

    ELEMENT_NAME = 'AcceptSharingInvitation'

    reference_item_id = ReferenceItemIdField(field_uri='item:ReferenceItemId')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var reference_item_id

Inherited members

class Address (**kwargs)
Expand source code
class Address(Mailbox):
    """Like Mailbox, but with a different tag name.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/address-emailaddresstype
    """

    ELEMENT_NAME = 'Address'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class AlternateId (**kwargs)
Expand source code
class AlternateId(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/alternateid"""

    ELEMENT_NAME = 'AlternateId'

    id = CharField(field_uri='Id', is_required=True, is_attribute=True)
    format = ChoiceField(field_uri='Format', is_required=True, is_attribute=True,
                         choices={Choice(c) for c in ID_FORMATS})
    mailbox = EmailAddressField(field_uri='Mailbox', is_required=True, is_attribute=True)
    is_archive = BooleanField(field_uri='IsArchive', is_required=False, is_attribute=True)

    @classmethod
    def response_tag(cls):
        # This element is in TNS in the request and MNS in the response...
        return '{%s}%s' % (MNS, cls.ELEMENT_NAME)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Static methods

def response_tag()
Expand source code
@classmethod
def response_tag(cls):
    # This element is in TNS in the request and MNS in the response...
    return '{%s}%s' % (MNS, cls.ELEMENT_NAME)

Instance variables

var format
var id
var is_archive
var mailbox

Inherited members

class AlternatePublicFolderId (**kwargs)
Expand source code
class AlternatePublicFolderId(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/alternatepublicfolderid"""

    ELEMENT_NAME = 'AlternatePublicFolderId'

    folder_id = CharField(field_uri='FolderId', is_required=True, is_attribute=True)
    format = ChoiceField(field_uri='Format', is_required=True, is_attribute=True,
                         choices={Choice(c) for c in ID_FORMATS})

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var folder_id
var format

Inherited members

class AlternatePublicFolderItemId (**kwargs)
Expand source code
class AlternatePublicFolderItemId(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/alternatepublicfolderitemid
    """

    ELEMENT_NAME = 'AlternatePublicFolderItemId'

    folder_id = CharField(field_uri='FolderId', is_required=True, is_attribute=True)
    format = ChoiceField(field_uri='Format', is_required=True, is_attribute=True,
                         choices={Choice(c) for c in ID_FORMATS})
    item_id = CharField(field_uri='ItemId', is_required=True, is_attribute=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var folder_id
var format
var item_id

Inherited members

class AssociatedCalendarItemId (*args, **kwargs)
Expand source code
class AssociatedCalendarItemId(ItemId):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/associatedcalendaritemid
    """

    ELEMENT_NAME = 'AssociatedCalendarItemId'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class Attendee (**kwargs)
Expand source code
class Attendee(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/attendee"""

    ELEMENT_NAME = 'Attendee'
    RESPONSE_TYPES = {'Unknown', 'Organizer', 'Tentative', 'Accept', 'Decline', 'NoResponseReceived'}

    mailbox = MailboxField(is_required=True)
    response_type = ChoiceField(field_uri='ResponseType', choices={Choice(c) for c in RESPONSE_TYPES},
                                default='Unknown')
    last_response_time = DateTimeField(field_uri='LastResponseTime')

    def __hash__(self):
        return hash(self.mailbox)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var RESPONSE_TYPES

Instance variables

var last_response_time
var mailbox
var response_type

Inherited members

class Attribution (**kwargs)
Expand source code
class Attribution(IdChangeKeyMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/phonenumber"""

    ELEMENT_NAME = 'Attribution'
    ID_ELEMENT_CLS = SourceId

    ID = CharField(field_uri='Id')
    _id = IdElementField(field_uri='SourceId', value_cls=ID_ELEMENT_CLS)
    display_name = CharField(field_uri='DisplayName')
    is_writable = BooleanField(field_uri='IsWritable')
    is_quick_contact = BooleanField(field_uri='IsQuickContact')
    is_hidden = BooleanField(field_uri='IsHidden')
    folder_id = EWSElementField(value_cls=FolderId)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var ID_ELEMENT_CLS

Instance variables

var ID
var display_name
var folder_id
var is_hidden
var is_quick_contact
var is_writable

Inherited members

class AvailabilityMailbox (**kwargs)
Expand source code
class AvailabilityMailbox(EWSElement):
    """Like Mailbox, but with slightly different attributes.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailbox-availability
    """

    ELEMENT_NAME = 'Mailbox'

    name = TextField(field_uri='Name')
    email_address = EmailAddressField(field_uri='Address', is_required=True)
    # RoutingType values restricted to EX and SMTP:
    # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/routingtype-emailaddress
    routing_type = RoutingTypeField(field_uri='RoutingType')

    def __hash__(self):
        # Exchange may add 'name' on insert. We're satisfied if the email address matches.
        if self.email_address:
            return hash(self.email_address.lower())
        return super().__hash__()

    @classmethod
    def from_mailbox(cls, mailbox):
        if not isinstance(mailbox, Mailbox):
            raise ValueError("'mailbox' %r must be a Mailbox instance" % mailbox)
        return cls(name=mailbox.name, email_address=mailbox.email_address, routing_type=mailbox.routing_type)

Ancestors

Subclasses

Class variables

var ELEMENT_NAME
var FIELDS

Static methods

def from_mailbox(mailbox)
Expand source code
@classmethod
def from_mailbox(cls, mailbox):
    if not isinstance(mailbox, Mailbox):
        raise ValueError("'mailbox' %r must be a Mailbox instance" % mailbox)
    return cls(name=mailbox.name, email_address=mailbox.email_address, routing_type=mailbox.routing_type)

Instance variables

var email_address
var name
var routing_type

Inherited members

class BaseItemId (*args, **kwargs)

Base class for ItemId elements.

Expand source code
class BaseItemId(EWSElement):
    """Base class for ItemId elements."""

    ID_ATTR = None
    CHANGEKEY_ATTR = None

    def __init__(self, *args, **kwargs):
        if not kwargs:
            # Allow to set attributes without keyword
            kwargs = dict(zip(self._slots_keys, args))
        super().__init__(**kwargs)

Ancestors

Subclasses

Class variables

var CHANGEKEY_ATTR
var ID_ATTR

Inherited members

class BasePermission (**kwargs)

Base class for the Permission and CalendarPermission classes

Expand source code
class BasePermission(EWSElement, metaclass=EWSMeta):
    """Base class for the Permission and CalendarPermission classes"""

    PERMISSION_ENUM = {Choice('None'), Choice('Owned'), Choice('All')}

    can_create_items = BooleanField(field_uri='CanCreateItems', default=False)
    can_create_subfolders = BooleanField(field_uri='CanCreateSubfolders', default=False)
    is_folder_owner = BooleanField(field_uri='IsFolderOwner', default=False)
    is_folder_visible = BooleanField(field_uri='IsFolderVisible', default=False)
    is_folder_contact = BooleanField(field_uri='IsFolderContact', default=False)
    edit_items = ChoiceField(field_uri='EditItems', choices=PERMISSION_ENUM, default='None')
    delete_items = ChoiceField(field_uri='DeleteItems', choices=PERMISSION_ENUM, default='None')
    read_items = ChoiceField(field_uri='ReadItems', choices={Choice('None'), Choice('FullDetails')}, default='None')
    user_id = EWSElementField(value_cls=UserId, is_required=True)

Ancestors

Subclasses

Class variables

var FIELDS
var PERMISSION_ENUM

Instance variables

var can_create_items
var can_create_subfolders
var delete_items
var edit_items
var is_folder_contact
var is_folder_owner
var is_folder_visible
var read_items
var user_id

Inherited members

class Body (...)

Helper to mark the 'body' field as a complex attribute.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/body

Expand source code
class Body(str):
    """Helper to mark the 'body' field as a complex attribute.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/body
    """

    body_type = 'Text'

    def __add__(self, other):
        # Make sure Body('') + 'foo' returns a Body type
        return self.__class__(super().__add__(other))

    def __mod__(self, other):
        # Make sure Body('%s') % 'foo' returns a Body type
        return self.__class__(super().__mod__(other))

    def format(self, *args, **kwargs):
        # Make sure Body('{}').format('foo') returns a Body type
        return self.__class__(super().format(*args, **kwargs))

Ancestors

  • builtins.str

Subclasses

Class variables

var body_type

Methods

def format(self, *args, **kwargs)

S.format(args, *kwargs) -> str

Return a formatted version of S, using substitutions from args and kwargs. The substitutions are identified by braces ('{' and '}').

Expand source code
def format(self, *args, **kwargs):
    # Make sure Body('{}').format('foo') returns a Body type
    return self.__class__(super().format(*args, **kwargs))
class BodyContentAttributedValue (**kwargs)
Expand source code
class BodyContentAttributedValue(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/bodycontentattributedvalue
    """

    ELEMENT_NAME = 'BodyContentAttributedValue'

    value = EWSElementField(value_cls=BodyContentValue)
    attributions = EWSElementListField(field_uri='Attributions', value_cls=Attribution)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var attributions
var value

Inherited members

class BodyContentValue (**kwargs)
Expand source code
class BodyContentValue(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/value-bodycontenttype
    """

    ELEMENT_NAME = 'Value'

    value = CharField(field_uri='Value')
    body_type = CharField(field_uri='BodyType')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var body_type
var value

Inherited members

class CalendarEvent (**kwargs)
Expand source code
class CalendarEvent(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendarevent"""

    ELEMENT_NAME = 'CalendarEvent'

    start = DateTimeField(field_uri='StartTime')
    end = DateTimeField(field_uri='EndTime')
    busy_type = FreeBusyStatusField(field_uri='BusyType', is_required=True, default='Busy')
    details = EWSElementField(value_cls=CalendarEventDetails)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var busy_type
var details
var end
var start

Inherited members

class CalendarEventDetails (**kwargs)
Expand source code
class CalendarEventDetails(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendareventdetails"""

    ELEMENT_NAME = 'CalendarEventDetails'

    id = CharField(field_uri='ID')
    subject = CharField(field_uri='Subject')
    location = CharField(field_uri='Location')
    is_meeting = BooleanField(field_uri='IsMeeting')
    is_recurring = BooleanField(field_uri='IsRecurring')
    is_exception = BooleanField(field_uri='IsException')
    is_reminder_set = BooleanField(field_uri='IsReminderSet')
    is_private = BooleanField(field_uri='IsPrivate')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var id
var is_exception
var is_meeting
var is_private
var is_recurring
var is_reminder_set
var location
var subject

Inherited members

class CalendarPermission (**kwargs)
Expand source code
class CalendarPermission(BasePermission):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendarpermission"""

    ELEMENT_NAME = 'CalendarPermission'
    LEVEL_CHOICES = (
        'None', 'Owner', 'PublishingEditor', 'Editor', 'PublishingAuthor', 'Author', 'NoneditingAuthor', 'Reviewer',
        'Contributor', 'FreeBusyTimeOnly', 'FreeBusyTimeAndSubjectAndLocation', 'Custom',
    )

    calendar_permission_level = ChoiceField(
        field_uri='CalendarPermissionLevel', choices={Choice(c) for c in LEVEL_CHOICES}, default=LEVEL_CHOICES[0]
    )

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var LEVEL_CHOICES

Instance variables

var calendar_permission_level

Inherited members

class CalendarView (**kwargs)
Expand source code
class CalendarView(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendarview"""

    ELEMENT_NAME = 'CalendarView'
    NAMESPACE = MNS

    start = DateTimeField(field_uri='StartDate', is_required=True, is_attribute=True)
    end = DateTimeField(field_uri='EndDate', is_required=True, is_attribute=True)
    max_items = IntegerField(field_uri='MaxEntriesReturned', min=1, is_attribute=True)

    def clean(self, version=None):
        super().clean(version=version)
        if self.end < self.start:
            raise ValueError("'start' must be before 'end'")

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var NAMESPACE

Instance variables

var end
var max_items
var start

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)
    if self.end < self.start:
        raise ValueError("'start' must be before 'end'")

Inherited members

class CompleteName (**kwargs)
Expand source code
class CompleteName(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/completename"""

    ELEMENT_NAME = 'CompleteName'

    title = CharField(field_uri='Title')
    first_name = CharField(field_uri='FirstName')
    middle_name = CharField(field_uri='MiddleName')
    last_name = CharField(field_uri='LastName')
    suffix = CharField(field_uri='Suffix')
    initials = CharField(field_uri='Initials')
    full_name = CharField(field_uri='FullName')
    nickname = CharField(field_uri='Nickname')
    yomi_first_name = CharField(field_uri='YomiFirstName')
    yomi_last_name = CharField(field_uri='YomiLastName')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var first_name
var full_name
var initials
var last_name
var middle_name
var nickname
var suffix
var title
var yomi_first_name
var yomi_last_name

Inherited members

class ConversationId (*args, **kwargs)
Expand source code
class ConversationId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/conversationid"""

    ELEMENT_NAME = 'ConversationId'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class CopiedEvent (**kwargs)
Expand source code
class CopiedEvent(OldTimestampEvent):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/copiedevent"""

    ELEMENT_NAME = 'CopiedEvent'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class CreatedEvent (**kwargs)
Expand source code
class CreatedEvent(TimestampEvent):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createdevent"""

    ELEMENT_NAME = 'CreatedEvent'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class DLMailbox (**kwargs)

Like Mailbox, but creates elements in the 'messages' namespace when sending requests.

Expand source code
class DLMailbox(Mailbox):
    """Like Mailbox, but creates elements in the 'messages' namespace when sending requests."""

    NAMESPACE = MNS

Ancestors

Class variables

var NAMESPACE

Inherited members

class DaylightTime (**kwargs)
Expand source code
class DaylightTime(TimeZoneTransition):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/daylighttime"""

    ELEMENT_NAME = 'DaylightTime'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class DelegatePermissions (**kwargs)
Expand source code
class DelegatePermissions(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/delegatepermissions"""

    ELEMENT_NAME = 'DelegatePermissions'
    PERMISSION_LEVEL_CHOICES = {
            Choice('None'), Choice('Editor'), Choice('Reviewer'), Choice('Author'), Choice('Custom'),
        }

    calendar_folder_permission_level = ChoiceField(field_uri='CalendarFolderPermissionLevel',
                                                   choices=PERMISSION_LEVEL_CHOICES, default='None')
    tasks_folder_permission_level = ChoiceField(field_uri='TasksFolderPermissionLevel',
                                                choices=PERMISSION_LEVEL_CHOICES, default='None')
    inbox_folder_permission_level = ChoiceField(field_uri='InboxFolderPermissionLevel',
                                                choices=PERMISSION_LEVEL_CHOICES, default='None')
    contacts_folder_permission_level = ChoiceField(field_uri='ContactsFolderPermissionLevel',
                                                   choices=PERMISSION_LEVEL_CHOICES, default='None')
    notes_folder_permission_level = ChoiceField(field_uri='NotesFolderPermissionLevel',
                                                choices=PERMISSION_LEVEL_CHOICES, default='None')
    journal_folder_permission_level = ChoiceField(field_uri='JournalFolderPermissionLevel',
                                                  choices=PERMISSION_LEVEL_CHOICES, default='None')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var PERMISSION_LEVEL_CHOICES

Instance variables

var calendar_folder_permission_level
var contacts_folder_permission_level
var inbox_folder_permission_level
var journal_folder_permission_level
var notes_folder_permission_level
var tasks_folder_permission_level

Inherited members

class DelegateUser (**kwargs)
Expand source code
class DelegateUser(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/delegateuser"""

    ELEMENT_NAME = 'DelegateUser'
    NAMESPACE = MNS

    user_id = EWSElementField(value_cls=UserId)
    delegate_permissions = EWSElementField(value_cls=DelegatePermissions)
    receive_copies_of_meeting_messages = BooleanField(field_uri='ReceiveCopiesOfMeetingMessages', default=False)
    view_private_items = BooleanField(field_uri='ViewPrivateItems', default=False)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var NAMESPACE

Instance variables

var delegate_permissions
var receive_copies_of_meeting_messages
var user_id
var view_private_items

Inherited members

class DeletedEvent (**kwargs)
Expand source code
class DeletedEvent(TimestampEvent):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deletedevent"""

    ELEMENT_NAME = 'DeletedEvent'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class DictionaryEntry (**kwargs)
Expand source code
class DictionaryEntry(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/dictionaryentry"""

    ELEMENT_NAME = 'DictionaryEntry'

    key = TypeValueField(field_uri='DictionaryKey')
    value = TypeValueField(field_uri='DictionaryValue')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var key
var value

Inherited members

class DistinguishedFolderId (*args, **kwargs)
Expand source code
class DistinguishedFolderId(FolderId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid"""

    ELEMENT_NAME = 'DistinguishedFolderId'

    mailbox = MailboxField()

    def clean(self, version=None):
        from .folders import PublicFoldersRoot
        super().clean(version=version)
        if self.id == PublicFoldersRoot.DISTINGUISHED_FOLDER_ID:
            # Avoid "ErrorInvalidOperation: It is not valid to specify a mailbox with the public folder root" from EWS
            self.mailbox = None

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var mailbox

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    from .folders import PublicFoldersRoot
    super().clean(version=version)
    if self.id == PublicFoldersRoot.DISTINGUISHED_FOLDER_ID:
        # Avoid "ErrorInvalidOperation: It is not valid to specify a mailbox with the public folder root" from EWS
        self.mailbox = None

Inherited members

class EWSElement (**kwargs)

Base class for all XML element implementations.

Expand source code
class EWSElement(metaclass=EWSMeta):
    """Base class for all XML element implementations."""

    ELEMENT_NAME = None  # The name of the XML tag
    FIELDS = Fields()  # A list of attributes supported by this item class, ordered the same way as in EWS documentation
    NAMESPACE = TNS  # The XML tag namespace. Either TNS or MNS

    _fields_lock = Lock()

    def __init__(self, **kwargs):
        for f in self.FIELDS:
            setattr(self, f.name, kwargs.pop(f.name, None))
        if kwargs:
            raise AttributeError("%s are invalid kwargs for this class" % ', '.join("'%s'" % k for k in kwargs))

    def __setattr__(self, key, value):
        # Avoid silently accepting spelling errors to field names that are not set via __init__. We need to be able to
        # set values for predefined and registered fields, whatever non-field attributes this class defines, and
        # property setters.
        if key in self.FIELDS:
            return super().__setattr__(key, value)
        if key in self._slots_keys:
            return super().__setattr__(key, value)
        if hasattr(self, key):
            # Property setters
            return super().__setattr__(key, value)
        raise AttributeError('%r is not a valid attribute. See %s.FIELDS for valid field names' % (
            key, self.__class__.__name__))

    def clean(self, version=None):
        # Validate attribute values using the field validator
        for f in self.FIELDS:
            if version and not f.supports_version(version):
                continue
            if isinstance(f, ExtendedPropertyField) and not hasattr(self, f.name):
                # The extended field may have been registered after this item was created. Set default values.
                setattr(self, f.name, f.clean(None, version=version))
                continue
            val = getattr(self, f.name)
            setattr(self, f.name, f.clean(val, version=version))

    @staticmethod
    def _clear(elem):
        # Clears an XML element to reduce memory consumption
        elem.clear()
        # Don't attempt to clean up previous siblings. We may not have parsed them yet.
        parent = elem.getparent()
        if parent is None:
            return
        parent.remove(elem)

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
        cls._clear(elem)
        return cls(**kwargs)

    def to_xml(self, version):
        self.clean(version=version)
        # WARNING: The order of addition of XML elements is VERY important. Exchange expects XML elements in a
        # specific, non-documented order and will fail with meaningless errors if the order is wrong.

        # Call create_element() without args, to not fill up the cache with unique attribute values.
        elem = create_element(self.request_tag())

        # Add attributes
        for f in self.attribute_fields():
            if f.is_read_only:
                continue
            value = getattr(self, f.name)
            if value is None or (f.is_list and not value):
                continue
            elem.set(f.field_uri, value_to_xml_text(getattr(self, f.name)))

        # Add elements and values
        for f in self.supported_fields(version=version):
            if f.is_read_only:
                continue
            value = getattr(self, f.name)
            if value is None or (f.is_list and not value):
                continue
            set_xml_value(elem, f.to_xml(value, version=version), version)
        return elem

    @classmethod
    def request_tag(cls):
        if not cls.ELEMENT_NAME:
            raise ValueError('Class %s is missing the ELEMENT_NAME attribute' % cls)
        return {
            TNS: 't:%s' % cls.ELEMENT_NAME,
            MNS: 'm:%s' % cls.ELEMENT_NAME,
        }[cls.NAMESPACE]

    @classmethod
    def response_tag(cls):
        if not cls.NAMESPACE:
            raise ValueError('Class %s is missing the NAMESPACE attribute' % cls)
        if not cls.ELEMENT_NAME:
            raise ValueError('Class %s is missing the ELEMENT_NAME attribute' % cls)
        return '{%s}%s' % (cls.NAMESPACE, cls.ELEMENT_NAME)

    @classmethod
    def attribute_fields(cls):
        return tuple(f for f in cls.FIELDS if f.is_attribute)

    @classmethod
    def supported_fields(cls, version):
        """Return the fields supported by the given server version."""

        return tuple(f for f in cls.FIELDS if not f.is_attribute and f.supports_version(version))

    @classmethod
    def get_field_by_fieldname(cls, fieldname):
        try:
            return cls.FIELDS[fieldname]
        except KeyError:
            raise InvalidField("'%s' is not a valid field name on '%s'" % (fieldname, cls.__name__))

    @classmethod
    def validate_field(cls, field, version):
        """Take a list of fieldnames, Field or FieldPath objects pointing to item fields, and check that they are
        valid for the given version.

        :param field:
        :param version:
        """
        if not isinstance(version, Version):
            raise ValueError("'version' %r must be a Version instance" % version)
        # Allow both Field and FieldPath instances and string field paths as input
        if isinstance(field, str):
            field = cls.get_field_by_fieldname(fieldname=field)
        elif isinstance(field, FieldPath):
            field = field.field
        if not isinstance(field, Field):
            raise ValueError("Field %r must be a string, Field or FieldPath instance" % field)
        cls.get_field_by_fieldname(fieldname=field.name)  # Will raise if field name is invalid
        if not field.supports_version(version):
            # The field exists but is not valid for this version
            raise InvalidFieldForVersion(
                "Field '%s' is not supported on server version %s (supported from: %s, deprecated from: %s)"
                % (field.name, version, field.supported_from, field.deprecated_from))

    @classmethod
    def add_field(cls, field, insert_after):
        """Insert a new field at the preferred place in the tuple and update the slots cache.

        :param field:
        :param insert_after:
        """
        with cls._fields_lock:
            idx = cls.FIELDS.index_by_name(insert_after) + 1
            # This class may not have its own FIELDS attribute. Make sure not to edit an attribute belonging to a parent
            # class.
            cls.FIELDS.insert(idx, field)
            setattr(cls, _mangle(field.name), field)

    @classmethod
    def remove_field(cls, field):
        """Remove the given field and and update the slots cache.

        :param field:
        """
        with cls._fields_lock:
            # This class may not have its own FIELDS attribute. Make sure not to edit an attribute belonging to a parent
            # class.
            cls.FIELDS.remove(field)
            delattr(cls, _mangle(field.name))

    def __eq__(self, other):
        return hash(self) == hash(other)

    def __hash__(self):
        return hash(
            tuple(tuple(getattr(self, f.name) or ()) if f.is_list else getattr(self, f.name) for f in self.FIELDS)
        )

    def _field_vals(self):
        field_vals = []  # Keep sorting
        for f in self.FIELDS:
            val = getattr(self, f.name)
            if isinstance(f, EnumField) and isinstance(val, int):
                val = f.as_string(val)
            field_vals.append((f.name, val))
        return field_vals

    def __str__(self):
        return self.__class__.__name__ + '(%s)' % ', '.join(
            '%s=%r' % (name, val) for name, val in self._field_vals() if val is not None
        )

    def __repr__(self):
        return self.__class__.__name__ + '(%s)' % ', '.join(
            '%s=%r' % (name, val) for name, val in self._field_vals()
        )

Subclasses

Class variables

var ELEMENT_NAME
var FIELDS
var NAMESPACE

Static methods

def add_field(field, insert_after)

Insert a new field at the preferred place in the tuple and update the slots cache.

:param field: :param insert_after:

Expand source code
@classmethod
def add_field(cls, field, insert_after):
    """Insert a new field at the preferred place in the tuple and update the slots cache.

    :param field:
    :param insert_after:
    """
    with cls._fields_lock:
        idx = cls.FIELDS.index_by_name(insert_after) + 1
        # This class may not have its own FIELDS attribute. Make sure not to edit an attribute belonging to a parent
        # class.
        cls.FIELDS.insert(idx, field)
        setattr(cls, _mangle(field.name), field)
def attribute_fields()
Expand source code
@classmethod
def attribute_fields(cls):
    return tuple(f for f in cls.FIELDS if f.is_attribute)
def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
    cls._clear(elem)
    return cls(**kwargs)
def get_field_by_fieldname(fieldname)
Expand source code
@classmethod
def get_field_by_fieldname(cls, fieldname):
    try:
        return cls.FIELDS[fieldname]
    except KeyError:
        raise InvalidField("'%s' is not a valid field name on '%s'" % (fieldname, cls.__name__))
def remove_field(field)

Remove the given field and and update the slots cache.

:param field:

Expand source code
@classmethod
def remove_field(cls, field):
    """Remove the given field and and update the slots cache.

    :param field:
    """
    with cls._fields_lock:
        # This class may not have its own FIELDS attribute. Make sure not to edit an attribute belonging to a parent
        # class.
        cls.FIELDS.remove(field)
        delattr(cls, _mangle(field.name))
def request_tag()
Expand source code
@classmethod
def request_tag(cls):
    if not cls.ELEMENT_NAME:
        raise ValueError('Class %s is missing the ELEMENT_NAME attribute' % cls)
    return {
        TNS: 't:%s' % cls.ELEMENT_NAME,
        MNS: 'm:%s' % cls.ELEMENT_NAME,
    }[cls.NAMESPACE]
def response_tag()
Expand source code
@classmethod
def response_tag(cls):
    if not cls.NAMESPACE:
        raise ValueError('Class %s is missing the NAMESPACE attribute' % cls)
    if not cls.ELEMENT_NAME:
        raise ValueError('Class %s is missing the ELEMENT_NAME attribute' % cls)
    return '{%s}%s' % (cls.NAMESPACE, cls.ELEMENT_NAME)
def supported_fields(version)

Return the fields supported by the given server version.

Expand source code
@classmethod
def supported_fields(cls, version):
    """Return the fields supported by the given server version."""

    return tuple(f for f in cls.FIELDS if not f.is_attribute and f.supports_version(version))
def validate_field(field, version)

Take a list of fieldnames, Field or FieldPath objects pointing to item fields, and check that they are valid for the given version.

:param field: :param version:

Expand source code
@classmethod
def validate_field(cls, field, version):
    """Take a list of fieldnames, Field or FieldPath objects pointing to item fields, and check that they are
    valid for the given version.

    :param field:
    :param version:
    """
    if not isinstance(version, Version):
        raise ValueError("'version' %r must be a Version instance" % version)
    # Allow both Field and FieldPath instances and string field paths as input
    if isinstance(field, str):
        field = cls.get_field_by_fieldname(fieldname=field)
    elif isinstance(field, FieldPath):
        field = field.field
    if not isinstance(field, Field):
        raise ValueError("Field %r must be a string, Field or FieldPath instance" % field)
    cls.get_field_by_fieldname(fieldname=field.name)  # Will raise if field name is invalid
    if not field.supports_version(version):
        # The field exists but is not valid for this version
        raise InvalidFieldForVersion(
            "Field '%s' is not supported on server version %s (supported from: %s, deprecated from: %s)"
            % (field.name, version, field.supported_from, field.deprecated_from))

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    # Validate attribute values using the field validator
    for f in self.FIELDS:
        if version and not f.supports_version(version):
            continue
        if isinstance(f, ExtendedPropertyField) and not hasattr(self, f.name):
            # The extended field may have been registered after this item was created. Set default values.
            setattr(self, f.name, f.clean(None, version=version))
            continue
        val = getattr(self, f.name)
        setattr(self, f.name, f.clean(val, version=version))
def to_xml(self, version)
Expand source code
def to_xml(self, version):
    self.clean(version=version)
    # WARNING: The order of addition of XML elements is VERY important. Exchange expects XML elements in a
    # specific, non-documented order and will fail with meaningless errors if the order is wrong.

    # Call create_element() without args, to not fill up the cache with unique attribute values.
    elem = create_element(self.request_tag())

    # Add attributes
    for f in self.attribute_fields():
        if f.is_read_only:
            continue
        value = getattr(self, f.name)
        if value is None or (f.is_list and not value):
            continue
        elem.set(f.field_uri, value_to_xml_text(getattr(self, f.name)))

    # Add elements and values
    for f in self.supported_fields(version=version):
        if f.is_read_only:
            continue
        value = getattr(self, f.name)
        if value is None or (f.is_list and not value):
            continue
        set_xml_value(elem, f.to_xml(value, version=version), version)
    return elem
class EWSMeta (*args, **kwargs)

type(object_or_name, bases, dict) type(object) -> the object's type type(name, bases, dict) -> a new type

Expand source code
class EWSMeta(type, metaclass=abc.ABCMeta):
    def __new__(mcs, name, bases, kwargs):
        # Collect fields defined directly on the class
        local_fields = Fields()
        for k in tuple(kwargs.keys()):
            v = kwargs[k]
            if isinstance(v, Field):
                v.name = k
                local_fields.append(v)
                del kwargs[k]

        # Build a list of fields defined on this and all base classes
        base_fields = Fields()
        for base in bases:
            if hasattr(base, 'FIELDS'):
                base_fields += base.FIELDS

        # FIELDS defined on a model overrides the base class fields
        fields = kwargs.get('FIELDS', base_fields) + local_fields

        # Include all fields as class attributes so we can use them as instance attributes
        kwargs.update({_mangle(f.name): f for f in fields})

        # Calculate __slots__ so we don't have to hard-code it on the model
        kwargs['__slots__'] = tuple(f.name for f in fields if f.name not in base_fields) + kwargs.get('__slots__', ())

        # FIELDS is mentioned in docs and expected by internal code. Add it here, but only if the class has its own
        # fields. Otherwise, we want the implicit FIELDS from the base class (used for injecting custom fields on the
        # Folder class, making the custom field available for subclasses).
        if local_fields:
            kwargs['FIELDS'] = fields
        cls = super().__new__(mcs, name, bases, kwargs)
        cls._slots_keys = mcs._get_slots_keys(cls)
        return cls

    @staticmethod
    def _get_slots_keys(cls):
        seen = set()
        keys = []
        for c in reversed(getmro(cls)):
            if not hasattr(c, '__slots__'):
                continue
            for k in c.__slots__:
                if k in seen:
                    # We allow duplicate keys because we don't want to require subclasses of e.g.
                    # ExtendedProperty to define an empty __slots__ class attribute.
                    continue
                keys.append(k)
                seen.add(k)
        return keys

    def __getattribute__(cls, k):
        """Return Field instances via their mangled class attribute"""
        try:
            return super().__getattribute__('__dict__')[_mangle(k)]
        except KeyError:
            return super().__getattribute__(k)

Ancestors

  • builtins.type
class EffectiveRights (**kwargs)
Expand source code
class EffectiveRights(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/effectiverights"""

    ELEMENT_NAME = 'EffectiveRights'

    create_associated = BooleanField(field_uri='CreateAssociated', default=False)
    create_contents = BooleanField(field_uri='CreateContents', default=False)
    create_hierarchy = BooleanField(field_uri='CreateHierarchy', default=False)
    delete = BooleanField(field_uri='Delete', default=False)
    modify = BooleanField(field_uri='Modify', default=False)
    read = BooleanField(field_uri='Read', default=False)
    view_private_items = BooleanField(field_uri='ViewPrivateItems', default=False)

    def __contains__(self, item):
        return getattr(self, item, False)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var create_associated
var create_contents
var create_hierarchy
var delete
var modify
var read
var view_private_items

Inherited members

class Email (**kwargs)
Expand source code
class Email(AvailabilityMailbox):
    """Like AvailabilityMailbox, but with a different tag name.
    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/email-emailaddresstype
    """

    ELEMENT_NAME = 'Email'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class EmailAddress (**kwargs)
Expand source code
class EmailAddress(Mailbox):
    """Like Mailbox, but with a different tag name.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/emailaddress-emailaddresstype
    """

    ELEMENT_NAME = 'EmailAddress'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class EmailAddressAttributedValue (**kwargs)
Expand source code
class EmailAddressAttributedValue(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/emailaddressattributedvalue
    """

    ELEMENT_NAME = 'EmailAddressAttributedValue'

    value = EWSElementField(value_cls=EmailAddressTypeValue)
    attributions = EWSElementListField(field_uri='Attributions', value_cls=Attribution)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var attributions
var value

Inherited members

class EmailAddressTypeValue (**kwargs)
Expand source code
class EmailAddressTypeValue(Mailbox):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/value-emailaddresstype
    """

    ELEMENT_NAME = 'Value'

    original_display_name = TextField(field_uri='OriginalDisplayName')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var original_display_name

Inherited members

class Event (**kwargs)

Base class for all event types.

Expand source code
class Event(EWSElement, metaclass=EWSMeta):
    """Base class for all event types."""

    watermark = CharField(field_uri='Watermark')

Ancestors

Subclasses

Class variables

var FIELDS

Instance variables

var watermark

Inherited members

class ExceptionFieldURI (**kwargs)
Expand source code
class ExceptionFieldURI(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/exceptionfielduri"""

    ELEMENT_NAME = 'ExceptionFieldURI'

    field_uri = CharField(field_uri='FieldURI', is_attribute=True, is_required=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var field_uri

Inherited members

class ExtendedFieldURI (**kwargs)
Expand source code
class ExtendedFieldURI(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedfielduri"""

    ELEMENT_NAME = 'ExtendedFieldURI'

    distinguished_property_set_id = CharField(field_uri='DistinguishedPropertySetId', is_attribute=True)
    property_set_id = CharField(field_uri='PropertySetId', is_attribute=True)
    property_tag = CharField(field_uri='PropertyTag', is_attribute=True)
    property_name = CharField(field_uri='PropertyName', is_attribute=True)
    property_id = CharField(field_uri='PropertyId', is_attribute=True)
    property_type = CharField(field_uri='PropertyType', is_attribute=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var distinguished_property_set_id
var property_id
var property_name
var property_set_id
var property_tag
var property_type

Inherited members

class FailedMailbox (**kwargs)
Expand source code
class FailedMailbox(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/failedmailbox"""

    ELEMENT_NAME = 'FailedMailbox'

    mailbox = CharField(field_uri='Mailbox')
    error_code = IntegerField(field_uri='ErrorCode')
    error_message = CharField(field_uri='ErrorMessage')
    is_archive = BooleanField(field_uri='IsArchive')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var error_code
var error_message
var is_archive
var mailbox

Inherited members

class FieldURI (**kwargs)
Expand source code
class FieldURI(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/fielduri"""

    ELEMENT_NAME = 'FieldURI'

    field_uri = CharField(field_uri='FieldURI', is_attribute=True, is_required=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var field_uri

Inherited members

class Fields (*fields)

A collection type for the FIELDS class attribute. Works like a list but supports fast lookup by name.

Expand source code
class Fields(list):
    """A collection type for the FIELDS class attribute. Works like a list but supports fast lookup by name."""

    def __init__(self, *fields):
        super().__init__(fields)
        self._dict = {}
        for f in fields:
            # Check for duplicate field names
            if f.name in self._dict:
                raise ValueError('Field %r is a duplicate' % f)
            self._dict[f.name] = f

    def __getitem__(self, idx_or_slice):
        # Support fast lookup by name. Make sure slicing returns an instance of this class
        if isinstance(idx_or_slice, str):
            return self._dict[idx_or_slice]
        if isinstance(idx_or_slice, int):
            return super().__getitem__(idx_or_slice)
        res = super().__getitem__(idx_or_slice)
        return self.__class__(*res)

    def __add__(self, other):
        # Make sure addition returns an instance of this class
        res = super().__add__(other)
        return self.__class__(*res)

    def __iadd__(self, other):
        for f in other:
            self.append(f)
        return self

    def __contains__(self, item):
        if isinstance(item, str):
            return item in self._dict
        return super().__contains__(item)

    def copy(self):
        return self.__class__(*self)

    def index_by_name(self, field_name):
        for i, f in enumerate(self):
            if f.name == field_name:
                return i
        raise ValueError('Unknown field name %r' % field_name)

    def insert(self, index, field):
        if field.name in self._dict:
            raise ValueError('Field %r is a duplicate' % field)
        super().insert(index, field)
        self._dict[field.name] = field

    def remove(self, field):
        super().remove(field)
        del self._dict[field.name]

    def append(self, field):
        super().append(field)
        self._dict[field.name] = field

Ancestors

  • builtins.list

Methods

def append(self, field)

Append object to the end of the list.

Expand source code
def append(self, field):
    super().append(field)
    self._dict[field.name] = field
def copy(self)

Return a shallow copy of the list.

Expand source code
def copy(self):
    return self.__class__(*self)
def index_by_name(self, field_name)
Expand source code
def index_by_name(self, field_name):
    for i, f in enumerate(self):
        if f.name == field_name:
            return i
    raise ValueError('Unknown field name %r' % field_name)
def insert(self, index, field)

Insert object before index.

Expand source code
def insert(self, index, field):
    if field.name in self._dict:
        raise ValueError('Field %r is a duplicate' % field)
    super().insert(index, field)
    self._dict[field.name] = field
def remove(self, field)

Remove first occurrence of value.

Raises ValueError if the value is not present.

Expand source code
def remove(self, field):
    super().remove(field)
    del self._dict[field.name]
class FolderId (*args, **kwargs)
Expand source code
class FolderId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/folderid"""

    ELEMENT_NAME = 'FolderId'

Ancestors

Subclasses

Class variables

var ELEMENT_NAME

Inherited members

class FreeBusyChangedEvent (**kwargs)
Expand source code
class FreeBusyChangedEvent(TimestampEvent):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/freebusychangedevent"""

    ELEMENT_NAME = 'FreeBusyChangedEvent'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class FreeBusyView (**kwargs)
Expand source code
class FreeBusyView(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/freebusyview"""

    ELEMENT_NAME = 'FreeBusyView'
    NAMESPACE = MNS
    view_type = ChoiceField(field_uri='FreeBusyViewType', choices={
        Choice('None'), Choice('MergedOnly'), Choice('FreeBusy'), Choice('FreeBusyMerged'), Choice('Detailed'),
        Choice('DetailedMerged'),
    }, is_required=True)
    # A string of digits. Each digit points to a position in .fields.FREE_BUSY_CHOICES
    merged = CharField(field_uri='MergedFreeBusy')
    calendar_events = EWSElementListField(field_uri='CalendarEventArray', value_cls=CalendarEvent)
    # WorkingPeriod is located inside the WorkingPeriodArray element which is inside the WorkingHours element
    working_hours = EWSElementListField(field_uri='WorkingPeriodArray', value_cls=WorkingPeriod)
    # TimeZone is also inside the WorkingHours element. It contains information about the timezone which the
    # account is located in.
    working_hours_timezone = EWSElementField(value_cls=TimeZone)

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {}
        working_hours_elem = elem.find('{%s}WorkingHours' % TNS)
        for f in cls.FIELDS:
            if f.name in ['working_hours', 'working_hours_timezone']:
                if working_hours_elem is None:
                    continue
                kwargs[f.name] = f.from_xml(elem=working_hours_elem, account=account)
                continue
            kwargs[f.name] = f.from_xml(elem=elem, account=account)
        cls._clear(elem)
        return cls(**kwargs)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var NAMESPACE

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    kwargs = {}
    working_hours_elem = elem.find('{%s}WorkingHours' % TNS)
    for f in cls.FIELDS:
        if f.name in ['working_hours', 'working_hours_timezone']:
            if working_hours_elem is None:
                continue
            kwargs[f.name] = f.from_xml(elem=working_hours_elem, account=account)
            continue
        kwargs[f.name] = f.from_xml(elem=elem, account=account)
    cls._clear(elem)
    return cls(**kwargs)

Instance variables

var calendar_events
var merged
var view_type
var working_hours
var working_hours_timezone

Inherited members

class FreeBusyViewOptions (**kwargs)
Expand source code
class FreeBusyViewOptions(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/freebusyviewoptions"""

    ELEMENT_NAME = 'FreeBusyViewOptions'
    REQUESTED_VIEWS = {'MergedOnly', 'FreeBusy', 'FreeBusyMerged', 'Detailed', 'DetailedMerged'}

    time_window = EWSElementField(value_cls=TimeWindow, is_required=True)
    # Interval value is in minutes
    merged_free_busy_interval = IntegerField(field_uri='MergedFreeBusyIntervalInMinutes', min=5, max=1440, default=30,
                                             is_required=True)
    requested_view = ChoiceField(field_uri='RequestedView', choices={Choice(c) for c in REQUESTED_VIEWS},
                                 is_required=True)  # Choice('None') is also valid, but only for responses

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var REQUESTED_VIEWS

Instance variables

var merged_free_busy_interval
var requested_view
var time_window

Inherited members

class HTMLBody (...)

Helper to mark the 'body' field as a complex attribute.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/body

Expand source code
class HTMLBody(Body):
    """Helper to mark the 'body' field as a complex attribute.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/body
    """

    body_type = 'HTML'

Ancestors

Class variables

var body_type

Inherited members

class IdChangeKeyMixIn (**kwargs)

Base class for classes that have a concept of 'id' and 'changekey' values. The values are actually stored on a separate element but we add convenience methods to hide that fact.

Expand source code
class IdChangeKeyMixIn(EWSElement, metaclass=EWSMeta):
    """Base class for classes that have a concept of 'id' and 'changekey' values. The values are actually stored on
    a separate element but we add convenience methods to hide that fact.
    """

    ID_ELEMENT_CLS = None

    def __init__(self, **kwargs):
        _id = self.ID_ELEMENT_CLS(kwargs.pop('id', None), kwargs.pop('changekey', None))
        if _id.id or _id.changekey:
            kwargs['_id'] = _id
        super().__init__(**kwargs)

    @classmethod
    def get_field_by_fieldname(cls, fieldname):
        if fieldname in ('id', 'changekey'):
            return cls.ID_ELEMENT_CLS.get_field_by_fieldname(fieldname=fieldname)
        return super().get_field_by_fieldname(fieldname=fieldname)

    @property
    def id(self):
        if self._id is None:
            return None
        return self._id.id

    @id.setter
    def id(self, value):
        if self._id is None:
            self._id = self.ID_ELEMENT_CLS()
        self._id.id = value

    @property
    def changekey(self):
        if self._id is None:
            return None
        return self._id.changekey

    @changekey.setter
    def changekey(self, value):
        if self._id is None:
            self._id = self.ID_ELEMENT_CLS()
        self._id.changekey = value

    @classmethod
    def id_from_xml(cls, elem):
        # This method must be reasonably fast
        id_elem = elem.find(cls.ID_ELEMENT_CLS.response_tag())
        if id_elem is None:
            return None, None
        return id_elem.get(cls.ID_ELEMENT_CLS.ID_ATTR), id_elem.get(cls.ID_ELEMENT_CLS.CHANGEKEY_ATTR)

    def to_id_xml(self, version):
        return self._id.to_xml(version=version)

    def __eq__(self, other):
        if isinstance(other, tuple):
            return hash((self.id, self.changekey)) == hash(other)
        return super().__eq__(other)

    def __hash__(self):
        # If we have an ID and changekey, use that as key. Else return a hash of all attributes
        if self.id:
            return hash((self.id, self.changekey))
        return super().__hash__()

Ancestors

Subclasses

Class variables

var ID_ELEMENT_CLS

Static methods

def get_field_by_fieldname(fieldname)
Expand source code
@classmethod
def get_field_by_fieldname(cls, fieldname):
    if fieldname in ('id', 'changekey'):
        return cls.ID_ELEMENT_CLS.get_field_by_fieldname(fieldname=fieldname)
    return super().get_field_by_fieldname(fieldname=fieldname)
def id_from_xml(elem)
Expand source code
@classmethod
def id_from_xml(cls, elem):
    # This method must be reasonably fast
    id_elem = elem.find(cls.ID_ELEMENT_CLS.response_tag())
    if id_elem is None:
        return None, None
    return id_elem.get(cls.ID_ELEMENT_CLS.ID_ATTR), id_elem.get(cls.ID_ELEMENT_CLS.CHANGEKEY_ATTR)

Instance variables

var changekey
Expand source code
@property
def changekey(self):
    if self._id is None:
        return None
    return self._id.changekey
var id
Expand source code
@property
def id(self):
    if self._id is None:
        return None
    return self._id.id

Methods

def to_id_xml(self, version)
Expand source code
def to_id_xml(self, version):
    return self._id.to_xml(version=version)

Inherited members

class IndexedFieldURI (**kwargs)
Expand source code
class IndexedFieldURI(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/indexedfielduri"""

    ELEMENT_NAME = 'IndexedFieldURI'

    field_uri = CharField(field_uri='FieldURI', is_attribute=True, is_required=True)
    field_index = CharField(field_uri='FieldIndex', is_attribute=True, is_required=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var field_index
var field_uri

Inherited members

class ItemId (*args, **kwargs)

'id' and 'changekey' are UUIDs generated by Exchange.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/itemid

Expand source code
class ItemId(BaseItemId):
    """'id' and 'changekey' are UUIDs generated by Exchange.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/itemid
    """

    ELEMENT_NAME = 'ItemId'
    ID_ATTR = 'Id'
    CHANGEKEY_ATTR = 'ChangeKey'

    id = IdField(field_uri=ID_ATTR, is_required=True)
    changekey = IdField(field_uri=CHANGEKEY_ATTR, is_required=False)

Ancestors

Subclasses

Class variables

var CHANGEKEY_ATTR
var ELEMENT_NAME
var FIELDS
var ID_ATTR

Instance variables

var changekey
var id

Inherited members

class MailTips (**kwargs)
Expand source code
class MailTips(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailtips"""

    ELEMENT_NAME = 'MailTips'
    NAMESPACE = MNS

    recipient_address = RecipientAddressField()
    pending_mail_tips = ChoiceField(field_uri='PendingMailTips', choices={Choice(c) for c in MAIL_TIPS_TYPES})
    out_of_office = EWSElementField(value_cls=OutOfOffice)
    mailbox_full = BooleanField(field_uri='MailboxFull')
    custom_mail_tip = TextField(field_uri='CustomMailTip')
    total_member_count = IntegerField(field_uri='TotalMemberCount')
    external_member_count = IntegerField(field_uri='ExternalMemberCount')
    max_message_size = IntegerField(field_uri='MaxMessageSize')
    delivery_restricted = BooleanField(field_uri='DeliveryRestricted')
    is_moderated = BooleanField(field_uri='IsModerated')
    invalid_recipient = BooleanField(field_uri='InvalidRecipient')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var NAMESPACE

Instance variables

var custom_mail_tip
var delivery_restricted
var external_member_count
var invalid_recipient
var is_moderated
var mailbox_full
var max_message_size
var out_of_office
var pending_mail_tips
var recipient_address
var total_member_count

Inherited members

class Mailbox (**kwargs)
Expand source code
class Mailbox(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailbox"""

    ELEMENT_NAME = 'Mailbox'
    MAILBOX = 'Mailbox'
    ONE_OFF = 'OneOff'
    MAILBOX_TYPE_CHOICES = {
            Choice(MAILBOX), Choice('PublicDL'), Choice('PrivateDL'), Choice('Contact'), Choice('PublicFolder'),
            Choice('Unknown'), Choice(ONE_OFF), Choice('GroupMailbox', supported_from=EXCHANGE_2013)
        }

    name = TextField(field_uri='Name')
    email_address = EmailAddressField(field_uri='EmailAddress')
    # RoutingType values are not restricted:
    # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/routingtype-emailaddresstype
    routing_type = TextField(field_uri='RoutingType', default='SMTP')
    mailbox_type = ChoiceField(field_uri='MailboxType', choices=MAILBOX_TYPE_CHOICES, default=MAILBOX)
    item_id = EWSElementField(value_cls=ItemId, is_read_only=True)

    def clean(self, version=None):
        super().clean(version=version)

        if self.mailbox_type != self.ONE_OFF and not self.email_address and not self.item_id:
            # A OneOff Mailbox (a one-off member of a personal distribution list) may lack these fields, but other
            # Mailboxes require at least one. See also "Remarks" section of
            # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailbox
            raise ValueError("Mailbox type %r must have either 'email_address' or 'item_id' set" % self.mailbox_type)

    def __hash__(self):
        # Exchange may add 'mailbox_type' and 'name' on insert. We're satisfied if the item_id or email address matches.
        if self.item_id:
            return hash(self.item_id)
        if self.email_address:
            return hash(self.email_address.lower())
        return super().__hash__()

Ancestors

Subclasses

Class variables

var ELEMENT_NAME
var FIELDS
var MAILBOX
var MAILBOX_TYPE_CHOICES
var ONE_OFF

Instance variables

var email_address
var item_id
var mailbox_type
var name
var routing_type

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)

    if self.mailbox_type != self.ONE_OFF and not self.email_address and not self.item_id:
        # A OneOff Mailbox (a one-off member of a personal distribution list) may lack these fields, but other
        # Mailboxes require at least one. See also "Remarks" section of
        # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailbox
        raise ValueError("Mailbox type %r must have either 'email_address' or 'item_id' set" % self.mailbox_type)

Inherited members

class MailboxData (**kwargs)
Expand source code
class MailboxData(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailboxdata"""

    ELEMENT_NAME = 'MailboxData'
    ATTENDEE_TYPES = {'Optional', 'Organizer', 'Required', 'Resource', 'Room'}

    email = EmailField()
    attendee_type = ChoiceField(field_uri='AttendeeType', choices={Choice(c) for c in ATTENDEE_TYPES})
    exclude_conflicts = BooleanField(field_uri='ExcludeConflicts')

Ancestors

Class variables

var ATTENDEE_TYPES
var ELEMENT_NAME
var FIELDS

Instance variables

var attendee_type
var email
var exclude_conflicts

Inherited members

class Member (**kwargs)
Expand source code
class Member(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/member-ex15websvcsotherref
    """

    ELEMENT_NAME = 'Member'

    mailbox = MailboxField(is_required=True)
    status = ChoiceField(field_uri='Status', choices={
            Choice('Unrecognized'), Choice('Normal'), Choice('Demoted')
        }, default='Normal')

    def __hash__(self):
        return hash(self.mailbox)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var mailbox
var status

Inherited members

class MessageHeader (**kwargs)
Expand source code
class MessageHeader(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/internetmessageheader"""

    ELEMENT_NAME = 'InternetMessageHeader'

    name = TextField(field_uri='HeaderName', is_attribute=True)
    value = SubField()

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var name
var value

Inherited members

class ModifiedEvent (**kwargs)
Expand source code
class ModifiedEvent(TimestampEvent):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/modifiedevent"""

    ELEMENT_NAME = 'ModifiedEvent'

    unread_count = IntegerField(field_uri='UnreadCount')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var unread_count

Inherited members

class MovedEvent (**kwargs)
Expand source code
class MovedEvent(OldTimestampEvent):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/movedevent"""

    ELEMENT_NAME = 'MovedEvent'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class MovedItemId (*args, **kwargs)
Expand source code
class MovedItemId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/moveditemid"""

    ELEMENT_NAME = 'MovedItemId'
    NAMESPACE = MNS

    @classmethod
    def id_from_xml(cls, elem):
        item = cls.from_xml(elem=elem, account=None)
        return item.id, item.changekey

Ancestors

Class variables

var ELEMENT_NAME
var NAMESPACE

Static methods

def id_from_xml(elem)
Expand source code
@classmethod
def id_from_xml(cls, elem):
    item = cls.from_xml(elem=elem, account=None)
    return item.id, item.changekey

Inherited members

class NewMailEvent (**kwargs)
Expand source code
class NewMailEvent(TimestampEvent):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/newmailevent"""

    ELEMENT_NAME = 'NewMailEvent'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class Notification (**kwargs)
Expand source code
class Notification(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/notification-ex15websvcsotherref
    """

    ELEMENT_NAME = 'Notification'
    NAMESPACE = MNS

    subscription_id = CharField(field_uri='SubscriptionId')
    previous_watermark = CharField(field_uri='PreviousWatermark')
    more_events = BooleanField(field_uri='MoreEvents')
    events = GenericEventListField('')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var NAMESPACE

Instance variables

var events
var more_events
var previous_watermark
var subscription_id

Inherited members

class OccurrenceItemId (*args, **kwargs)
Expand source code
class OccurrenceItemId(BaseItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/occurrenceitemid"""

    ELEMENT_NAME = 'OccurrenceItemId'
    ID_ATTR = 'RecurringMasterId'
    CHANGEKEY_ATTR = 'ChangeKey'

    id = IdField(field_uri=ID_ATTR, is_required=True)
    changekey = IdField(field_uri=CHANGEKEY_ATTR, is_required=False)
    instance_index = IntegerField(field_uri='InstanceIndex', is_attribute=True, is_required=True, min=1)

Ancestors

Class variables

var CHANGEKEY_ATTR
var ELEMENT_NAME
var FIELDS
var ID_ATTR

Instance variables

var changekey
var id
var instance_index

Inherited members

class OldTimestampEvent (**kwargs)

Base class for both item and folder copy/move events.

Expand source code
class OldTimestampEvent(TimestampEvent, metaclass=EWSMeta):
    """Base class for both item and folder copy/move events."""

    old_item_id = EWSElementField(field_uri='OldItemId', value_cls=ItemId)
    old_folder_id = EWSElementField(field_uri='OldFolderId', value_cls=FolderId)
    old_parent_folder_id = EWSElementField(field_uri='OldParentFolderId', value_cls=ParentFolderId)

Ancestors

Subclasses

Class variables

var FIELDS

Instance variables

var old_folder_id
var old_item_id
var old_parent_folder_id

Inherited members

class OutOfOffice (**kwargs)
Expand source code
class OutOfOffice(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/outofoffice"""

    ELEMENT_NAME = 'OutOfOffice'

    reply_body = MessageField(field_uri='ReplyBody')
    start = DateTimeField(field_uri='StartTime', is_required=False)
    end = DateTimeField(field_uri='EndTime', is_required=False)

    @classmethod
    def duration_to_start_end(cls, elem, account):
        kwargs = {}
        duration = elem.find('{%s}Duration' % TNS)
        if duration is not None:
            for attr in ('start', 'end'):
                f = cls.get_field_by_fieldname(attr)
                kwargs[attr] = f.from_xml(elem=duration, account=account)
        return kwargs

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {}
        for attr in ('reply_body',):
            f = cls.get_field_by_fieldname(attr)
            kwargs[attr] = f.from_xml(elem=elem, account=account)
        kwargs.update(cls.duration_to_start_end(elem=elem, account=account))
        cls._clear(elem)
        return cls(**kwargs)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Static methods

def duration_to_start_end(elem, account)
Expand source code
@classmethod
def duration_to_start_end(cls, elem, account):
    kwargs = {}
    duration = elem.find('{%s}Duration' % TNS)
    if duration is not None:
        for attr in ('start', 'end'):
            f = cls.get_field_by_fieldname(attr)
            kwargs[attr] = f.from_xml(elem=duration, account=account)
    return kwargs
def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    kwargs = {}
    for attr in ('reply_body',):
        f = cls.get_field_by_fieldname(attr)
        kwargs[attr] = f.from_xml(elem=elem, account=account)
    kwargs.update(cls.duration_to_start_end(elem=elem, account=account))
    cls._clear(elem)
    return cls(**kwargs)

Instance variables

var end
var reply_body
var start

Inherited members

class ParentFolderId (*args, **kwargs)
Expand source code
class ParentFolderId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/parentfolderid"""

    ELEMENT_NAME = 'ParentFolderId'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class ParentItemId (*args, **kwargs)
Expand source code
class ParentItemId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/parentitemid"""

    ELEMENT_NAME = 'ParentItemId'
    NAMESPACE = MNS

Ancestors

Class variables

var ELEMENT_NAME
var NAMESPACE

Inherited members

class Permission (**kwargs)
Expand source code
class Permission(BasePermission):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/permission"""

    ELEMENT_NAME = 'Permission'
    LEVEL_CHOICES = (
        'None', 'Owner', 'PublishingEditor', 'Editor', 'PublishingAuthor', 'Author', 'NoneditingAuthor', 'Reviewer',
        'Contributor', 'Custom',
    )

    permission_level = ChoiceField(
        field_uri='CalendarPermissionLevel', choices={Choice(c) for c in LEVEL_CHOICES}, default=LEVEL_CHOICES[0]
    )

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var LEVEL_CHOICES

Instance variables

var permission_level

Inherited members

class PermissionSet (**kwargs)
Expand source code
class PermissionSet(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/permissionset-permissionsettype
    and
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/permissionset-calendarpermissionsettype
    """

    # For simplicity, we implement the two distinct but equally names elements as one class.
    ELEMENT_NAME = 'PermissionSet'

    permissions = EWSElementListField(field_uri='Permissions', value_cls=Permission)
    calendar_permissions = EWSElementListField(field_uri='CalendarPermissions', value_cls=CalendarPermission)
    unknown_entries = UnknownEntriesField(field_uri='UnknownEntries')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var calendar_permissions
var permissions
var unknown_entries

Inherited members

class PersonaId (*args, **kwargs)
Expand source code
class PersonaId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/personaid"""

    ELEMENT_NAME = 'PersonaId'
    NAMESPACE = MNS

    @classmethod
    def response_tag(cls):
        # This element is in MNS in the request and TNS in the response...
        return '{%s}%s' % (TNS, cls.ELEMENT_NAME)

Ancestors

Class variables

var ELEMENT_NAME
var NAMESPACE

Static methods

def response_tag()
Expand source code
@classmethod
def response_tag(cls):
    # This element is in MNS in the request and TNS in the response...
    return '{%s}%s' % (TNS, cls.ELEMENT_NAME)

Inherited members

class PersonaPhoneNumberTypeValue (**kwargs)
Expand source code
class PersonaPhoneNumberTypeValue(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/value-personaphonenumbertype
    """

    ELEMENT_NAME = 'Value'

    number = CharField(field_uri='Number')
    type = CharField(field_uri='Type')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var number
var type

Inherited members

class PersonaPostalAddressTypeValue (**kwargs)
Expand source code
class PersonaPostalAddressTypeValue(Mailbox):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/value-personapostaladdresstype
    """

    ELEMENT_NAME = 'Value'

    street = TextField(field_uri='Street')
    city = TextField(field_uri='City')
    state = TextField(field_uri='State')
    country = TextField(field_uri='Country')
    postal_code = TextField(field_uri='PostalCode')
    post_office_box = TextField(field_uri='PostOfficeBox')
    type = TextField(field_uri='Type')
    latitude = TextField(field_uri='Latitude')
    longitude = TextField(field_uri='Longitude')
    accuracy = TextField(field_uri='Accuracy')
    altitude = TextField(field_uri='Altitude')
    altitude_accuracy = TextField(field_uri='AltitudeAccuracy')
    formatted_address = TextField(field_uri='FormattedAddress')
    location_uri = TextField(field_uri='LocationUri')
    location_source = TextField(field_uri='LocationSource')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var accuracy
var altitude
var altitude_accuracy
var city
var country
var formatted_address
var latitude
var location_source
var location_uri
var longitude
var post_office_box
var postal_code
var state
var street
var type

Inherited members

class PhoneNumber (**kwargs)
Expand source code
class PhoneNumber(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/phonenumber"""

    ELEMENT_NAME = 'PhoneNumber'

    number = CharField(field_uri='Number')
    type = CharField(field_uri='Type')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var number
var type

Inherited members

class PhoneNumberAttributedValue (**kwargs)
Expand source code
class PhoneNumberAttributedValue(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/phonenumberattributedvalue
    """

    ELEMENT_NAME = 'PhoneNumberAttributedValue'

    value = EWSElementField(value_cls=PersonaPhoneNumberTypeValue)
    attributions = CharListField(field_uri='Attributions', list_elem_name='Attribution')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var attributions
var value

Inherited members

class PostalAddressAttributedValue (**kwargs)
Expand source code
class PostalAddressAttributedValue(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postaladdressattributedvalue
    """

    ELEMENT_NAME = 'PostalAddressAttributedValue'

    value = EWSElementField(value_cls=PersonaPostalAddressTypeValue)
    attributions = EWSElementListField(field_uri='Attributions', value_cls=Attribution)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var attributions
var value

Inherited members

class RecipientAddress (**kwargs)
Expand source code
class RecipientAddress(Mailbox):
    """Like Mailbox, but with a different tag name.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/recipientaddress
    """

    ELEMENT_NAME = 'RecipientAddress'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class RecurringMasterItemId (*args, **kwargs)
Expand source code
class RecurringMasterItemId(BaseItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/recurringmasteritemid"""

    ELEMENT_NAME = 'RecurringMasterItemId'
    ID_ATTR = 'OccurrenceId'
    CHANGEKEY_ATTR = 'ChangeKey'

    id = IdField(field_uri=ID_ATTR, is_required=True)
    changekey = IdField(field_uri=CHANGEKEY_ATTR, is_required=False)

Ancestors

Class variables

var CHANGEKEY_ATTR
var ELEMENT_NAME
var FIELDS
var ID_ATTR

Instance variables

var changekey
var id

Inherited members

class ReferenceItemId (*args, **kwargs)
Expand source code
class ReferenceItemId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/referenceitemid"""

    ELEMENT_NAME = 'ReferenceItemId'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class ReminderMessageData (**kwargs)
Expand source code
class ReminderMessageData(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/remindermessagedata"""

    ELEMENT_NAME = 'ReminderMessageData'

    reminder_text = CharField(field_uri='ReminderText')
    location = CharField(field_uri='Location')
    start_time = TimeField(field_uri='StartTime')
    end_time = TimeField(field_uri='EndTime')
    associated_calendar_item_id = AssociatedCalendarItemIdField(field_uri='AssociatedCalendarItemId',
                                                                supported_from=Build(15, 0, 913, 9))

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var associated_calendar_item_id
var end_time
var location
var reminder_text
var start_time

Inherited members

class RemoveItem (**kwargs)
Expand source code
class RemoveItem(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/removeitem"""

    ELEMENT_NAME = 'RemoveItem'

    reference_item_id = ReferenceItemIdField(field_uri='item:ReferenceItemId')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var reference_item_id

Inherited members

class ResponseObjects (**kwargs)
Expand source code
class ResponseObjects(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/responseobjects"""

    ELEMENT_NAME = 'ResponseObjects'
    NAMESPACE = EWSElement.NAMESPACE

    accept_item = EWSElementField(field_uri='AcceptItem', value_cls='AcceptItem', namespace=NAMESPACE)
    tentatively_accept_item = EWSElementField(field_uri='TentativelyAcceptItem', value_cls='TentativelyAcceptItem',
                                              namespace=NAMESPACE)
    decline_item = EWSElementField(field_uri='DeclineItem', value_cls='DeclineItem', namespace=NAMESPACE)
    reply_to_item = EWSElementField(field_uri='ReplyToItem', value_cls='ReplyToItem', namespace=NAMESPACE)
    forward_item = EWSElementField(field_uri='ForwardItem', value_cls='ForwardItem', namespace=NAMESPACE)
    reply_all_to_item = EWSElementField(field_uri='ReplyAllToItem', value_cls='ReplyAllToItem', namespace=NAMESPACE)
    cancel_calendar_item = EWSElementField(field_uri='CancelCalendarItem', value_cls='CancelCalendarItem',
                                           namespace=NAMESPACE)
    remove_item = EWSElementField(field_uri='RemoveItem', value_cls=RemoveItem)
    post_reply_item = EWSElementField(field_uri='PostReplyItem', value_cls='PostReplyItem',
                                      namespace=EWSElement.NAMESPACE)
    success_read_receipt = EWSElementField(field_uri='SuppressReadReceipt', value_cls=SuppressReadReceipt)
    accept_sharing_invitation = EWSElementField(field_uri='AcceptSharingInvitation',
                                                value_cls=AcceptSharingInvitation)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var NAMESPACE

Instance variables

var accept_item
var accept_sharing_invitation
var cancel_calendar_item
var decline_item
var forward_item
var post_reply_item
var remove_item
var reply_all_to_item
var reply_to_item
var success_read_receipt
var tentatively_accept_item

Inherited members

class Room (**kwargs)
Expand source code
class Room(Mailbox):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/room"""

    ELEMENT_NAME = 'Room'

    @classmethod
    def from_xml(cls, elem, account):
        id_elem = elem.find('{%s}Id' % TNS)
        item_id_elem = id_elem.find(ItemId.response_tag())
        kwargs = dict(
            name=get_xml_attr(id_elem, '{%s}Name' % TNS),
            email_address=get_xml_attr(id_elem, '{%s}EmailAddress' % TNS),
            mailbox_type=get_xml_attr(id_elem, '{%s}MailboxType' % TNS),
            item_id=ItemId.from_xml(elem=item_id_elem, account=account) if item_id_elem else None,
        )
        cls._clear(elem)
        return cls(**kwargs)

Ancestors

Class variables

var ELEMENT_NAME

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    id_elem = elem.find('{%s}Id' % TNS)
    item_id_elem = id_elem.find(ItemId.response_tag())
    kwargs = dict(
        name=get_xml_attr(id_elem, '{%s}Name' % TNS),
        email_address=get_xml_attr(id_elem, '{%s}EmailAddress' % TNS),
        mailbox_type=get_xml_attr(id_elem, '{%s}MailboxType' % TNS),
        item_id=ItemId.from_xml(elem=item_id_elem, account=account) if item_id_elem else None,
    )
    cls._clear(elem)
    return cls(**kwargs)

Inherited members

class RoomList (**kwargs)
Expand source code
class RoomList(Mailbox):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/roomlist"""

    ELEMENT_NAME = 'RoomList'
    NAMESPACE = MNS

    @classmethod
    def response_tag(cls):
        # In a GetRoomLists response, room lists are delivered as Address elements. See
        # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/address-emailaddresstype
        return '{%s}Address' % TNS

Ancestors

Class variables

var ELEMENT_NAME
var NAMESPACE

Static methods

def response_tag()
Expand source code
@classmethod
def response_tag(cls):
    # In a GetRoomLists response, room lists are delivered as Address elements. See
    # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/address-emailaddresstype
    return '{%s}Address' % TNS

Inherited members

class RootItemId (*args, **kwargs)
Expand source code
class RootItemId(BaseItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/rootitemid"""

    ELEMENT_NAME = 'RootItemId'
    NAMESPACE = MNS
    ID_ATTR = 'RootItemId'
    CHANGEKEY_ATTR = 'RootItemChangeKey'

    id = IdField(field_uri=ID_ATTR, is_required=True)
    changekey = IdField(field_uri=CHANGEKEY_ATTR, is_required=True)

Ancestors

Class variables

var CHANGEKEY_ATTR
var ELEMENT_NAME
var FIELDS
var ID_ATTR
var NAMESPACE

Instance variables

var changekey
var id

Inherited members

class SearchableMailbox (**kwargs)
Expand source code
class SearchableMailbox(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/searchablemailbox"""

    ELEMENT_NAME = 'SearchableMailbox'

    guid = CharField(field_uri='Guid')
    primary_smtp_address = EmailAddressField(field_uri='PrimarySmtpAddress')
    is_external = BooleanField(field_uri='IsExternalMailbox')
    external_email = EmailAddressField(field_uri='ExternalEmailAddress')
    display_name = CharField(field_uri='DisplayName')
    is_membership_group = BooleanField(field_uri='IsMembershipGroup')
    reference_id = CharField(field_uri='ReferenceId')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var display_name
var external_email
var guid
var is_external
var is_membership_group
var primary_smtp_address
var reference_id

Inherited members

class SendingAs (**kwargs)

Like Mailbox, but creates elements in the 'messages' namespace when sending requests.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/sendingas

Expand source code
class SendingAs(Mailbox):
    """Like Mailbox, but creates elements in the 'messages' namespace when sending requests.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/sendingas
    """

    ELEMENT_NAME = 'SendingAs'
    NAMESPACE = MNS

Ancestors

Class variables

var ELEMENT_NAME
var NAMESPACE

Inherited members

class SourceId (*args, **kwargs)
Expand source code
class SourceId(ItemId):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/sourceid"""

    ELEMENT_NAME = 'SourceId'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class StandardTime (**kwargs)
Expand source code
class StandardTime(TimeZoneTransition):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/standardtime"""

    ELEMENT_NAME = 'StandardTime'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class StatusEvent (**kwargs)
Expand source code
class StatusEvent(Event):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/statusevent"""

    ELEMENT_NAME = 'StatusEvent'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class StringAttributedValue (**kwargs)
Expand source code
class StringAttributedValue(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/stringattributedvalue
    """

    ELEMENT_NAME = 'StringAttributedValue'

    value = CharField(field_uri='Value')
    attributions = CharListField(field_uri='Attributions', list_elem_name='Attribution')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var attributions
var value

Inherited members

class SuppressReadReceipt (**kwargs)
Expand source code
class SuppressReadReceipt(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/suppressreadreceipt"""

    ELEMENT_NAME = 'SuppressReadReceipt'

    reference_item_id = ReferenceItemIdField(field_uri='item:ReferenceItemId')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var reference_item_id

Inherited members

class TimeWindow (**kwargs)
Expand source code
class TimeWindow(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/timewindow"""

    ELEMENT_NAME = 'TimeWindow'

    start = DateTimeField(field_uri='StartTime', is_required=True)
    end = DateTimeField(field_uri='EndTime', is_required=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var end
var start

Inherited members

class TimeZone (**kwargs)
Expand source code
class TimeZone(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/timezone-availability"""

    ELEMENT_NAME = 'TimeZone'

    bias = IntegerField(field_uri='Bias', is_required=True)  # Standard (non-DST) offset from UTC, in minutes
    standard_time = EWSElementField(value_cls=StandardTime)
    daylight_time = EWSElementField(value_cls=DaylightTime)

    def to_server_timezone(self, timezones, for_year):
        """Return the Microsoft timezone ID corresponding to this timezone. There may not be a match at all, and there
        may be multiple matches. If so, return a random timezone ID.

        :param timezones: A list of server timezones, as returned by
          Protocol.get_timezones(return_full_timezone_data=True)
        :param for_year: return: A Microsoft timezone ID, as a string

        :return: A Microsoft timezone ID, as a string
        """
        candidates = set()
        for tz_id, tz_name, tz_periods, tz_transitions, tz_transitions_groups in timezones:
            candidate = self.from_server_timezone(tz_periods, tz_transitions, tz_transitions_groups, for_year)
            if candidate == self:
                log.debug('Found exact candidate: %s (%s)', tz_id, tz_name)
                # We prefer this timezone over anything else. Return immediately.
                return tz_id
            # Reduce list based on base bias and standard / daylight bias values
            if candidate.bias != self.bias:
                continue
            if candidate.standard_time is None:
                if self.standard_time is not None:
                    continue
            else:
                if self.standard_time is None:
                    continue
                if candidate.standard_time.bias != self.standard_time.bias:
                    continue
            if candidate.daylight_time is None:
                if self.daylight_time is not None:
                    continue
            else:
                if self.daylight_time is None:
                    continue
                if candidate.daylight_time.bias != self.daylight_time.bias:
                    continue
            log.debug('Found candidate with matching biases: %s (%s)', tz_id, tz_name)
            candidates.add(tz_id)
        if not candidates:
            raise ValueError('No server timezones match this timezone definition')
        if len(candidates) == 1:
            log.info('Could not find an exact timezone match for %s. Selecting the best candidate', self)
        else:
            log.warning('Could not find an exact timezone match for %s. Selecting a random candidate', self)
        return candidates.pop()

    @classmethod
    def from_server_timezone(cls, periods, transitions, transitionsgroups, for_year):
        # Creates a TimeZone object from the result of a GetServerTimeZones call with full timezone data

        # Get the default bias
        bias = cls._get_bias(periods=periods, for_year=for_year)

        # Get a relevant transition ID
        valid_tg_id = cls._get_valid_transition_id(transitions=transitions, for_year=for_year)
        transitiongroup = transitionsgroups[valid_tg_id]
        if not 0 <= len(transitiongroup) <= 2:
            raise ValueError('Expected 0-2 transitions in transitionsgroup %s' % transitiongroup)

        standard_time, daylight_time = cls._get_std_and_dst(transitiongroup=transitiongroup, periods=periods, bias=bias)
        return cls(bias=bias, standard_time=standard_time, daylight_time=daylight_time)

    @staticmethod
    def _get_bias(periods, for_year):
        # Set a default bias
        valid_period = None
        for (year, period_type), period in sorted(periods.items()):
            if year > for_year:
                break
            if period_type != 'Standard':
                continue
            valid_period = period
        if valid_period is None:
            raise TimezoneDefinitionInvalidForYear('Year %s not included in periods %s' % (for_year, periods))
        return int(valid_period['bias'].total_seconds()) // 60  # Convert to minutes

    @staticmethod
    def _get_valid_transition_id(transitions, for_year):
        # Look through the transitions, and pick the relevant one according to the 'for_year' value
        valid_tg_id = None
        for tg_id, from_date in sorted(transitions.items()):
            if from_date and from_date.year > for_year:
                break
            valid_tg_id = tg_id
        if valid_tg_id is None:
            raise ValueError('No valid transition for year %s: %s' % (for_year, transitions))
        return valid_tg_id

    @staticmethod
    def _get_std_and_dst(transitiongroup, periods, bias):
        # Return 'standard_time' and 'daylight_time' objects. We do unnecessary work here, but it keeps code simple.
        standard_time, daylight_time = None, None
        for transition in transitiongroup:
            period = periods[transition['to']]
            if len(transition) == 1:
                # This is a simple transition representing a timezone with no DST. Some servers don't accept TimeZone
                # elements without a STD and DST element (see issue #488). Return StandardTime and DaylightTime objects
                # with dummy values and 0 bias - this satisfies the broken servers and hopefully doesn't break the
                # well-behaving servers.
                standard_time = StandardTime(bias=0, time=datetime.time(0), occurrence=1, iso_month=1, weekday=1)
                daylight_time = DaylightTime(bias=0, time=datetime.time(0), occurrence=5, iso_month=12, weekday=7)
                continue
            # 'offset' is the time of day to transition, as timedelta since midnight. Must be a reasonable value
            if not datetime.timedelta(0) <= transition['offset'] < datetime.timedelta(days=1):
                raise ValueError("'offset' value %s must be be between 0 and 24 hours" % transition['offset'])
            transition_kwargs = dict(
                time=(datetime.datetime(2000, 1, 1) + transition['offset']).time(),
                occurrence=transition['occurrence'],
                iso_month=transition['iso_month'],
                weekday=transition['iso_weekday'],
            )
            if period['name'] == 'Standard':
                transition_kwargs['bias'] = 0
                standard_time = StandardTime(**transition_kwargs)
                continue
            if period['name'] == 'Daylight':
                dst_bias = int(period['bias'].total_seconds()) // 60  # Convert to minutes
                transition_kwargs['bias'] = dst_bias - bias
                daylight_time = DaylightTime(**transition_kwargs)
                continue
            raise ValueError('Unknown transition: %s' % transition)
        return standard_time, daylight_time

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Static methods

def from_server_timezone(periods, transitions, transitionsgroups, for_year)
Expand source code
@classmethod
def from_server_timezone(cls, periods, transitions, transitionsgroups, for_year):
    # Creates a TimeZone object from the result of a GetServerTimeZones call with full timezone data

    # Get the default bias
    bias = cls._get_bias(periods=periods, for_year=for_year)

    # Get a relevant transition ID
    valid_tg_id = cls._get_valid_transition_id(transitions=transitions, for_year=for_year)
    transitiongroup = transitionsgroups[valid_tg_id]
    if not 0 <= len(transitiongroup) <= 2:
        raise ValueError('Expected 0-2 transitions in transitionsgroup %s' % transitiongroup)

    standard_time, daylight_time = cls._get_std_and_dst(transitiongroup=transitiongroup, periods=periods, bias=bias)
    return cls(bias=bias, standard_time=standard_time, daylight_time=daylight_time)

Instance variables

var bias
var daylight_time
var standard_time

Methods

def to_server_timezone(self, timezones, for_year)

Return the Microsoft timezone ID corresponding to this timezone. There may not be a match at all, and there may be multiple matches. If so, return a random timezone ID.

:param timezones: A list of server timezones, as returned by Protocol.get_timezones(return_full_timezone_data=True) :param for_year: return: A Microsoft timezone ID, as a string

:return: A Microsoft timezone ID, as a string

Expand source code
def to_server_timezone(self, timezones, for_year):
    """Return the Microsoft timezone ID corresponding to this timezone. There may not be a match at all, and there
    may be multiple matches. If so, return a random timezone ID.

    :param timezones: A list of server timezones, as returned by
      Protocol.get_timezones(return_full_timezone_data=True)
    :param for_year: return: A Microsoft timezone ID, as a string

    :return: A Microsoft timezone ID, as a string
    """
    candidates = set()
    for tz_id, tz_name, tz_periods, tz_transitions, tz_transitions_groups in timezones:
        candidate = self.from_server_timezone(tz_periods, tz_transitions, tz_transitions_groups, for_year)
        if candidate == self:
            log.debug('Found exact candidate: %s (%s)', tz_id, tz_name)
            # We prefer this timezone over anything else. Return immediately.
            return tz_id
        # Reduce list based on base bias and standard / daylight bias values
        if candidate.bias != self.bias:
            continue
        if candidate.standard_time is None:
            if self.standard_time is not None:
                continue
        else:
            if self.standard_time is None:
                continue
            if candidate.standard_time.bias != self.standard_time.bias:
                continue
        if candidate.daylight_time is None:
            if self.daylight_time is not None:
                continue
        else:
            if self.daylight_time is None:
                continue
            if candidate.daylight_time.bias != self.daylight_time.bias:
                continue
        log.debug('Found candidate with matching biases: %s (%s)', tz_id, tz_name)
        candidates.add(tz_id)
    if not candidates:
        raise ValueError('No server timezones match this timezone definition')
    if len(candidates) == 1:
        log.info('Could not find an exact timezone match for %s. Selecting the best candidate', self)
    else:
        log.warning('Could not find an exact timezone match for %s. Selecting a random candidate', self)
    return candidates.pop()

Inherited members

class TimeZoneTransition (**kwargs)

Base class for StandardTime and DaylightTime classes.

Expand source code
class TimeZoneTransition(EWSElement, metaclass=EWSMeta):
    """Base class for StandardTime and DaylightTime classes."""

    bias = IntegerField(field_uri='Bias', is_required=True)  # Offset from the default bias, in minutes
    time = TimeField(field_uri='Time', is_required=True)
    occurrence = IntegerField(field_uri='DayOrder', is_required=True)  # n'th occurrence of weekday in iso_month
    iso_month = IntegerField(field_uri='Month', is_required=True)
    weekday = EnumField(field_uri='DayOfWeek', enum=WEEKDAY_NAMES, is_required=True)
    # 'Year' is not implemented yet

    @classmethod
    def from_xml(cls, elem, account):
        res = super().from_xml(elem, account)
        # Some parts of EWS use '5' to mean 'last occurrence in month', others use '-1'. Let's settle on '5' because
        # only '5' is accepted in requests.
        if res.occurrence == -1:
            res.occurrence = 5
        return res

    def clean(self, version=None):
        super().clean(version=version)
        if self.occurrence == -1:
            # See from_xml()
            self.occurrence = 5

Ancestors

Subclasses

Class variables

var FIELDS

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    res = super().from_xml(elem, account)
    # Some parts of EWS use '5' to mean 'last occurrence in month', others use '-1'. Let's settle on '5' because
    # only '5' is accepted in requests.
    if res.occurrence == -1:
        res.occurrence = 5
    return res

Instance variables

var bias
var iso_month
var occurrence
var time
var weekday

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)
    if self.occurrence == -1:
        # See from_xml()
        self.occurrence = 5

Inherited members

class TimestampEvent (**kwargs)

Base class for both item and folder events with a timestamp.

Expand source code
class TimestampEvent(Event, metaclass=EWSMeta):
    """Base class for both item and folder events with a timestamp."""

    FOLDER = 'folder'
    ITEM = 'item'

    timestamp = DateTimeField(field_uri='TimeStamp')
    item_id = EWSElementField(field_uri='ItemId', value_cls=ItemId)
    folder_id = EWSElementField(field_uri='FolderId', value_cls=FolderId)
    parent_folder_id = EWSElementField(field_uri='ParentFolderId', value_cls=ParentFolderId)

    @property
    def event_type(self):
        if self.item_id is not None:
            return self.ITEM
        if self.folder_id is not None:
            return self.FOLDER
        return None  # Empty object

Ancestors

Subclasses

Class variables

var FIELDS
var FOLDER
var ITEM

Instance variables

var event_type
Expand source code
@property
def event_type(self):
    if self.item_id is not None:
        return self.ITEM
    if self.folder_id is not None:
        return self.FOLDER
    return None  # Empty object
var folder_id
var item_id
var parent_folder_id
var timestamp

Inherited members

class UID (uid)

Helper class to encode Calendar UIDs. See issue #453. Example:

class GlobalObjectId(ExtendedProperty): distinguished_property_set_id = 'Meeting' property_id = 3 property_type = 'Binary'

CalendarItem.register('global_object_id', GlobalObjectId) account.calendar.filter(global_object_id=UID('261cbc18-1f65-5a0a-bd11-23b1e224cc2f'))

Expand source code
class UID(bytes):
    """Helper class to encode Calendar UIDs. See issue #453. Example:

    class GlobalObjectId(ExtendedProperty):
        distinguished_property_set_id = 'Meeting'
        property_id = 3
        property_type = 'Binary'

    CalendarItem.register('global_object_id', GlobalObjectId)
    account.calendar.filter(global_object_id=UID('261cbc18-1f65-5a0a-bd11-23b1e224cc2f'))
    """

    _HEADER = binascii.hexlify(bytearray((
        0x04, 0x00, 0x00, 0x00,
        0x82, 0x00, 0xE0, 0x00,
        0x74, 0xC5, 0xB7, 0x10,
        0x1A, 0x82, 0xE0, 0x08)))

    _EXCEPTION_REPLACEMENT_TIME = binascii.hexlify(bytearray((
        0, 0, 0, 0)))

    _CREATION_TIME = binascii.hexlify(bytearray((
        0, 0, 0, 0,
        0, 0, 0, 0)))

    _RESERVED = binascii.hexlify(bytearray((
        0, 0, 0, 0,
        0, 0, 0, 0)))

    # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxocal/1d3aac05-a7b9-45cc-a213-47f0a0a2c5c1
    # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-asemail/e7424ddc-dd10-431e-a0b7-5c794863370e
    # https://stackoverflow.com/questions/42259122
    # https://stackoverflow.com/questions/33757805

    def __new__(cls, uid):
        payload = binascii.hexlify(bytearray('vCal-Uid\x01\x00\x00\x00{}\x00'.format(uid).encode('ascii')))
        length = binascii.hexlify(bytearray(struct.pack('<I', int(len(payload)/2))))
        encoding = b''.join([
            cls._HEADER, cls._EXCEPTION_REPLACEMENT_TIME, cls._CREATION_TIME, cls._RESERVED, length, payload
        ])
        return super().__new__(cls, codecs.decode(encoding, 'hex'))

    @classmethod
    def to_global_object_id(cls, uid):
        """Converts a UID as returned by EWS to GlobalObjectId format"""
        return binascii.unhexlify(uid)

Ancestors

  • builtins.bytes

Static methods

def to_global_object_id(uid)

Converts a UID as returned by EWS to GlobalObjectId format

Expand source code
@classmethod
def to_global_object_id(cls, uid):
    """Converts a UID as returned by EWS to GlobalObjectId format"""
    return binascii.unhexlify(uid)
class UserConfiguration (**kwargs)
Expand source code
class UserConfiguration(IdChangeKeyMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/userconfiguration"""

    ELEMENT_NAME = 'UserConfiguration'
    NAMESPACE = MNS
    ID_ELEMENT_CLS = ItemId

    _id = IdElementField(field_uri='ItemId', value_cls=ID_ELEMENT_CLS)
    user_configuration_name = EWSElementField(value_cls=UserConfigurationName)
    dictionary = DictionaryField(field_uri='Dictionary')
    xml_data = Base64Field(field_uri='XmlData')
    binary_data = Base64Field(field_uri='BinaryData')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var ID_ELEMENT_CLS

'id' and 'changekey' are UUIDs generated by Exchange.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/itemid

var NAMESPACE

Instance variables

var binary_data
var dictionary
var user_configuration_name
var xml_data

Inherited members

class UserConfigurationName (**kwargs)
Expand source code
class UserConfigurationName(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/userconfigurationname"""

    ELEMENT_NAME = 'UserConfigurationName'
    NAMESPACE = TNS

    name = CharField(field_uri='Name', is_attribute=True)
    folder = EWSElementField(value_cls=FolderId)

    def clean(self, version=None):
        from .folders import BaseFolder
        if isinstance(self.folder, BaseFolder):
            self.folder = self.folder.to_folder_id()
        super().clean(version=version)

    @classmethod
    def from_xml(cls, elem, account):
        # We also accept distinguished folders
        f = EWSElementField(value_cls=DistinguishedFolderId)
        distinguished_folder_id = f.from_xml(elem=elem, account=account)
        res = super().from_xml(elem=elem, account=account)
        if distinguished_folder_id:
            res.folder = distinguished_folder_id
        return res

Ancestors

Subclasses

Class variables

var ELEMENT_NAME
var FIELDS
var NAMESPACE

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    # We also accept distinguished folders
    f = EWSElementField(value_cls=DistinguishedFolderId)
    distinguished_folder_id = f.from_xml(elem=elem, account=account)
    res = super().from_xml(elem=elem, account=account)
    if distinguished_folder_id:
        res.folder = distinguished_folder_id
    return res

Instance variables

var folder
var name

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    from .folders import BaseFolder
    if isinstance(self.folder, BaseFolder):
        self.folder = self.folder.to_folder_id()
    super().clean(version=version)

Inherited members

class UserConfigurationNameMNS (**kwargs)
Expand source code
class UserConfigurationNameMNS(UserConfigurationName):
    """Like UserConfigurationName, but in the MNS namespace.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/userconfigurationname
    """

    NAMESPACE = MNS

Ancestors

Class variables

var NAMESPACE

Inherited members

class UserId (**kwargs)
Expand source code
class UserId(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/userid"""

    ELEMENT_NAME = 'UserId'

    sid = CharField(field_uri='SID')
    primary_smtp_address = EmailAddressField(field_uri='PrimarySmtpAddress')
    display_name = CharField(field_uri='DisplayName')
    distinguished_user = ChoiceField(field_uri='DistinguishedUser', choices={
        Choice('Default'), Choice('Anonymous')
    })
    external_user_identity = CharField(field_uri='ExternalUserIdentity')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var display_name
var distinguished_user
var external_user_identity
var primary_smtp_address
var sid

Inherited members

class WorkingPeriod (**kwargs)
Expand source code
class WorkingPeriod(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/workingperiod"""

    ELEMENT_NAME = 'WorkingPeriod'

    weekdays = EnumListField(field_uri='DayOfWeek', enum=WEEKDAY_NAMES, is_required=True)
    start = TimeField(field_uri='StartTimeInMinutes', is_required=True)
    end = TimeField(field_uri='EndTimeInMinutes', is_required=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var end
var start
var weekdays

Inherited members

exchangelib-4.6.1/docs/exchangelib/protocol.html000066400000000000000000004332641414601472700217450ustar00rootroot00000000000000 exchangelib.protocol API documentation

Module exchangelib.protocol

A protocol is an endpoint for EWS service connections. It contains all necessary information to make HTTPS connections.

Protocols should be accessed through an Account, and are either created from a default Configuration or autodiscovered when creating an Account.

Expand source code
"""
A protocol is an endpoint for EWS service connections. It contains all necessary information to make HTTPS connections.

Protocols should be accessed through an Account, and are either created from a default Configuration or autodiscovered
when creating an Account.
"""
import abc
import datetime
import logging
import os
from queue import LifoQueue, Empty, Full
from threading import Lock

import requests.adapters
import requests.sessions
import requests.utils
from oauthlib.oauth2 import BackendApplicationClient, WebApplicationClient
from requests_oauthlib import OAuth2Session

from .credentials import OAuth2AuthorizationCodeCredentials, OAuth2Credentials
from .errors import TransportError, SessionPoolMinSizeReached, SessionPoolMaxSizeReached, RateLimitError, CASError, \
    ErrorInvalidSchemaVersionForMailboxVersion, UnauthorizedError, MalformedResponseError
from .properties import FreeBusyViewOptions, MailboxData, TimeWindow, TimeZone, RoomList, DLMailbox
from .services import GetServerTimeZones, GetRoomLists, GetRooms, ResolveNames, GetUserAvailability, \
    GetSearchableMailboxes, ExpandDL, ConvertId
from .transport import get_auth_instance, get_service_authtype, NTLM, OAUTH2, CREDENTIALS_REQUIRED, DEFAULT_HEADERS
from .version import Version, API_VERSIONS

log = logging.getLogger(__name__)


def close_connections():
    CachingProtocol.clear_cache()


class BaseProtocol:
    """Base class for Protocol which implements the bare essentials."""

    # The maximum number of sessions (== TCP connections, see below) we will open to this service endpoint. Keep this
    # low unless you have an agreement with the Exchange admin on the receiving end to hammer the server and
    # rate-limiting policies have been disabled for the connecting user. Changing this setting only makes sense if
    # you are using a thread pool to run multiple concurrent workers in this process.
    SESSION_POOLSIZE = 1
    # We want only 1 TCP connection per Session object. We may have lots of different credentials hitting the server and
    # each credential needs its own session (NTLM auth will only send credentials once and then secure the connection,
    # so a connection can only handle requests for one credential). Having multiple connections per Session could
    # quickly exhaust the maximum number of concurrent connections the Exchange server allows from one client.
    CONNECTIONS_PER_SESSION = 1
    # The number of times a session may be reused before creating a new session object. 'None' means "infinite".
    # Discarding sessions after a certain number of usages may limit memory leaks in the Session object.
    MAX_SESSION_USAGE_COUNT = None
    # Timeout for HTTP requests
    TIMEOUT = 120

    # The adapter class to use for HTTP requests. Override this if you need e.g. proxy support or specific TLS versions
    HTTP_ADAPTER_CLS = requests.adapters.HTTPAdapter

    # The User-Agent header to use for HTTP requests. Override this to set an app-specific one
    USERAGENT = None

    def __init__(self, config):
        from .configuration import Configuration
        if not isinstance(config, Configuration):
            raise ValueError("'config' %r must be a Configuration instance" % config)
        if not config.service_endpoint:
            raise AttributeError("'config.service_endpoint' must be set")
        self.config = config
        self._session_pool_size = 0
        self._session_pool_maxsize = config.max_connections or self.SESSION_POOLSIZE

        # Try to behave nicely with the remote server. We want to keep the connection open between requests.
        # We also want to re-use sessions, to avoid the NTLM auth handshake on every request. We must know the
        # authentication method to create sessions.
        self._session_pool = LifoQueue()
        self._session_pool_lock = Lock()

    @property
    def service_endpoint(self):
        return self.config.service_endpoint

    @property
    def auth_type(self):
        return self.config.auth_type

    @property
    def credentials(self):
        return self.config.credentials

    @credentials.setter
    def credentials(self, value):
        # We are updating credentials, but that doesn't automatically propagate to the session objects. The simplest
        # solution is to just kill the sessions in the pool.
        with self._session_pool_lock:
            self.config._credentials = value
            self.close()

    @property
    def retry_policy(self):
        return self.config.retry_policy

    @property
    def server(self):
        return self.config.server

    def __getstate__(self):
        # The session pool and lock cannot be pickled
        state = self.__dict__.copy()
        del state['_session_pool']
        del state['_session_pool_lock']
        return state

    def __setstate__(self, state):
        # Restore the session pool and lock
        self.__dict__.update(state)
        self._session_pool = LifoQueue()
        self._session_pool_lock = Lock()

    def __del__(self):
        # pylint: disable=bare-except
        try:
            self.close()
        except Exception:  # nosec
            # __del__ should never fail
            pass

    def close(self):
        log.debug('Server %s: Closing sessions', self.server)
        while True:
            try:
                session = self._session_pool.get(block=False)
                self.close_session(session)
                self._session_pool_size -= 1
            except Empty:
                break

    @classmethod
    def get_adapter(cls):
        # We want just one connection per session. No retries, since we wrap all requests in our own retry handler
        return cls.HTTP_ADAPTER_CLS(
            pool_block=True,
            pool_connections=cls.CONNECTIONS_PER_SESSION,
            pool_maxsize=cls.CONNECTIONS_PER_SESSION,
            max_retries=0,
        )

    @property
    def session_pool_size(self):
        return self._session_pool_size

    def increase_poolsize(self):
        """Increases the session pool size. We increase by one session per call."""
        # Create a single session and insert it into the pool. We need to protect this with a lock while we are changing
        # the pool size variable, to avoid race conditions. We must not exceed the pool size limit.
        if self._session_pool_size == self._session_pool_maxsize:
            raise SessionPoolMaxSizeReached('Session pool size cannot be increased further')
        with self._session_pool_lock:
            if self._session_pool_size >= self._session_pool_maxsize:
                log.debug('Session pool size was increased in another thread')
                return
            log.debug('Server %s: Increasing session pool size from %s to %s', self.server, self._session_pool_size,
                      self._session_pool_size + 1)
            self._session_pool.put(self.create_session(), block=False)
            self._session_pool_size += 1

    def decrease_poolsize(self):
        """Decreases the session pool size in response to error messages from the server requesting to rate-limit
        requests. We decrease by one session per call.
        """
        # Take a single session from the pool and discard it. We need to protect this with a lock while we are changing
        # the pool size variable, to avoid race conditions. We must keep at least one session in the pool.
        if self._session_pool_size <= 1:
            raise SessionPoolMinSizeReached('Session pool size cannot be decreased further')
        with self._session_pool_lock:
            if self._session_pool_size <= 1:
                log.debug('Session pool size was decreased in another thread')
                return
            log.warning('Server %s: Decreasing session pool size from %s to %s', self.server, self._session_pool_size,
                        self._session_pool_size - 1)
            session = self.get_session()
            self.close_session(session)
            self._session_pool_size -= 1

    def get_session(self):
        # Try to get a session from the queue. If the queue is empty, try to add one more session to the queue. If the
        # queue is already at its max, wait until a session becomes available.
        _timeout = 60  # Rate-limit messages about session starvation
        try:
            session = self._session_pool.get(block=False)
            log.debug('Server %s: Got session immediately', self.server)
        except Empty:
            try:
                self.increase_poolsize()
            except SessionPoolMaxSizeReached:
                pass
            while True:
                try:
                    log.debug('Server %s: Waiting for session', self.server)
                    session = self._session_pool.get(timeout=_timeout)
                    break
                except Empty:
                    # This is normal when we have many worker threads starving for available sessions
                    log.debug('Server %s: No sessions available for %s seconds', self.server, _timeout)
        log.debug('Server %s: Got session %s', self.server, session.session_id)
        session.usage_count += 1
        return session

    def release_session(self, session):
        # This should never fail, as we don't have more sessions than the queue contains
        log.debug('Server %s: Releasing session %s', self.server, session.session_id)
        if self.MAX_SESSION_USAGE_COUNT and session.usage_count > self.MAX_SESSION_USAGE_COUNT:
            log.debug('Server %s: session %s usage exceeded limit. Discarding', self.server, session.session_id)
            session = self.renew_session(session)
        try:
            self._session_pool.put(session, block=False)
        except Full:
            log.debug('Server %s: Session pool was already full %s', self.server, session.session_id)

    @staticmethod
    def close_session(session):
        session.close()
        del session

    def retire_session(self, session):
        # The session is useless. Close it completely and place a fresh session in the pool
        log.debug('Server %s: Retiring session %s', self.server, session.session_id)
        self.close_session(session)
        self.release_session(self.create_session())

    def renew_session(self, session):
        # The session is useless. Close it completely and place a fresh session in the pool
        log.debug('Server %s: Renewing session %s', self.server, session.session_id)
        self.close_session(session)
        return self.create_session()

    def refresh_credentials(self, session):
        # Credentials need to be refreshed, probably due to an OAuth
        # access token expiring. If we've gotten here, it's because the
        # application didn't provide an OAuth client secret, so we can't
        # handle token refreshing for it.
        with self.credentials.lock:
            if self.credentials.sig() == session.credentials_sig:
                # Credentials have not been refreshed by another thread:
                # they're the same as the session was created with. If
                # this isn't the case, we can just go ahead with a new
                # session using the already-updated credentials.
                self.credentials.refresh(session=session)
        return self.renew_session(session)

    def create_session(self):
        if self.auth_type is None:
            raise ValueError('Cannot create session without knowing the auth type')
        if self.credentials is None:
            if self.auth_type in CREDENTIALS_REQUIRED:
                raise ValueError('Auth type %r requires credentials' % self.auth_type)
            session = self.raw_session(self.service_endpoint)
            session.auth = get_auth_instance(auth_type=self.auth_type)
        else:
            with self.credentials.lock:
                if isinstance(self.credentials, OAuth2Credentials):
                    session = self.create_oauth2_session()
                    # Keep track of the credentials used to create this session. If
                    # and when we need to renew credentials (for example, refreshing
                    # an OAuth access token), this lets us easily determine whether
                    # the credentials have already been refreshed in another thread
                    # by the time this session tries.
                    session.credentials_sig = self.credentials.sig()
                else:
                    if self.auth_type == NTLM and self.credentials.type == self.credentials.EMAIL:
                        username = '\\' + self.credentials.username
                    else:
                        username = self.credentials.username
                    session = self.raw_session(self.service_endpoint)
                    session.auth = get_auth_instance(auth_type=self.auth_type, username=username,
                                                     password=self.credentials.password)

        # Add some extra info
        session.session_id = sum(map(ord, str(os.urandom(100))))  # Used for debugging messages in services
        session.usage_count = 0
        session.protocol = self
        log.debug('Server %s: Created session %s', self.server, session.session_id)
        return session

    def create_oauth2_session(self):
        if self.auth_type != OAUTH2:
            raise ValueError(
                'Auth type must be %r for credentials type %s' % (OAUTH2, self.credentials.__class__.__name__)
            )

        has_token = False
        scope = ['https://outlook.office365.com/.default']
        session_params = {}
        token_params = {}

        if isinstance(self.credentials, OAuth2AuthorizationCodeCredentials):
            # Ask for a refresh token
            scope.append('offline_access')

            # We don't know (or need) the Microsoft tenant ID. Use
            # common/ to let Microsoft select the appropriate tenant
            # for the provided authorization code or refresh token.
            #
            # Suppress looks-like-password warning from Bandit.
            token_url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'  # nosec

            client_params = {}
            has_token = self.credentials.access_token is not None
            if has_token:
                session_params['token'] = self.credentials.access_token
            elif self.credentials.authorization_code is not None:
                token_params['code'] = self.credentials.authorization_code
                self.credentials.authorization_code = None

            if self.credentials.client_id is not None and self.credentials.client_secret is not None:
                # If we're given a client ID and secret, we have enough
                # to refresh access tokens ourselves. In other cases the
                # session will raise TokenExpiredError and we'll need to
                # ask the calling application to refresh the token (that
                # covers cases where the caller doesn't have access to
                # the client secret but is working with a service that
                # can provide it refreshed tokens on a limited basis).
                session_params.update({
                    'auto_refresh_kwargs': {
                        'client_id': self.credentials.client_id,
                        'client_secret': self.credentials.client_secret,
                    },
                    'auto_refresh_url': token_url,
                    'token_updater': self.credentials.on_token_auto_refreshed,
                })
            client = WebApplicationClient(self.credentials.client_id, **client_params)
        else:
            token_url = 'https://login.microsoftonline.com/%s/oauth2/v2.0/token' % self.credentials.tenant_id
            client = BackendApplicationClient(client_id=self.credentials.client_id)

        session = self.raw_session(self.service_endpoint, oauth2_client=client, oauth2_session_params=session_params)
        if not has_token:
            # Fetch the token explicitly -- it doesn't occur implicitly
            token = session.fetch_token(token_url=token_url, client_id=self.credentials.client_id,
                                        client_secret=self.credentials.client_secret, scope=scope,
                                        timeout=self.TIMEOUT, **token_params)
            # Allow the credentials object to update its copy of the new
            # token, and give the application an opportunity to cache it
            self.credentials.on_token_auto_refreshed(token)
        session.auth = get_auth_instance(auth_type=OAUTH2, client=client)

        return session

    @classmethod
    def raw_session(cls, prefix, oauth2_client=None, oauth2_session_params=None):
        if oauth2_client:
            session = OAuth2Session(client=oauth2_client, **(oauth2_session_params or {}))
        else:
            session = requests.sessions.Session()
        session.headers.update(DEFAULT_HEADERS)
        session.headers['User-Agent'] = cls.USERAGENT
        session.mount(prefix, adapter=cls.get_adapter())
        return session

    def __repr__(self):
        return self.__class__.__name__ + repr((self.service_endpoint, self.credentials, self.auth_type))


class CachingProtocol(type):
    """A metaclass for Protocol that caches Protocol instances based on a server+username key."""

    _protocol_cache = {}
    _protocol_cache_lock = Lock()

    def __call__(cls, *args, **kwargs):
        # Cache Protocol instances that point to the same endpoint and use the same credentials. This ensures that we
        # re-use thread and connection pools etc. instead of flooding the remote server. This is a modified Singleton
        # pattern.
        #
        # We ignore auth_type from kwargs in the cache key. We trust caller to supply the correct auth_type - otherwise
        # __init__ will guess the correct auth type.

        config = kwargs['config']
        _protocol_cache_key = cls._cache_key(config)

        try:
            protocol, _ = cls._protocol_cache[_protocol_cache_key]
        except KeyError:
            pass
        else:
            if isinstance(protocol, Exception):
                # The input data leads to a TransportError. Re-throw
                raise protocol
            return protocol

        # Acquire lock to guard against multiple threads competing to cache information. Having a per-server lock is
        # probably overkill although it would reduce lock contention.
        log.debug('Waiting for _protocol_cache_lock')
        with cls._protocol_cache_lock:
            try:
                protocol, _ = cls._protocol_cache[_protocol_cache_key]
            except KeyError:
                pass
            else:
                if isinstance(protocol, Exception):
                    # We already tried this combination, possibly in a different competing thread, but the input
                    # data leads to a TransportError.
                    raise protocol
                return protocol

            log.debug("Protocol __call__ cache miss. Adding key '%s'", str(_protocol_cache_key))
            try:
                protocol = super().__call__(*args, **kwargs)
            except TransportError as e:
                # This can happen if, for example, autodiscover supplies us with a bogus EWS endpoint
                log.warning('Failed to create cached protocol with key %s: %s', _protocol_cache_key, e)
                cls._protocol_cache[_protocol_cache_key] = e, datetime.datetime.now()
                raise e
            cls._protocol_cache[_protocol_cache_key] = protocol, datetime.datetime.now()
        return protocol

    @staticmethod
    def _cache_key(config):
        # We may be using multiple different credentials for the same service endpoint. This key combination should be
        # safe.
        return config.service_endpoint, config.credentials

    def __getitem__(cls, config):
        return cls._protocol_cache[cls._cache_key(config)]

    def __delitem__(cls, config):
        del cls._protocol_cache[cls._cache_key(config)]

    @classmethod
    def clear_cache(mcs):
        with mcs._protocol_cache_lock:
            for key, (protocol, _) in mcs._protocol_cache.items():
                if isinstance(protocol, Exception):
                    continue
                service_endpoint = key[0]
                log.debug("Service endpoint '%s': Closing sessions", service_endpoint)
                with protocol._session_pool_lock:
                    protocol.close()
            mcs._protocol_cache.clear()


class Protocol(BaseProtocol, metaclass=CachingProtocol):
    """A class to handle all the low-level communication with an Exchange server. Contains a session pool, knows how to
    negotiate the authentication type of the server, refresh credentials, etc. Also contains methods for calling EWS
    services that are not tied to an account.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._api_version_hint = None
        self._version_lock = Lock()
        # Autodetect authentication type if necessary
        if self.config.auth_type is None:
            self.config.auth_type = self.get_auth_type()

    def get_auth_type(self):
        # Autodetect authentication type. We also set version hint here.
        name = str(self.credentials) if self.credentials and str(self.credentials) else 'DUMMY'
        auth_type, api_version_hint = get_service_authtype(
            service_endpoint=self.service_endpoint, retry_policy=self.retry_policy, api_versions=API_VERSIONS, name=name
        )
        self._api_version_hint = api_version_hint
        return auth_type

    @property
    def version(self):
        # Make sure only one thread does the guessing.
        if not self.config.version or not self.config.version.build:
            with self._version_lock:
                if not self.config.version or not self.config.version.build:
                    # Version.guess() needs auth objects and a working session pool
                    self.config.version = Version.guess(self, api_version_hint=self._api_version_hint)
        return self.config.version

    def get_timezones(self, timezones=None, return_full_timezone_data=False):
        """Get timezone definitions from the server.

        :param timezones: A list of EWSDateTime instances. If None, fetches all timezones from server
          (Default value = None)
        :param return_full_timezone_data: If true, also returns periods and transitions (Default value = False)

        :return: A list of (tz_id, name, periods, transitions) tuples
        """
        return GetServerTimeZones(protocol=self).call(
            timezones=timezones, return_full_timezone_data=return_full_timezone_data
        )

    def get_free_busy_info(self, accounts, start, end, merged_free_busy_interval=30, requested_view='DetailedMerged'):
        """Return free/busy information for a list of accounts.

        :param accounts: A list of (account, attendee_type, exclude_conflicts) tuples, where account is either an
          Account object or a string, attendee_type is a MailboxData.attendee_type choice, and exclude_conflicts is a
          boolean.
        :param start: The start datetime of the request
        :param end: The end datetime of the request
        :param merged_free_busy_interval: The interval, in minutes, of merged free/busy information (Default value = 30)
        :param requested_view: The type of information returned. Possible values are defined in the
          FreeBusyViewOptions.requested_view choices. (Default value = 'DetailedMerged')

        :return: A generator of FreeBusyView objects
        """
        from .account import Account
        for account, attendee_type, exclude_conflicts in accounts:
            if not isinstance(account, (Account, str)):
                raise ValueError("'accounts' item %r must be an 'Account' or 'str' instance" % account)
            if attendee_type not in MailboxData.ATTENDEE_TYPES:
                raise ValueError("'accounts' item %r must be one of %s" % (attendee_type, MailboxData.ATTENDEE_TYPES))
            if not isinstance(exclude_conflicts, bool):
                raise ValueError("'accounts' item %r must be a 'bool' instance" % exclude_conflicts)
        if start >= end:
            raise ValueError("'start' must be less than 'end' (%s -> %s)" % (start, end))
        if not isinstance(merged_free_busy_interval, int):
            raise ValueError("'merged_free_busy_interval' value %r must be an 'int'" % merged_free_busy_interval)
        if requested_view not in FreeBusyViewOptions.REQUESTED_VIEWS:
            raise ValueError(
                "'requested_view' value %r must be one of %s" % (requested_view, FreeBusyViewOptions.REQUESTED_VIEWS))
        _, _, periods, transitions, transitions_groups = list(self.get_timezones(
            timezones=[start.tzinfo],
            return_full_timezone_data=True
        ))[0]
        return GetUserAvailability(self).call(
                timezone=TimeZone.from_server_timezone(
                    periods=periods,
                    transitions=transitions,
                    transitionsgroups=transitions_groups,
                    for_year=start.year
                ),
                mailbox_data=[MailboxData(
                    email=account.primary_smtp_address if isinstance(account, Account) else account,
                    attendee_type=attendee_type,
                    exclude_conflicts=exclude_conflicts
                ) for account, attendee_type, exclude_conflicts in accounts],
                free_busy_view_options=FreeBusyViewOptions(
                    time_window=TimeWindow(start=start, end=end),
                    merged_free_busy_interval=merged_free_busy_interval,
                    requested_view=requested_view,
                ),
        )

    def get_roomlists(self):
        return GetRoomLists(protocol=self).call()

    def get_rooms(self, roomlist):
        return GetRooms(protocol=self).call(roomlist=RoomList(email_address=roomlist))

    def resolve_names(self, names, return_full_contact_data=False, search_scope=None, shape=None):
        """Resolve accounts on the server using partial account data, e.g. an email address or initials.

        :param names: A list of identifiers to query
        :param return_full_contact_data: If True, returns full contact data (Default value = False)
        :param search_scope: The scope to perform the search. Must be one of SEARCH_SCOPE_CHOICES (Default value = None)
        :param shape: (Default value = None)

        :return: A list of Mailbox items or, if return_full_contact_data is True, tuples of (Mailbox, Contact) items
        """
        return list(ResolveNames(protocol=self).call(
            unresolved_entries=names, return_full_contact_data=return_full_contact_data, search_scope=search_scope,
            contact_data_shape=shape,
        ))

    def expand_dl(self, distribution_list):
        """Expand distribution list into it's members.

        :param distribution_list: SMTP address of the distribution list to expand, or a DLMailbox representing the list

        :return: List of Mailbox items that are members of the distribution list
        """
        if isinstance(distribution_list, str):
            distribution_list = DLMailbox(email_address=distribution_list, mailbox_type='PublicDL')
        return list(ExpandDL(protocol=self).call(distribution_list=distribution_list))

    def get_searchable_mailboxes(self, search_filter=None, expand_group_membership=False):
        """Call the GetSearchableMailboxes service to get mailboxes that can be searched.

        This method is only available to users who have been assigned the Discovery Management RBAC role. See
        https://docs.microsoft.com/en-us/exchange/permissions-exo/permissions-exo

        :param search_filter: Is set, must be a single email alias (Default value = None)
        :param expand_group_membership: If True, returned distribution lists are expanded (Default value = False)

        :return: a list of SearchableMailbox, FailedMailbox or Exception instances
        """
        return list(GetSearchableMailboxes(protocol=self).call(
            search_filter=search_filter,
            expand_group_membership=expand_group_membership,
        ))

    def convert_ids(self, ids, destination_format):
        """Convert item and folder IDs between multiple formats.

        :param ids: a list of AlternateId, AlternatePublicFolderId or AlternatePublicFolderItemId instances
        :param destination_format: A string

        :return: a generator of AlternateId, AlternatePublicFolderId or AlternatePublicFolderItemId instances
        """
        return ConvertId(protocol=self).call(items=ids, destination_format=destination_format)

    def __getstate__(self):
        # The lock and thread pool cannot be pickled
        state = super().__getstate__()
        del state['_version_lock']
        try:
            del state['thread_pool']
        except KeyError:
            # thread_pool is a cached property and may not exist
            pass
        return state

    def __setstate__(self, state):
        # Restore the lock. The thread pool is a cached property and will be recreated automatically.
        self.__dict__.update(state)
        self._version_lock = Lock()

    def __str__(self):
        # Don't trigger version guessing here just for the sake of printing
        if self.config.version:
            fullname, api_version, build = self.version.fullname, self.version.api_version, self.version.build
        else:
            fullname, api_version, build = '[unknown]', '[unknown]', '[unknown]'

        return '''\
EWS url: %s
Product name: %s
EWS API version: %s
Build number: %s
EWS auth: %s''' % (self.service_endpoint, fullname, api_version, build, self.auth_type)


class NoVerifyHTTPAdapter(requests.adapters.HTTPAdapter):
    """An HTTP adapter that ignores TLS validation errors. Use at own risk."""

    def cert_verify(self, conn, url, verify, cert):
        # pylint: disable=unused-argument
        # We're overriding a method so we have to keep the signature
        super().cert_verify(conn=conn, url=url, verify=False, cert=cert)


class TLSClientAuth(requests.adapters.HTTPAdapter):
    """An HTTP adapter that implements Certificate Based Authentication (CBA)."""

    cert_file = None

    def init_poolmanager(self, *args, **kwargs):
        kwargs['cert_file'] = self.cert_file
        return super().init_poolmanager(*args, **kwargs)


class RetryPolicy(metaclass=abc.ABCMeta):
    """Stores retry logic used when faced with errors from the server."""

    @property
    @abc.abstractmethod
    def fail_fast(self):
        # Used to choose the error handling policy. When True, a fault-tolerant policy is used. False, a fail-fast
        # policy is used.
        pass

    @property
    @abc.abstractmethod
    def back_off_until(self):
        pass

    @back_off_until.setter
    @abc.abstractmethod
    def back_off_until(self, value):
        pass

    @abc.abstractmethod
    def back_off(self, seconds):
        pass

    @abc.abstractmethod
    def may_retry_on_error(self, response, wait):
        pass

    def raise_response_errors(self, response):
        cas_error = response.headers.get('X-CasErrorCode')
        if cas_error:
            if cas_error.startswith('CAS error:'):
                # Remove unnecessary text
                cas_error = cas_error.split(':', 1)[1].strip()
            raise CASError(cas_error=cas_error, response=response)
        if response.status_code == 500 and (b'The specified server version is invalid' in response.content or
                                            b'ErrorInvalidSchemaVersionForMailboxVersion' in response.content):
            # Another way of communicating invalid schema versions
            raise ErrorInvalidSchemaVersionForMailboxVersion('Invalid server version')
        if b'The referenced account is currently locked out' in response.content:
            raise UnauthorizedError('The referenced account is currently locked out')
        if response.status_code == 401 and self.fail_fast:
            # This is a login failure
            raise UnauthorizedError('Invalid credentials for %s' % response.url)
        if 'TimeoutException' in response.headers:
            # A header set by us on CONNECTION_ERRORS
            raise response.headers['TimeoutException']
        # This could be anything. Let higher layers handle this
        raise MalformedResponseError(
            'Unknown failure in response. Code: %s headers: %s content: %s'
            % (response.status_code, response.headers, response.text)
        )


class FailFast(RetryPolicy):
    """Fail immediately on server errors."""

    @property
    def fail_fast(self):
        return True

    @property
    def back_off_until(self):
        return None

    def back_off(self, seconds):
        raise ValueError('Cannot back off with fail-fast policy')

    def may_retry_on_error(self, response, wait):
        log.debug('No retry: no fail-fast policy')
        return False


class FaultTolerance(RetryPolicy):
    """Enables fault-tolerant error handling. Tells internal methods to do an exponential back off when requests start
    failing, and wait up to max_wait seconds before failing.
    """

    # Back off 60 seconds if we didn't get an explicit suggested value
    DEFAULT_BACKOFF = 60

    def __init__(self, max_wait=3600):
        self.max_wait = max_wait
        self._back_off_until = None
        self._back_off_lock = Lock()

    def __getstate__(self):
        # Locks cannot be pickled
        state = self.__dict__.copy()
        del state['_back_off_lock']
        return state

    def __setstate__(self, state):
        # Restore the lock
        self.__dict__.update(state)
        self._back_off_lock = Lock()

    @property
    def fail_fast(self):
        return False

    @property
    def back_off_until(self):
        """Return the back off value as a datetime. Reset the current back off value if it has expired."""
        if self._back_off_until is None:
            return None
        with self._back_off_lock:
            if self._back_off_until is None:
                return None
            if self._back_off_until < datetime.datetime.now():
                self._back_off_until = None  # The back off value has expired. Reset
                return None
            return self._back_off_until

    @back_off_until.setter
    def back_off_until(self, value):
        with self._back_off_lock:
            self._back_off_until = value

    def back_off(self, seconds):
        if seconds is None:
            seconds = self.DEFAULT_BACKOFF
        value = datetime.datetime.now() + datetime.timedelta(seconds=seconds)
        with self._back_off_lock:
            self._back_off_until = value

    def may_retry_on_error(self, response, wait):
        if response.status_code not in (301, 302, 401, 500, 503):
            # Don't retry if we didn't get a status code that we can hope to recover from
            log.debug('No retry: wrong status code %s', response.status_code)
            return False
        if wait > self.max_wait:
            # We lost patience. Session is cleaned up in outer loop
            raise RateLimitError(
                'Max timeout reached', url=response.url, status_code=response.status_code, total_wait=wait)
        if response.status_code == 401:
            # EWS sometimes throws 401's when it wants us to throttle connections. OK to retry.
            return True
        if response.headers.get('connection') == 'close':
            # Connection closed. OK to retry.
            return True
        if response.status_code == 302 and response.headers.get('location', '').lower() \
                == '/ews/genericerrorpage.htm?aspxerrorpath=/ews/exchange.asmx':
            # The genericerrorpage.htm/internalerror.asp is ridiculous behaviour for random outages. OK to retry.
            #
            # Redirect to '/internalsite/internalerror.asp' or '/internalsite/initparams.aspx' is caused by e.g. TLS
            # certificate f*ckups on the Exchange server. We should not retry those.
            return True
        if response.status_code == 503:
            # Internal server error. OK to retry.
            return True
        if response.status_code == 500 and b"Server Error in '/EWS' Application" in response.content:
            # "Server Error in '/EWS' Application" has been seen in highly concurrent settings. OK to retry.
            log.debug('Retry allowed: conditions met')
            return True
        return False

Functions

def close_connections()
Expand source code
def close_connections():
    CachingProtocol.clear_cache()

Classes

class BaseProtocol (config)

Base class for Protocol which implements the bare essentials.

Expand source code
class BaseProtocol:
    """Base class for Protocol which implements the bare essentials."""

    # The maximum number of sessions (== TCP connections, see below) we will open to this service endpoint. Keep this
    # low unless you have an agreement with the Exchange admin on the receiving end to hammer the server and
    # rate-limiting policies have been disabled for the connecting user. Changing this setting only makes sense if
    # you are using a thread pool to run multiple concurrent workers in this process.
    SESSION_POOLSIZE = 1
    # We want only 1 TCP connection per Session object. We may have lots of different credentials hitting the server and
    # each credential needs its own session (NTLM auth will only send credentials once and then secure the connection,
    # so a connection can only handle requests for one credential). Having multiple connections per Session could
    # quickly exhaust the maximum number of concurrent connections the Exchange server allows from one client.
    CONNECTIONS_PER_SESSION = 1
    # The number of times a session may be reused before creating a new session object. 'None' means "infinite".
    # Discarding sessions after a certain number of usages may limit memory leaks in the Session object.
    MAX_SESSION_USAGE_COUNT = None
    # Timeout for HTTP requests
    TIMEOUT = 120

    # The adapter class to use for HTTP requests. Override this if you need e.g. proxy support or specific TLS versions
    HTTP_ADAPTER_CLS = requests.adapters.HTTPAdapter

    # The User-Agent header to use for HTTP requests. Override this to set an app-specific one
    USERAGENT = None

    def __init__(self, config):
        from .configuration import Configuration
        if not isinstance(config, Configuration):
            raise ValueError("'config' %r must be a Configuration instance" % config)
        if not config.service_endpoint:
            raise AttributeError("'config.service_endpoint' must be set")
        self.config = config
        self._session_pool_size = 0
        self._session_pool_maxsize = config.max_connections or self.SESSION_POOLSIZE

        # Try to behave nicely with the remote server. We want to keep the connection open between requests.
        # We also want to re-use sessions, to avoid the NTLM auth handshake on every request. We must know the
        # authentication method to create sessions.
        self._session_pool = LifoQueue()
        self._session_pool_lock = Lock()

    @property
    def service_endpoint(self):
        return self.config.service_endpoint

    @property
    def auth_type(self):
        return self.config.auth_type

    @property
    def credentials(self):
        return self.config.credentials

    @credentials.setter
    def credentials(self, value):
        # We are updating credentials, but that doesn't automatically propagate to the session objects. The simplest
        # solution is to just kill the sessions in the pool.
        with self._session_pool_lock:
            self.config._credentials = value
            self.close()

    @property
    def retry_policy(self):
        return self.config.retry_policy

    @property
    def server(self):
        return self.config.server

    def __getstate__(self):
        # The session pool and lock cannot be pickled
        state = self.__dict__.copy()
        del state['_session_pool']
        del state['_session_pool_lock']
        return state

    def __setstate__(self, state):
        # Restore the session pool and lock
        self.__dict__.update(state)
        self._session_pool = LifoQueue()
        self._session_pool_lock = Lock()

    def __del__(self):
        # pylint: disable=bare-except
        try:
            self.close()
        except Exception:  # nosec
            # __del__ should never fail
            pass

    def close(self):
        log.debug('Server %s: Closing sessions', self.server)
        while True:
            try:
                session = self._session_pool.get(block=False)
                self.close_session(session)
                self._session_pool_size -= 1
            except Empty:
                break

    @classmethod
    def get_adapter(cls):
        # We want just one connection per session. No retries, since we wrap all requests in our own retry handler
        return cls.HTTP_ADAPTER_CLS(
            pool_block=True,
            pool_connections=cls.CONNECTIONS_PER_SESSION,
            pool_maxsize=cls.CONNECTIONS_PER_SESSION,
            max_retries=0,
        )

    @property
    def session_pool_size(self):
        return self._session_pool_size

    def increase_poolsize(self):
        """Increases the session pool size. We increase by one session per call."""
        # Create a single session and insert it into the pool. We need to protect this with a lock while we are changing
        # the pool size variable, to avoid race conditions. We must not exceed the pool size limit.
        if self._session_pool_size == self._session_pool_maxsize:
            raise SessionPoolMaxSizeReached('Session pool size cannot be increased further')
        with self._session_pool_lock:
            if self._session_pool_size >= self._session_pool_maxsize:
                log.debug('Session pool size was increased in another thread')
                return
            log.debug('Server %s: Increasing session pool size from %s to %s', self.server, self._session_pool_size,
                      self._session_pool_size + 1)
            self._session_pool.put(self.create_session(), block=False)
            self._session_pool_size += 1

    def decrease_poolsize(self):
        """Decreases the session pool size in response to error messages from the server requesting to rate-limit
        requests. We decrease by one session per call.
        """
        # Take a single session from the pool and discard it. We need to protect this with a lock while we are changing
        # the pool size variable, to avoid race conditions. We must keep at least one session in the pool.
        if self._session_pool_size <= 1:
            raise SessionPoolMinSizeReached('Session pool size cannot be decreased further')
        with self._session_pool_lock:
            if self._session_pool_size <= 1:
                log.debug('Session pool size was decreased in another thread')
                return
            log.warning('Server %s: Decreasing session pool size from %s to %s', self.server, self._session_pool_size,
                        self._session_pool_size - 1)
            session = self.get_session()
            self.close_session(session)
            self._session_pool_size -= 1

    def get_session(self):
        # Try to get a session from the queue. If the queue is empty, try to add one more session to the queue. If the
        # queue is already at its max, wait until a session becomes available.
        _timeout = 60  # Rate-limit messages about session starvation
        try:
            session = self._session_pool.get(block=False)
            log.debug('Server %s: Got session immediately', self.server)
        except Empty:
            try:
                self.increase_poolsize()
            except SessionPoolMaxSizeReached:
                pass
            while True:
                try:
                    log.debug('Server %s: Waiting for session', self.server)
                    session = self._session_pool.get(timeout=_timeout)
                    break
                except Empty:
                    # This is normal when we have many worker threads starving for available sessions
                    log.debug('Server %s: No sessions available for %s seconds', self.server, _timeout)
        log.debug('Server %s: Got session %s', self.server, session.session_id)
        session.usage_count += 1
        return session

    def release_session(self, session):
        # This should never fail, as we don't have more sessions than the queue contains
        log.debug('Server %s: Releasing session %s', self.server, session.session_id)
        if self.MAX_SESSION_USAGE_COUNT and session.usage_count > self.MAX_SESSION_USAGE_COUNT:
            log.debug('Server %s: session %s usage exceeded limit. Discarding', self.server, session.session_id)
            session = self.renew_session(session)
        try:
            self._session_pool.put(session, block=False)
        except Full:
            log.debug('Server %s: Session pool was already full %s', self.server, session.session_id)

    @staticmethod
    def close_session(session):
        session.close()
        del session

    def retire_session(self, session):
        # The session is useless. Close it completely and place a fresh session in the pool
        log.debug('Server %s: Retiring session %s', self.server, session.session_id)
        self.close_session(session)
        self.release_session(self.create_session())

    def renew_session(self, session):
        # The session is useless. Close it completely and place a fresh session in the pool
        log.debug('Server %s: Renewing session %s', self.server, session.session_id)
        self.close_session(session)
        return self.create_session()

    def refresh_credentials(self, session):
        # Credentials need to be refreshed, probably due to an OAuth
        # access token expiring. If we've gotten here, it's because the
        # application didn't provide an OAuth client secret, so we can't
        # handle token refreshing for it.
        with self.credentials.lock:
            if self.credentials.sig() == session.credentials_sig:
                # Credentials have not been refreshed by another thread:
                # they're the same as the session was created with. If
                # this isn't the case, we can just go ahead with a new
                # session using the already-updated credentials.
                self.credentials.refresh(session=session)
        return self.renew_session(session)

    def create_session(self):
        if self.auth_type is None:
            raise ValueError('Cannot create session without knowing the auth type')
        if self.credentials is None:
            if self.auth_type in CREDENTIALS_REQUIRED:
                raise ValueError('Auth type %r requires credentials' % self.auth_type)
            session = self.raw_session(self.service_endpoint)
            session.auth = get_auth_instance(auth_type=self.auth_type)
        else:
            with self.credentials.lock:
                if isinstance(self.credentials, OAuth2Credentials):
                    session = self.create_oauth2_session()
                    # Keep track of the credentials used to create this session. If
                    # and when we need to renew credentials (for example, refreshing
                    # an OAuth access token), this lets us easily determine whether
                    # the credentials have already been refreshed in another thread
                    # by the time this session tries.
                    session.credentials_sig = self.credentials.sig()
                else:
                    if self.auth_type == NTLM and self.credentials.type == self.credentials.EMAIL:
                        username = '\\' + self.credentials.username
                    else:
                        username = self.credentials.username
                    session = self.raw_session(self.service_endpoint)
                    session.auth = get_auth_instance(auth_type=self.auth_type, username=username,
                                                     password=self.credentials.password)

        # Add some extra info
        session.session_id = sum(map(ord, str(os.urandom(100))))  # Used for debugging messages in services
        session.usage_count = 0
        session.protocol = self
        log.debug('Server %s: Created session %s', self.server, session.session_id)
        return session

    def create_oauth2_session(self):
        if self.auth_type != OAUTH2:
            raise ValueError(
                'Auth type must be %r for credentials type %s' % (OAUTH2, self.credentials.__class__.__name__)
            )

        has_token = False
        scope = ['https://outlook.office365.com/.default']
        session_params = {}
        token_params = {}

        if isinstance(self.credentials, OAuth2AuthorizationCodeCredentials):
            # Ask for a refresh token
            scope.append('offline_access')

            # We don't know (or need) the Microsoft tenant ID. Use
            # common/ to let Microsoft select the appropriate tenant
            # for the provided authorization code or refresh token.
            #
            # Suppress looks-like-password warning from Bandit.
            token_url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'  # nosec

            client_params = {}
            has_token = self.credentials.access_token is not None
            if has_token:
                session_params['token'] = self.credentials.access_token
            elif self.credentials.authorization_code is not None:
                token_params['code'] = self.credentials.authorization_code
                self.credentials.authorization_code = None

            if self.credentials.client_id is not None and self.credentials.client_secret is not None:
                # If we're given a client ID and secret, we have enough
                # to refresh access tokens ourselves. In other cases the
                # session will raise TokenExpiredError and we'll need to
                # ask the calling application to refresh the token (that
                # covers cases where the caller doesn't have access to
                # the client secret but is working with a service that
                # can provide it refreshed tokens on a limited basis).
                session_params.update({
                    'auto_refresh_kwargs': {
                        'client_id': self.credentials.client_id,
                        'client_secret': self.credentials.client_secret,
                    },
                    'auto_refresh_url': token_url,
                    'token_updater': self.credentials.on_token_auto_refreshed,
                })
            client = WebApplicationClient(self.credentials.client_id, **client_params)
        else:
            token_url = 'https://login.microsoftonline.com/%s/oauth2/v2.0/token' % self.credentials.tenant_id
            client = BackendApplicationClient(client_id=self.credentials.client_id)

        session = self.raw_session(self.service_endpoint, oauth2_client=client, oauth2_session_params=session_params)
        if not has_token:
            # Fetch the token explicitly -- it doesn't occur implicitly
            token = session.fetch_token(token_url=token_url, client_id=self.credentials.client_id,
                                        client_secret=self.credentials.client_secret, scope=scope,
                                        timeout=self.TIMEOUT, **token_params)
            # Allow the credentials object to update its copy of the new
            # token, and give the application an opportunity to cache it
            self.credentials.on_token_auto_refreshed(token)
        session.auth = get_auth_instance(auth_type=OAUTH2, client=client)

        return session

    @classmethod
    def raw_session(cls, prefix, oauth2_client=None, oauth2_session_params=None):
        if oauth2_client:
            session = OAuth2Session(client=oauth2_client, **(oauth2_session_params or {}))
        else:
            session = requests.sessions.Session()
        session.headers.update(DEFAULT_HEADERS)
        session.headers['User-Agent'] = cls.USERAGENT
        session.mount(prefix, adapter=cls.get_adapter())
        return session

    def __repr__(self):
        return self.__class__.__name__ + repr((self.service_endpoint, self.credentials, self.auth_type))

Subclasses

Class variables

var CONNECTIONS_PER_SESSION
var HTTP_ADAPTER_CLS

The built-in HTTP Adapter for urllib3.

Provides a general-case interface for Requests sessions to contact HTTP and HTTPS urls by implementing the Transport Adapter interface. This class will usually be created by the :class:Session <Session> class under the covers.

:param pool_connections: The number of urllib3 connection pools to cache. :param pool_maxsize: The maximum number of connections to save in the pool. :param max_retries: The maximum number of retries each connection should attempt. Note, this applies only to failed DNS lookups, socket connections and connection timeouts, never to requests where data has made it to the server. By default, Requests does not retry failed connections. If you need granular control over the conditions under which we retry a request, import urllib3's Retry class and pass that instead. :param pool_block: Whether the connection pool should block for connections.

Usage::

import requests s = requests.Session() a = requests.adapters.HTTPAdapter(max_retries=3) s.mount('http://', a)

var MAX_SESSION_USAGE_COUNT
var SESSION_POOLSIZE
var TIMEOUT
var USERAGENT

Static methods

def close_session(session)
Expand source code
@staticmethod
def close_session(session):
    session.close()
    del session
def get_adapter()
Expand source code
@classmethod
def get_adapter(cls):
    # We want just one connection per session. No retries, since we wrap all requests in our own retry handler
    return cls.HTTP_ADAPTER_CLS(
        pool_block=True,
        pool_connections=cls.CONNECTIONS_PER_SESSION,
        pool_maxsize=cls.CONNECTIONS_PER_SESSION,
        max_retries=0,
    )
def raw_session(prefix, oauth2_client=None, oauth2_session_params=None)
Expand source code
@classmethod
def raw_session(cls, prefix, oauth2_client=None, oauth2_session_params=None):
    if oauth2_client:
        session = OAuth2Session(client=oauth2_client, **(oauth2_session_params or {}))
    else:
        session = requests.sessions.Session()
    session.headers.update(DEFAULT_HEADERS)
    session.headers['User-Agent'] = cls.USERAGENT
    session.mount(prefix, adapter=cls.get_adapter())
    return session

Instance variables

var auth_type
Expand source code
@property
def auth_type(self):
    return self.config.auth_type
var credentials
Expand source code
@property
def credentials(self):
    return self.config.credentials
var retry_policy
Expand source code
@property
def retry_policy(self):
    return self.config.retry_policy
var server
Expand source code
@property
def server(self):
    return self.config.server
var service_endpoint
Expand source code
@property
def service_endpoint(self):
    return self.config.service_endpoint
var session_pool_size
Expand source code
@property
def session_pool_size(self):
    return self._session_pool_size

Methods

def close(self)
Expand source code
def close(self):
    log.debug('Server %s: Closing sessions', self.server)
    while True:
        try:
            session = self._session_pool.get(block=False)
            self.close_session(session)
            self._session_pool_size -= 1
        except Empty:
            break
def create_oauth2_session(self)
Expand source code
def create_oauth2_session(self):
    if self.auth_type != OAUTH2:
        raise ValueError(
            'Auth type must be %r for credentials type %s' % (OAUTH2, self.credentials.__class__.__name__)
        )

    has_token = False
    scope = ['https://outlook.office365.com/.default']
    session_params = {}
    token_params = {}

    if isinstance(self.credentials, OAuth2AuthorizationCodeCredentials):
        # Ask for a refresh token
        scope.append('offline_access')

        # We don't know (or need) the Microsoft tenant ID. Use
        # common/ to let Microsoft select the appropriate tenant
        # for the provided authorization code or refresh token.
        #
        # Suppress looks-like-password warning from Bandit.
        token_url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'  # nosec

        client_params = {}
        has_token = self.credentials.access_token is not None
        if has_token:
            session_params['token'] = self.credentials.access_token
        elif self.credentials.authorization_code is not None:
            token_params['code'] = self.credentials.authorization_code
            self.credentials.authorization_code = None

        if self.credentials.client_id is not None and self.credentials.client_secret is not None:
            # If we're given a client ID and secret, we have enough
            # to refresh access tokens ourselves. In other cases the
            # session will raise TokenExpiredError and we'll need to
            # ask the calling application to refresh the token (that
            # covers cases where the caller doesn't have access to
            # the client secret but is working with a service that
            # can provide it refreshed tokens on a limited basis).
            session_params.update({
                'auto_refresh_kwargs': {
                    'client_id': self.credentials.client_id,
                    'client_secret': self.credentials.client_secret,
                },
                'auto_refresh_url': token_url,
                'token_updater': self.credentials.on_token_auto_refreshed,
            })
        client = WebApplicationClient(self.credentials.client_id, **client_params)
    else:
        token_url = 'https://login.microsoftonline.com/%s/oauth2/v2.0/token' % self.credentials.tenant_id
        client = BackendApplicationClient(client_id=self.credentials.client_id)

    session = self.raw_session(self.service_endpoint, oauth2_client=client, oauth2_session_params=session_params)
    if not has_token:
        # Fetch the token explicitly -- it doesn't occur implicitly
        token = session.fetch_token(token_url=token_url, client_id=self.credentials.client_id,
                                    client_secret=self.credentials.client_secret, scope=scope,
                                    timeout=self.TIMEOUT, **token_params)
        # Allow the credentials object to update its copy of the new
        # token, and give the application an opportunity to cache it
        self.credentials.on_token_auto_refreshed(token)
    session.auth = get_auth_instance(auth_type=OAUTH2, client=client)

    return session
def create_session(self)
Expand source code
def create_session(self):
    if self.auth_type is None:
        raise ValueError('Cannot create session without knowing the auth type')
    if self.credentials is None:
        if self.auth_type in CREDENTIALS_REQUIRED:
            raise ValueError('Auth type %r requires credentials' % self.auth_type)
        session = self.raw_session(self.service_endpoint)
        session.auth = get_auth_instance(auth_type=self.auth_type)
    else:
        with self.credentials.lock:
            if isinstance(self.credentials, OAuth2Credentials):
                session = self.create_oauth2_session()
                # Keep track of the credentials used to create this session. If
                # and when we need to renew credentials (for example, refreshing
                # an OAuth access token), this lets us easily determine whether
                # the credentials have already been refreshed in another thread
                # by the time this session tries.
                session.credentials_sig = self.credentials.sig()
            else:
                if self.auth_type == NTLM and self.credentials.type == self.credentials.EMAIL:
                    username = '\\' + self.credentials.username
                else:
                    username = self.credentials.username
                session = self.raw_session(self.service_endpoint)
                session.auth = get_auth_instance(auth_type=self.auth_type, username=username,
                                                 password=self.credentials.password)

    # Add some extra info
    session.session_id = sum(map(ord, str(os.urandom(100))))  # Used for debugging messages in services
    session.usage_count = 0
    session.protocol = self
    log.debug('Server %s: Created session %s', self.server, session.session_id)
    return session
def decrease_poolsize(self)

Decreases the session pool size in response to error messages from the server requesting to rate-limit requests. We decrease by one session per call.

Expand source code
def decrease_poolsize(self):
    """Decreases the session pool size in response to error messages from the server requesting to rate-limit
    requests. We decrease by one session per call.
    """
    # Take a single session from the pool and discard it. We need to protect this with a lock while we are changing
    # the pool size variable, to avoid race conditions. We must keep at least one session in the pool.
    if self._session_pool_size <= 1:
        raise SessionPoolMinSizeReached('Session pool size cannot be decreased further')
    with self._session_pool_lock:
        if self._session_pool_size <= 1:
            log.debug('Session pool size was decreased in another thread')
            return
        log.warning('Server %s: Decreasing session pool size from %s to %s', self.server, self._session_pool_size,
                    self._session_pool_size - 1)
        session = self.get_session()
        self.close_session(session)
        self._session_pool_size -= 1
def get_session(self)
Expand source code
def get_session(self):
    # Try to get a session from the queue. If the queue is empty, try to add one more session to the queue. If the
    # queue is already at its max, wait until a session becomes available.
    _timeout = 60  # Rate-limit messages about session starvation
    try:
        session = self._session_pool.get(block=False)
        log.debug('Server %s: Got session immediately', self.server)
    except Empty:
        try:
            self.increase_poolsize()
        except SessionPoolMaxSizeReached:
            pass
        while True:
            try:
                log.debug('Server %s: Waiting for session', self.server)
                session = self._session_pool.get(timeout=_timeout)
                break
            except Empty:
                # This is normal when we have many worker threads starving for available sessions
                log.debug('Server %s: No sessions available for %s seconds', self.server, _timeout)
    log.debug('Server %s: Got session %s', self.server, session.session_id)
    session.usage_count += 1
    return session
def increase_poolsize(self)

Increases the session pool size. We increase by one session per call.

Expand source code
def increase_poolsize(self):
    """Increases the session pool size. We increase by one session per call."""
    # Create a single session and insert it into the pool. We need to protect this with a lock while we are changing
    # the pool size variable, to avoid race conditions. We must not exceed the pool size limit.
    if self._session_pool_size == self._session_pool_maxsize:
        raise SessionPoolMaxSizeReached('Session pool size cannot be increased further')
    with self._session_pool_lock:
        if self._session_pool_size >= self._session_pool_maxsize:
            log.debug('Session pool size was increased in another thread')
            return
        log.debug('Server %s: Increasing session pool size from %s to %s', self.server, self._session_pool_size,
                  self._session_pool_size + 1)
        self._session_pool.put(self.create_session(), block=False)
        self._session_pool_size += 1
def refresh_credentials(self, session)
Expand source code
def refresh_credentials(self, session):
    # Credentials need to be refreshed, probably due to an OAuth
    # access token expiring. If we've gotten here, it's because the
    # application didn't provide an OAuth client secret, so we can't
    # handle token refreshing for it.
    with self.credentials.lock:
        if self.credentials.sig() == session.credentials_sig:
            # Credentials have not been refreshed by another thread:
            # they're the same as the session was created with. If
            # this isn't the case, we can just go ahead with a new
            # session using the already-updated credentials.
            self.credentials.refresh(session=session)
    return self.renew_session(session)
def release_session(self, session)
Expand source code
def release_session(self, session):
    # This should never fail, as we don't have more sessions than the queue contains
    log.debug('Server %s: Releasing session %s', self.server, session.session_id)
    if self.MAX_SESSION_USAGE_COUNT and session.usage_count > self.MAX_SESSION_USAGE_COUNT:
        log.debug('Server %s: session %s usage exceeded limit. Discarding', self.server, session.session_id)
        session = self.renew_session(session)
    try:
        self._session_pool.put(session, block=False)
    except Full:
        log.debug('Server %s: Session pool was already full %s', self.server, session.session_id)
def renew_session(self, session)
Expand source code
def renew_session(self, session):
    # The session is useless. Close it completely and place a fresh session in the pool
    log.debug('Server %s: Renewing session %s', self.server, session.session_id)
    self.close_session(session)
    return self.create_session()
def retire_session(self, session)
Expand source code
def retire_session(self, session):
    # The session is useless. Close it completely and place a fresh session in the pool
    log.debug('Server %s: Retiring session %s', self.server, session.session_id)
    self.close_session(session)
    self.release_session(self.create_session())
class CachingProtocol (*args, **kwargs)

A metaclass for Protocol that caches Protocol instances based on a server+username key.

Expand source code
class CachingProtocol(type):
    """A metaclass for Protocol that caches Protocol instances based on a server+username key."""

    _protocol_cache = {}
    _protocol_cache_lock = Lock()

    def __call__(cls, *args, **kwargs):
        # Cache Protocol instances that point to the same endpoint and use the same credentials. This ensures that we
        # re-use thread and connection pools etc. instead of flooding the remote server. This is a modified Singleton
        # pattern.
        #
        # We ignore auth_type from kwargs in the cache key. We trust caller to supply the correct auth_type - otherwise
        # __init__ will guess the correct auth type.

        config = kwargs['config']
        _protocol_cache_key = cls._cache_key(config)

        try:
            protocol, _ = cls._protocol_cache[_protocol_cache_key]
        except KeyError:
            pass
        else:
            if isinstance(protocol, Exception):
                # The input data leads to a TransportError. Re-throw
                raise protocol
            return protocol

        # Acquire lock to guard against multiple threads competing to cache information. Having a per-server lock is
        # probably overkill although it would reduce lock contention.
        log.debug('Waiting for _protocol_cache_lock')
        with cls._protocol_cache_lock:
            try:
                protocol, _ = cls._protocol_cache[_protocol_cache_key]
            except KeyError:
                pass
            else:
                if isinstance(protocol, Exception):
                    # We already tried this combination, possibly in a different competing thread, but the input
                    # data leads to a TransportError.
                    raise protocol
                return protocol

            log.debug("Protocol __call__ cache miss. Adding key '%s'", str(_protocol_cache_key))
            try:
                protocol = super().__call__(*args, **kwargs)
            except TransportError as e:
                # This can happen if, for example, autodiscover supplies us with a bogus EWS endpoint
                log.warning('Failed to create cached protocol with key %s: %s', _protocol_cache_key, e)
                cls._protocol_cache[_protocol_cache_key] = e, datetime.datetime.now()
                raise e
            cls._protocol_cache[_protocol_cache_key] = protocol, datetime.datetime.now()
        return protocol

    @staticmethod
    def _cache_key(config):
        # We may be using multiple different credentials for the same service endpoint. This key combination should be
        # safe.
        return config.service_endpoint, config.credentials

    def __getitem__(cls, config):
        return cls._protocol_cache[cls._cache_key(config)]

    def __delitem__(cls, config):
        del cls._protocol_cache[cls._cache_key(config)]

    @classmethod
    def clear_cache(mcs):
        with mcs._protocol_cache_lock:
            for key, (protocol, _) in mcs._protocol_cache.items():
                if isinstance(protocol, Exception):
                    continue
                service_endpoint = key[0]
                log.debug("Service endpoint '%s': Closing sessions", service_endpoint)
                with protocol._session_pool_lock:
                    protocol.close()
            mcs._protocol_cache.clear()

Ancestors

  • builtins.type

Static methods

def clear_cache()
Expand source code
@classmethod
def clear_cache(mcs):
    with mcs._protocol_cache_lock:
        for key, (protocol, _) in mcs._protocol_cache.items():
            if isinstance(protocol, Exception):
                continue
            service_endpoint = key[0]
            log.debug("Service endpoint '%s': Closing sessions", service_endpoint)
            with protocol._session_pool_lock:
                protocol.close()
        mcs._protocol_cache.clear()
class FailFast

Fail immediately on server errors.

Expand source code
class FailFast(RetryPolicy):
    """Fail immediately on server errors."""

    @property
    def fail_fast(self):
        return True

    @property
    def back_off_until(self):
        return None

    def back_off(self, seconds):
        raise ValueError('Cannot back off with fail-fast policy')

    def may_retry_on_error(self, response, wait):
        log.debug('No retry: no fail-fast policy')
        return False

Ancestors

Instance variables

var back_off_until
Expand source code
@property
def back_off_until(self):
    return None
var fail_fast
Expand source code
@property
def fail_fast(self):
    return True

Methods

def back_off(self, seconds)
Expand source code
def back_off(self, seconds):
    raise ValueError('Cannot back off with fail-fast policy')
def may_retry_on_error(self, response, wait)
Expand source code
def may_retry_on_error(self, response, wait):
    log.debug('No retry: no fail-fast policy')
    return False
class FaultTolerance (max_wait=3600)

Enables fault-tolerant error handling. Tells internal methods to do an exponential back off when requests start failing, and wait up to max_wait seconds before failing.

Expand source code
class FaultTolerance(RetryPolicy):
    """Enables fault-tolerant error handling. Tells internal methods to do an exponential back off when requests start
    failing, and wait up to max_wait seconds before failing.
    """

    # Back off 60 seconds if we didn't get an explicit suggested value
    DEFAULT_BACKOFF = 60

    def __init__(self, max_wait=3600):
        self.max_wait = max_wait
        self._back_off_until = None
        self._back_off_lock = Lock()

    def __getstate__(self):
        # Locks cannot be pickled
        state = self.__dict__.copy()
        del state['_back_off_lock']
        return state

    def __setstate__(self, state):
        # Restore the lock
        self.__dict__.update(state)
        self._back_off_lock = Lock()

    @property
    def fail_fast(self):
        return False

    @property
    def back_off_until(self):
        """Return the back off value as a datetime. Reset the current back off value if it has expired."""
        if self._back_off_until is None:
            return None
        with self._back_off_lock:
            if self._back_off_until is None:
                return None
            if self._back_off_until < datetime.datetime.now():
                self._back_off_until = None  # The back off value has expired. Reset
                return None
            return self._back_off_until

    @back_off_until.setter
    def back_off_until(self, value):
        with self._back_off_lock:
            self._back_off_until = value

    def back_off(self, seconds):
        if seconds is None:
            seconds = self.DEFAULT_BACKOFF
        value = datetime.datetime.now() + datetime.timedelta(seconds=seconds)
        with self._back_off_lock:
            self._back_off_until = value

    def may_retry_on_error(self, response, wait):
        if response.status_code not in (301, 302, 401, 500, 503):
            # Don't retry if we didn't get a status code that we can hope to recover from
            log.debug('No retry: wrong status code %s', response.status_code)
            return False
        if wait > self.max_wait:
            # We lost patience. Session is cleaned up in outer loop
            raise RateLimitError(
                'Max timeout reached', url=response.url, status_code=response.status_code, total_wait=wait)
        if response.status_code == 401:
            # EWS sometimes throws 401's when it wants us to throttle connections. OK to retry.
            return True
        if response.headers.get('connection') == 'close':
            # Connection closed. OK to retry.
            return True
        if response.status_code == 302 and response.headers.get('location', '').lower() \
                == '/ews/genericerrorpage.htm?aspxerrorpath=/ews/exchange.asmx':
            # The genericerrorpage.htm/internalerror.asp is ridiculous behaviour for random outages. OK to retry.
            #
            # Redirect to '/internalsite/internalerror.asp' or '/internalsite/initparams.aspx' is caused by e.g. TLS
            # certificate f*ckups on the Exchange server. We should not retry those.
            return True
        if response.status_code == 503:
            # Internal server error. OK to retry.
            return True
        if response.status_code == 500 and b"Server Error in '/EWS' Application" in response.content:
            # "Server Error in '/EWS' Application" has been seen in highly concurrent settings. OK to retry.
            log.debug('Retry allowed: conditions met')
            return True
        return False

Ancestors

Class variables

var DEFAULT_BACKOFF

Instance variables

var back_off_until

Return the back off value as a datetime. Reset the current back off value if it has expired.

Expand source code
@property
def back_off_until(self):
    """Return the back off value as a datetime. Reset the current back off value if it has expired."""
    if self._back_off_until is None:
        return None
    with self._back_off_lock:
        if self._back_off_until is None:
            return None
        if self._back_off_until < datetime.datetime.now():
            self._back_off_until = None  # The back off value has expired. Reset
            return None
        return self._back_off_until
var fail_fast
Expand source code
@property
def fail_fast(self):
    return False

Methods

def back_off(self, seconds)
Expand source code
def back_off(self, seconds):
    if seconds is None:
        seconds = self.DEFAULT_BACKOFF
    value = datetime.datetime.now() + datetime.timedelta(seconds=seconds)
    with self._back_off_lock:
        self._back_off_until = value
def may_retry_on_error(self, response, wait)
Expand source code
def may_retry_on_error(self, response, wait):
    if response.status_code not in (301, 302, 401, 500, 503):
        # Don't retry if we didn't get a status code that we can hope to recover from
        log.debug('No retry: wrong status code %s', response.status_code)
        return False
    if wait > self.max_wait:
        # We lost patience. Session is cleaned up in outer loop
        raise RateLimitError(
            'Max timeout reached', url=response.url, status_code=response.status_code, total_wait=wait)
    if response.status_code == 401:
        # EWS sometimes throws 401's when it wants us to throttle connections. OK to retry.
        return True
    if response.headers.get('connection') == 'close':
        # Connection closed. OK to retry.
        return True
    if response.status_code == 302 and response.headers.get('location', '').lower() \
            == '/ews/genericerrorpage.htm?aspxerrorpath=/ews/exchange.asmx':
        # The genericerrorpage.htm/internalerror.asp is ridiculous behaviour for random outages. OK to retry.
        #
        # Redirect to '/internalsite/internalerror.asp' or '/internalsite/initparams.aspx' is caused by e.g. TLS
        # certificate f*ckups on the Exchange server. We should not retry those.
        return True
    if response.status_code == 503:
        # Internal server error. OK to retry.
        return True
    if response.status_code == 500 and b"Server Error in '/EWS' Application" in response.content:
        # "Server Error in '/EWS' Application" has been seen in highly concurrent settings. OK to retry.
        log.debug('Retry allowed: conditions met')
        return True
    return False
class NoVerifyHTTPAdapter (pool_connections=10, pool_maxsize=10, max_retries=0, pool_block=False)

An HTTP adapter that ignores TLS validation errors. Use at own risk.

Expand source code
class NoVerifyHTTPAdapter(requests.adapters.HTTPAdapter):
    """An HTTP adapter that ignores TLS validation errors. Use at own risk."""

    def cert_verify(self, conn, url, verify, cert):
        # pylint: disable=unused-argument
        # We're overriding a method so we have to keep the signature
        super().cert_verify(conn=conn, url=url, verify=False, cert=cert)

Ancestors

  • requests.adapters.HTTPAdapter
  • requests.adapters.BaseAdapter

Methods

def cert_verify(self, conn, url, verify, cert)

Verify a SSL certificate. This method should not be called from user code, and is only exposed for use when subclassing the :class:HTTPAdapter <requests.adapters.HTTPAdapter>.

:param conn: The urllib3 connection object associated with the cert. :param url: The requested URL. :param verify: Either a boolean, in which case it controls whether we verify the server's TLS certificate, or a string, in which case it must be a path to a CA bundle to use :param cert: The SSL certificate to verify.

Expand source code
def cert_verify(self, conn, url, verify, cert):
    # pylint: disable=unused-argument
    # We're overriding a method so we have to keep the signature
    super().cert_verify(conn=conn, url=url, verify=False, cert=cert)
class Protocol (*args, **kwargs)

A class to handle all the low-level communication with an Exchange server. Contains a session pool, knows how to negotiate the authentication type of the server, refresh credentials, etc. Also contains methods for calling EWS services that are not tied to an account.

Expand source code
class Protocol(BaseProtocol, metaclass=CachingProtocol):
    """A class to handle all the low-level communication with an Exchange server. Contains a session pool, knows how to
    negotiate the authentication type of the server, refresh credentials, etc. Also contains methods for calling EWS
    services that are not tied to an account.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._api_version_hint = None
        self._version_lock = Lock()
        # Autodetect authentication type if necessary
        if self.config.auth_type is None:
            self.config.auth_type = self.get_auth_type()

    def get_auth_type(self):
        # Autodetect authentication type. We also set version hint here.
        name = str(self.credentials) if self.credentials and str(self.credentials) else 'DUMMY'
        auth_type, api_version_hint = get_service_authtype(
            service_endpoint=self.service_endpoint, retry_policy=self.retry_policy, api_versions=API_VERSIONS, name=name
        )
        self._api_version_hint = api_version_hint
        return auth_type

    @property
    def version(self):
        # Make sure only one thread does the guessing.
        if not self.config.version or not self.config.version.build:
            with self._version_lock:
                if not self.config.version or not self.config.version.build:
                    # Version.guess() needs auth objects and a working session pool
                    self.config.version = Version.guess(self, api_version_hint=self._api_version_hint)
        return self.config.version

    def get_timezones(self, timezones=None, return_full_timezone_data=False):
        """Get timezone definitions from the server.

        :param timezones: A list of EWSDateTime instances. If None, fetches all timezones from server
          (Default value = None)
        :param return_full_timezone_data: If true, also returns periods and transitions (Default value = False)

        :return: A list of (tz_id, name, periods, transitions) tuples
        """
        return GetServerTimeZones(protocol=self).call(
            timezones=timezones, return_full_timezone_data=return_full_timezone_data
        )

    def get_free_busy_info(self, accounts, start, end, merged_free_busy_interval=30, requested_view='DetailedMerged'):
        """Return free/busy information for a list of accounts.

        :param accounts: A list of (account, attendee_type, exclude_conflicts) tuples, where account is either an
          Account object or a string, attendee_type is a MailboxData.attendee_type choice, and exclude_conflicts is a
          boolean.
        :param start: The start datetime of the request
        :param end: The end datetime of the request
        :param merged_free_busy_interval: The interval, in minutes, of merged free/busy information (Default value = 30)
        :param requested_view: The type of information returned. Possible values are defined in the
          FreeBusyViewOptions.requested_view choices. (Default value = 'DetailedMerged')

        :return: A generator of FreeBusyView objects
        """
        from .account import Account
        for account, attendee_type, exclude_conflicts in accounts:
            if not isinstance(account, (Account, str)):
                raise ValueError("'accounts' item %r must be an 'Account' or 'str' instance" % account)
            if attendee_type not in MailboxData.ATTENDEE_TYPES:
                raise ValueError("'accounts' item %r must be one of %s" % (attendee_type, MailboxData.ATTENDEE_TYPES))
            if not isinstance(exclude_conflicts, bool):
                raise ValueError("'accounts' item %r must be a 'bool' instance" % exclude_conflicts)
        if start >= end:
            raise ValueError("'start' must be less than 'end' (%s -> %s)" % (start, end))
        if not isinstance(merged_free_busy_interval, int):
            raise ValueError("'merged_free_busy_interval' value %r must be an 'int'" % merged_free_busy_interval)
        if requested_view not in FreeBusyViewOptions.REQUESTED_VIEWS:
            raise ValueError(
                "'requested_view' value %r must be one of %s" % (requested_view, FreeBusyViewOptions.REQUESTED_VIEWS))
        _, _, periods, transitions, transitions_groups = list(self.get_timezones(
            timezones=[start.tzinfo],
            return_full_timezone_data=True
        ))[0]
        return GetUserAvailability(self).call(
                timezone=TimeZone.from_server_timezone(
                    periods=periods,
                    transitions=transitions,
                    transitionsgroups=transitions_groups,
                    for_year=start.year
                ),
                mailbox_data=[MailboxData(
                    email=account.primary_smtp_address if isinstance(account, Account) else account,
                    attendee_type=attendee_type,
                    exclude_conflicts=exclude_conflicts
                ) for account, attendee_type, exclude_conflicts in accounts],
                free_busy_view_options=FreeBusyViewOptions(
                    time_window=TimeWindow(start=start, end=end),
                    merged_free_busy_interval=merged_free_busy_interval,
                    requested_view=requested_view,
                ),
        )

    def get_roomlists(self):
        return GetRoomLists(protocol=self).call()

    def get_rooms(self, roomlist):
        return GetRooms(protocol=self).call(roomlist=RoomList(email_address=roomlist))

    def resolve_names(self, names, return_full_contact_data=False, search_scope=None, shape=None):
        """Resolve accounts on the server using partial account data, e.g. an email address or initials.

        :param names: A list of identifiers to query
        :param return_full_contact_data: If True, returns full contact data (Default value = False)
        :param search_scope: The scope to perform the search. Must be one of SEARCH_SCOPE_CHOICES (Default value = None)
        :param shape: (Default value = None)

        :return: A list of Mailbox items or, if return_full_contact_data is True, tuples of (Mailbox, Contact) items
        """
        return list(ResolveNames(protocol=self).call(
            unresolved_entries=names, return_full_contact_data=return_full_contact_data, search_scope=search_scope,
            contact_data_shape=shape,
        ))

    def expand_dl(self, distribution_list):
        """Expand distribution list into it's members.

        :param distribution_list: SMTP address of the distribution list to expand, or a DLMailbox representing the list

        :return: List of Mailbox items that are members of the distribution list
        """
        if isinstance(distribution_list, str):
            distribution_list = DLMailbox(email_address=distribution_list, mailbox_type='PublicDL')
        return list(ExpandDL(protocol=self).call(distribution_list=distribution_list))

    def get_searchable_mailboxes(self, search_filter=None, expand_group_membership=False):
        """Call the GetSearchableMailboxes service to get mailboxes that can be searched.

        This method is only available to users who have been assigned the Discovery Management RBAC role. See
        https://docs.microsoft.com/en-us/exchange/permissions-exo/permissions-exo

        :param search_filter: Is set, must be a single email alias (Default value = None)
        :param expand_group_membership: If True, returned distribution lists are expanded (Default value = False)

        :return: a list of SearchableMailbox, FailedMailbox or Exception instances
        """
        return list(GetSearchableMailboxes(protocol=self).call(
            search_filter=search_filter,
            expand_group_membership=expand_group_membership,
        ))

    def convert_ids(self, ids, destination_format):
        """Convert item and folder IDs between multiple formats.

        :param ids: a list of AlternateId, AlternatePublicFolderId or AlternatePublicFolderItemId instances
        :param destination_format: A string

        :return: a generator of AlternateId, AlternatePublicFolderId or AlternatePublicFolderItemId instances
        """
        return ConvertId(protocol=self).call(items=ids, destination_format=destination_format)

    def __getstate__(self):
        # The lock and thread pool cannot be pickled
        state = super().__getstate__()
        del state['_version_lock']
        try:
            del state['thread_pool']
        except KeyError:
            # thread_pool is a cached property and may not exist
            pass
        return state

    def __setstate__(self, state):
        # Restore the lock. The thread pool is a cached property and will be recreated automatically.
        self.__dict__.update(state)
        self._version_lock = Lock()

    def __str__(self):
        # Don't trigger version guessing here just for the sake of printing
        if self.config.version:
            fullname, api_version, build = self.version.fullname, self.version.api_version, self.version.build
        else:
            fullname, api_version, build = '[unknown]', '[unknown]', '[unknown]'

        return '''\
EWS url: %s
Product name: %s
EWS API version: %s
Build number: %s
EWS auth: %s''' % (self.service_endpoint, fullname, api_version, build, self.auth_type)

Ancestors

Instance variables

var version
Expand source code
@property
def version(self):
    # Make sure only one thread does the guessing.
    if not self.config.version or not self.config.version.build:
        with self._version_lock:
            if not self.config.version or not self.config.version.build:
                # Version.guess() needs auth objects and a working session pool
                self.config.version = Version.guess(self, api_version_hint=self._api_version_hint)
    return self.config.version

Methods

def convert_ids(self, ids, destination_format)

Convert item and folder IDs between multiple formats.

:param ids: a list of AlternateId, AlternatePublicFolderId or AlternatePublicFolderItemId instances :param destination_format: A string

:return: a generator of AlternateId, AlternatePublicFolderId or AlternatePublicFolderItemId instances

Expand source code
def convert_ids(self, ids, destination_format):
    """Convert item and folder IDs between multiple formats.

    :param ids: a list of AlternateId, AlternatePublicFolderId or AlternatePublicFolderItemId instances
    :param destination_format: A string

    :return: a generator of AlternateId, AlternatePublicFolderId or AlternatePublicFolderItemId instances
    """
    return ConvertId(protocol=self).call(items=ids, destination_format=destination_format)
def expand_dl(self, distribution_list)

Expand distribution list into it's members.

:param distribution_list: SMTP address of the distribution list to expand, or a DLMailbox representing the list

:return: List of Mailbox items that are members of the distribution list

Expand source code
def expand_dl(self, distribution_list):
    """Expand distribution list into it's members.

    :param distribution_list: SMTP address of the distribution list to expand, or a DLMailbox representing the list

    :return: List of Mailbox items that are members of the distribution list
    """
    if isinstance(distribution_list, str):
        distribution_list = DLMailbox(email_address=distribution_list, mailbox_type='PublicDL')
    return list(ExpandDL(protocol=self).call(distribution_list=distribution_list))
def get_auth_type(self)
Expand source code
def get_auth_type(self):
    # Autodetect authentication type. We also set version hint here.
    name = str(self.credentials) if self.credentials and str(self.credentials) else 'DUMMY'
    auth_type, api_version_hint = get_service_authtype(
        service_endpoint=self.service_endpoint, retry_policy=self.retry_policy, api_versions=API_VERSIONS, name=name
    )
    self._api_version_hint = api_version_hint
    return auth_type
def get_free_busy_info(self, accounts, start, end, merged_free_busy_interval=30, requested_view='DetailedMerged')

Return free/busy information for a list of accounts.

:param accounts: A list of (account, attendee_type, exclude_conflicts) tuples, where account is either an Account object or a string, attendee_type is a MailboxData.attendee_type choice, and exclude_conflicts is a boolean. :param start: The start datetime of the request :param end: The end datetime of the request :param merged_free_busy_interval: The interval, in minutes, of merged free/busy information (Default value = 30) :param requested_view: The type of information returned. Possible values are defined in the FreeBusyViewOptions.requested_view choices. (Default value = 'DetailedMerged')

:return: A generator of FreeBusyView objects

Expand source code
def get_free_busy_info(self, accounts, start, end, merged_free_busy_interval=30, requested_view='DetailedMerged'):
    """Return free/busy information for a list of accounts.

    :param accounts: A list of (account, attendee_type, exclude_conflicts) tuples, where account is either an
      Account object or a string, attendee_type is a MailboxData.attendee_type choice, and exclude_conflicts is a
      boolean.
    :param start: The start datetime of the request
    :param end: The end datetime of the request
    :param merged_free_busy_interval: The interval, in minutes, of merged free/busy information (Default value = 30)
    :param requested_view: The type of information returned. Possible values are defined in the
      FreeBusyViewOptions.requested_view choices. (Default value = 'DetailedMerged')

    :return: A generator of FreeBusyView objects
    """
    from .account import Account
    for account, attendee_type, exclude_conflicts in accounts:
        if not isinstance(account, (Account, str)):
            raise ValueError("'accounts' item %r must be an 'Account' or 'str' instance" % account)
        if attendee_type not in MailboxData.ATTENDEE_TYPES:
            raise ValueError("'accounts' item %r must be one of %s" % (attendee_type, MailboxData.ATTENDEE_TYPES))
        if not isinstance(exclude_conflicts, bool):
            raise ValueError("'accounts' item %r must be a 'bool' instance" % exclude_conflicts)
    if start >= end:
        raise ValueError("'start' must be less than 'end' (%s -> %s)" % (start, end))
    if not isinstance(merged_free_busy_interval, int):
        raise ValueError("'merged_free_busy_interval' value %r must be an 'int'" % merged_free_busy_interval)
    if requested_view not in FreeBusyViewOptions.REQUESTED_VIEWS:
        raise ValueError(
            "'requested_view' value %r must be one of %s" % (requested_view, FreeBusyViewOptions.REQUESTED_VIEWS))
    _, _, periods, transitions, transitions_groups = list(self.get_timezones(
        timezones=[start.tzinfo],
        return_full_timezone_data=True
    ))[0]
    return GetUserAvailability(self).call(
            timezone=TimeZone.from_server_timezone(
                periods=periods,
                transitions=transitions,
                transitionsgroups=transitions_groups,
                for_year=start.year
            ),
            mailbox_data=[MailboxData(
                email=account.primary_smtp_address if isinstance(account, Account) else account,
                attendee_type=attendee_type,
                exclude_conflicts=exclude_conflicts
            ) for account, attendee_type, exclude_conflicts in accounts],
            free_busy_view_options=FreeBusyViewOptions(
                time_window=TimeWindow(start=start, end=end),
                merged_free_busy_interval=merged_free_busy_interval,
                requested_view=requested_view,
            ),
    )
def get_roomlists(self)
Expand source code
def get_roomlists(self):
    return GetRoomLists(protocol=self).call()
def get_rooms(self, roomlist)
Expand source code
def get_rooms(self, roomlist):
    return GetRooms(protocol=self).call(roomlist=RoomList(email_address=roomlist))
def get_searchable_mailboxes(self, search_filter=None, expand_group_membership=False)

Call the GetSearchableMailboxes service to get mailboxes that can be searched.

This method is only available to users who have been assigned the Discovery Management RBAC role. See https://docs.microsoft.com/en-us/exchange/permissions-exo/permissions-exo

:param search_filter: Is set, must be a single email alias (Default value = None) :param expand_group_membership: If True, returned distribution lists are expanded (Default value = False)

:return: a list of SearchableMailbox, FailedMailbox or Exception instances

Expand source code
def get_searchable_mailboxes(self, search_filter=None, expand_group_membership=False):
    """Call the GetSearchableMailboxes service to get mailboxes that can be searched.

    This method is only available to users who have been assigned the Discovery Management RBAC role. See
    https://docs.microsoft.com/en-us/exchange/permissions-exo/permissions-exo

    :param search_filter: Is set, must be a single email alias (Default value = None)
    :param expand_group_membership: If True, returned distribution lists are expanded (Default value = False)

    :return: a list of SearchableMailbox, FailedMailbox or Exception instances
    """
    return list(GetSearchableMailboxes(protocol=self).call(
        search_filter=search_filter,
        expand_group_membership=expand_group_membership,
    ))
def get_timezones(self, timezones=None, return_full_timezone_data=False)

Get timezone definitions from the server.

:param timezones: A list of EWSDateTime instances. If None, fetches all timezones from server (Default value = None) :param return_full_timezone_data: If true, also returns periods and transitions (Default value = False)

:return: A list of (tz_id, name, periods, transitions) tuples

Expand source code
def get_timezones(self, timezones=None, return_full_timezone_data=False):
    """Get timezone definitions from the server.

    :param timezones: A list of EWSDateTime instances. If None, fetches all timezones from server
      (Default value = None)
    :param return_full_timezone_data: If true, also returns periods and transitions (Default value = False)

    :return: A list of (tz_id, name, periods, transitions) tuples
    """
    return GetServerTimeZones(protocol=self).call(
        timezones=timezones, return_full_timezone_data=return_full_timezone_data
    )
def resolve_names(self, names, return_full_contact_data=False, search_scope=None, shape=None)

Resolve accounts on the server using partial account data, e.g. an email address or initials.

:param names: A list of identifiers to query :param return_full_contact_data: If True, returns full contact data (Default value = False) :param search_scope: The scope to perform the search. Must be one of SEARCH_SCOPE_CHOICES (Default value = None) :param shape: (Default value = None)

:return: A list of Mailbox items or, if return_full_contact_data is True, tuples of (Mailbox, Contact) items

Expand source code
def resolve_names(self, names, return_full_contact_data=False, search_scope=None, shape=None):
    """Resolve accounts on the server using partial account data, e.g. an email address or initials.

    :param names: A list of identifiers to query
    :param return_full_contact_data: If True, returns full contact data (Default value = False)
    :param search_scope: The scope to perform the search. Must be one of SEARCH_SCOPE_CHOICES (Default value = None)
    :param shape: (Default value = None)

    :return: A list of Mailbox items or, if return_full_contact_data is True, tuples of (Mailbox, Contact) items
    """
    return list(ResolveNames(protocol=self).call(
        unresolved_entries=names, return_full_contact_data=return_full_contact_data, search_scope=search_scope,
        contact_data_shape=shape,
    ))

Inherited members

class RetryPolicy

Stores retry logic used when faced with errors from the server.

Expand source code
class RetryPolicy(metaclass=abc.ABCMeta):
    """Stores retry logic used when faced with errors from the server."""

    @property
    @abc.abstractmethod
    def fail_fast(self):
        # Used to choose the error handling policy. When True, a fault-tolerant policy is used. False, a fail-fast
        # policy is used.
        pass

    @property
    @abc.abstractmethod
    def back_off_until(self):
        pass

    @back_off_until.setter
    @abc.abstractmethod
    def back_off_until(self, value):
        pass

    @abc.abstractmethod
    def back_off(self, seconds):
        pass

    @abc.abstractmethod
    def may_retry_on_error(self, response, wait):
        pass

    def raise_response_errors(self, response):
        cas_error = response.headers.get('X-CasErrorCode')
        if cas_error:
            if cas_error.startswith('CAS error:'):
                # Remove unnecessary text
                cas_error = cas_error.split(':', 1)[1].strip()
            raise CASError(cas_error=cas_error, response=response)
        if response.status_code == 500 and (b'The specified server version is invalid' in response.content or
                                            b'ErrorInvalidSchemaVersionForMailboxVersion' in response.content):
            # Another way of communicating invalid schema versions
            raise ErrorInvalidSchemaVersionForMailboxVersion('Invalid server version')
        if b'The referenced account is currently locked out' in response.content:
            raise UnauthorizedError('The referenced account is currently locked out')
        if response.status_code == 401 and self.fail_fast:
            # This is a login failure
            raise UnauthorizedError('Invalid credentials for %s' % response.url)
        if 'TimeoutException' in response.headers:
            # A header set by us on CONNECTION_ERRORS
            raise response.headers['TimeoutException']
        # This could be anything. Let higher layers handle this
        raise MalformedResponseError(
            'Unknown failure in response. Code: %s headers: %s content: %s'
            % (response.status_code, response.headers, response.text)
        )

Subclasses

Instance variables

var back_off_until
Expand source code
@property
@abc.abstractmethod
def back_off_until(self):
    pass
var fail_fast
Expand source code
@property
@abc.abstractmethod
def fail_fast(self):
    # Used to choose the error handling policy. When True, a fault-tolerant policy is used. False, a fail-fast
    # policy is used.
    pass

Methods

def back_off(self, seconds)
Expand source code
@abc.abstractmethod
def back_off(self, seconds):
    pass
def may_retry_on_error(self, response, wait)
Expand source code
@abc.abstractmethod
def may_retry_on_error(self, response, wait):
    pass
def raise_response_errors(self, response)
Expand source code
def raise_response_errors(self, response):
    cas_error = response.headers.get('X-CasErrorCode')
    if cas_error:
        if cas_error.startswith('CAS error:'):
            # Remove unnecessary text
            cas_error = cas_error.split(':', 1)[1].strip()
        raise CASError(cas_error=cas_error, response=response)
    if response.status_code == 500 and (b'The specified server version is invalid' in response.content or
                                        b'ErrorInvalidSchemaVersionForMailboxVersion' in response.content):
        # Another way of communicating invalid schema versions
        raise ErrorInvalidSchemaVersionForMailboxVersion('Invalid server version')
    if b'The referenced account is currently locked out' in response.content:
        raise UnauthorizedError('The referenced account is currently locked out')
    if response.status_code == 401 and self.fail_fast:
        # This is a login failure
        raise UnauthorizedError('Invalid credentials for %s' % response.url)
    if 'TimeoutException' in response.headers:
        # A header set by us on CONNECTION_ERRORS
        raise response.headers['TimeoutException']
    # This could be anything. Let higher layers handle this
    raise MalformedResponseError(
        'Unknown failure in response. Code: %s headers: %s content: %s'
        % (response.status_code, response.headers, response.text)
    )
class TLSClientAuth (pool_connections=10, pool_maxsize=10, max_retries=0, pool_block=False)

An HTTP adapter that implements Certificate Based Authentication (CBA).

Expand source code
class TLSClientAuth(requests.adapters.HTTPAdapter):
    """An HTTP adapter that implements Certificate Based Authentication (CBA)."""

    cert_file = None

    def init_poolmanager(self, *args, **kwargs):
        kwargs['cert_file'] = self.cert_file
        return super().init_poolmanager(*args, **kwargs)

Ancestors

  • requests.adapters.HTTPAdapter
  • requests.adapters.BaseAdapter

Class variables

var cert_file

Methods

def init_poolmanager(self, *args, **kwargs)

Initializes a urllib3 PoolManager.

This method should not be called from user code, and is only exposed for use when subclassing the :class:HTTPAdapter <requests.adapters.HTTPAdapter>.

:param connections: The number of urllib3 connection pools to cache. :param maxsize: The maximum number of connections to save in the pool. :param block: Block when no free connections are available. :param pool_kwargs: Extra keyword arguments used to initialize the Pool Manager.

Expand source code
def init_poolmanager(self, *args, **kwargs):
    kwargs['cert_file'] = self.cert_file
    return super().init_poolmanager(*args, **kwargs)
exchangelib-4.6.1/docs/exchangelib/queryset.html000066400000000000000000002662571414601472700217730ustar00rootroot00000000000000 exchangelib.queryset API documentation

Module exchangelib.queryset

Expand source code
import abc
import logging
import warnings
from copy import deepcopy
from itertools import islice

from .errors import MultipleObjectsReturned, DoesNotExist
from .fields import FieldPath, FieldOrder
from .items import CalendarItem, ID_ONLY
from .properties import InvalidField
from .restriction import Q
from .services import CHUNK_SIZE
from .version import EXCHANGE_2010

log = logging.getLogger(__name__)


class SearchableMixIn:
    """Implement a search API for inheritance."""

    @abc.abstractmethod
    def get(self, *args, **kwargs):
        pass

    @abc.abstractmethod
    def all(self):
        pass

    @abc.abstractmethod
    def none(self):
        pass

    @abc.abstractmethod
    def filter(self, *args, **kwargs):
        pass

    @abc.abstractmethod
    def exclude(self, *args, **kwargs):
        pass

    @abc.abstractmethod
    def people(self):
        pass


class QuerySet(SearchableMixIn):
    """A Django QuerySet-like class for querying items. Defers queries until the QuerySet is consumed. Supports chaining to
    build up complex queries.

    Django QuerySet documentation: https://docs.djangoproject.com/en/dev/ref/models/querysets/
    """

    VALUES = 'values'
    VALUES_LIST = 'values_list'
    FLAT = 'flat'
    NONE = 'none'
    RETURN_TYPES = (VALUES, VALUES_LIST, FLAT, NONE)

    ITEM = 'item'
    PERSONA = 'persona'
    REQUEST_TYPES = (ITEM, PERSONA)

    def __init__(self, folder_collection, request_type=ITEM):
        from .folders import FolderCollection
        if not isinstance(folder_collection, FolderCollection):
            raise ValueError("folder_collection value '%s' must be a FolderCollection instance" % folder_collection)
        self.folder_collection = folder_collection  # A FolderCollection instance
        if request_type not in self.REQUEST_TYPES:
            raise ValueError("'request_type' %r must be one of %s" % (request_type, self.REQUEST_TYPES))
        self.request_type = request_type
        self.q = Q()  # Default to no restrictions
        self.only_fields = None
        self.order_fields = None
        self.return_format = self.NONE
        self.calendar_view = None
        self.page_size = None
        self.max_items = None
        self.offset = 0
        self._depth = None

    def _copy_self(self):
        # When we copy a queryset where the cache has already been filled, we don't copy the cache. Thus, a copied
        # queryset will fetch results from the server again.
        #
        # All other behaviour would be awkward:
        #
        # qs = QuerySet(f).filter(foo='bar')
        # items = list(qs)
        # new_qs = qs.exclude(bar='baz')  # This should work, and should fetch from the server
        #
        if not isinstance(self.q, Q):
            raise ValueError("self.q value '%s' must be None or a Q instance" % self.q)
        if not isinstance(self.only_fields, (type(None), tuple)):
            raise ValueError("self.only_fields value '%s' must be None or a tuple" % self.only_fields)
        if not isinstance(self.order_fields, (type(None), tuple)):
            raise ValueError("self.order_fields value '%s' must be None or a tuple" % self.order_fields)
        if self.return_format not in self.RETURN_TYPES:
            raise ValueError("self.return_value '%s' must be one of %s" % (self.return_format, self.RETURN_TYPES))
        # Only mutable objects need to be deepcopied. Folder should be the same object
        new_qs = self.__class__(self.folder_collection, request_type=self.request_type)
        new_qs.q = deepcopy(self.q)
        new_qs.only_fields = self.only_fields
        new_qs.order_fields = None if self.order_fields is None else deepcopy(self.order_fields)
        new_qs.return_format = self.return_format
        new_qs.calendar_view = self.calendar_view
        new_qs.page_size = self.page_size
        new_qs.max_items = self.max_items
        new_qs.offset = self.offset
        new_qs._depth = self._depth
        return new_qs

    def _get_field_path(self, field_path):
        from .items import Persona
        if self.request_type == self.PERSONA:
            return FieldPath(field=Persona.get_field_by_fieldname(field_path))
        for folder in self.folder_collection:
            try:
                return FieldPath.from_string(field_path=field_path, folder=folder)
            except InvalidField:
                pass
        raise InvalidField("Unknown field path %r on folders %s" % (field_path, self.folder_collection.folders))

    def _get_field_order(self, field_path):
        from .items import Persona
        if self.request_type == self.PERSONA:
            return FieldOrder(
                field_path=FieldPath(field=Persona.get_field_by_fieldname(field_path.lstrip('-'))),
                reverse=field_path.startswith('-'),
            )
        for folder in self.folder_collection:
            try:
                return FieldOrder.from_string(field_path=field_path, folder=folder)
            except InvalidField:
                pass
        raise InvalidField("Unknown field path %r on folders %s" % (field_path, self.folder_collection.folders))

    @property
    def _id_field(self):
        return self._get_field_path('id')

    @property
    def _changekey_field(self):
        return self._get_field_path('changekey')

    def _additional_fields(self):
        if not isinstance(self.only_fields, tuple):
            raise ValueError("'only_fields' value %r must be a tuple" % self.only_fields)
        # Remove ItemId and ChangeKey. We get them unconditionally
        additional_fields = {f for f in self.only_fields if not f.field.is_attribute}
        if self.request_type != self.ITEM:
            return additional_fields

        # For CalendarItem items, we want to inject internal timezone fields into the requested fields.
        has_start = 'start' in {f.field.name for f in additional_fields}
        has_end = 'end' in {f.field.name for f in additional_fields}
        meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
        if self.folder_collection.account.version.build < EXCHANGE_2010:
            if has_start or has_end:
                additional_fields.add(FieldPath(field=meeting_tz_field))
        else:
            if has_start:
                additional_fields.add(FieldPath(field=start_tz_field))
            if has_end:
                additional_fields.add(FieldPath(field=end_tz_field))
        return additional_fields

    def _format_items(self, items, return_format):
        return {
            self.VALUES: self._as_values,
            self.VALUES_LIST: self._as_values_list,
            self.FLAT: self._as_flat_values_list,
            self.NONE: self._as_items,
        }[return_format](items)

    def _query(self):
        if self.only_fields is None:
            # We didn't restrict list of field paths. Get all fields from the server, including extended properties.
            if self.request_type == self.PERSONA:
                additional_fields = {}  # GetPersona doesn't take explicit fields. Don't bother calculating the list
                complex_fields_requested = True
            else:
                additional_fields = {FieldPath(field=f) for f in self.folder_collection.allowed_item_fields()}
                complex_fields_requested = True
        else:
            additional_fields = self._additional_fields()
            complex_fields_requested = any(f.field.is_complex for f in additional_fields)

        # EWS can do server-side sorting on multiple fields. A caveat is that server-side sorting is not supported
        # for calendar views. In this case, we do all the sorting client-side.
        if self.calendar_view:
            must_sort_clientside = bool(self.order_fields)
            order_fields = None
        else:
            must_sort_clientside = False
            order_fields = self.order_fields

        if must_sort_clientside:
            # Also fetch order_by fields that we only need for client-side sorting.
            extra_order_fields = {f.field_path for f in self.order_fields} - additional_fields
            if extra_order_fields:
                additional_fields.update(extra_order_fields)
        else:
            extra_order_fields = set()

        find_kwargs = dict(
            shape=ID_ONLY,  # Always use IdOnly here, because AllProperties doesn't actually get *all* properties
            depth=self._depth,
            additional_fields=additional_fields,
            order_fields=order_fields,
            page_size=self.page_size,
            max_items=self.max_items,
            offset=self.offset,
        )
        if self.request_type == self.PERSONA:
            if complex_fields_requested:
                find_kwargs['additional_fields'] = None
                items = self.folder_collection.account.fetch_personas(
                    ids=self.folder_collection.find_people(self.q, **find_kwargs)
                )
            else:
                if not additional_fields:
                    find_kwargs['additional_fields'] = None
                items = self.folder_collection.find_people(self.q, **find_kwargs)
        else:
            find_kwargs['calendar_view'] = self.calendar_view
            if complex_fields_requested:
                # The FindItem service does not support complex field types. Tell find_items() to return
                # (id, changekey) tuples, and pass that to fetch().
                find_kwargs['additional_fields'] = None
                items = self.folder_collection.account.fetch(
                    ids=self.folder_collection.find_items(self.q, **find_kwargs),
                    only_fields=additional_fields,
                    chunk_size=self.page_size,
                )
            else:
                if not additional_fields:
                    # If additional_fields is the empty set, we only requested ID and changekey fields. We can then
                    # take a shortcut by using (shape=ID_ONLY, additional_fields=None) to tell find_items() to return
                    # (id, changekey) tuples. We'll post-process those later.
                    find_kwargs['additional_fields'] = None
                items = self.folder_collection.find_items(self.q, **find_kwargs)

        if not must_sort_clientside:
            return items

        # Resort to client-side sorting of the order_by fields. This is greedy. Sorting in Python is stable, so when
        # sorting on multiple fields, we can just do a sort on each of the requested fields in reverse order. Reverse
        # each sort operation if the field was marked as such.
        for f in reversed(self.order_fields):
            try:
                items = sorted(items, key=lambda i: _get_sort_value_or_default(i, f), reverse=f.reverse)
            except TypeError as e:
                if 'unorderable types' not in e.args[0]:
                    raise
                raise ValueError((
                    "Cannot sort on field '%s'. The field has no default value defined, and there are either items "
                    "with None values for this field, or the query contains exception instances (original error: %s).")
                                 % (f.field_path, e))
        if not extra_order_fields:
            return items

        # Nullify the fields we only needed for sorting before returning
        return (_rinse_item(i, extra_order_fields) for i in items)

    def __iter__(self):
        # Fill cache if this is the first iteration. Return an iterator over the results. Make this non-greedy by
        # filling the cache while we are iterating.
        #
        if self.q.is_never():
            return

        log.debug('Initializing cache')
        yield from self._format_items(items=self._query(), return_format=self.return_format)

    """Do not implement __len__. The implementation of list() tries to preallocate memory by calling __len__ on the
    given sequence, before calling __iter__. If we implemented __len__, we would end up calling FindItems twice, once
    to get the result of self.count(), an once to return the actual result.

    Also, according to https://stackoverflow.com/questions/37189968/how-to-have-list-consume-iter-without-calling-len,
    a __len__ implementation should be cheap. That does not hold for self.count().

    def __len__(self):
        # This queryset has no cache yet. Call the optimized counting implementation
        return self.count()
    """

    def __getitem__(self, idx_or_slice):
        # Support indexing and slicing. This is non-greedy when possible (slicing start, stop and step are not negative,
        # and we're ordering on at most one field), and will only fill the cache if the entire query is iterated.
        if isinstance(idx_or_slice, int):
            return self._getitem_idx(idx_or_slice)
        return self._getitem_slice(idx_or_slice)

    def _getitem_idx(self, idx):
        if idx < 0:
            # Support negative indexes by reversing the queryset and negating the index value
            reverse_idx = -(idx+1)
            return self.reverse()[reverse_idx]
        # Optimize by setting an exact offset and fetching only 1 item
        new_qs = self._copy_self()
        new_qs.max_items = 1
        new_qs.page_size = 1
        new_qs.offset = idx
        # The iterator will return at most 1 item
        for item in new_qs.__iter__():
            return item
        raise IndexError()

    def _getitem_slice(self, s):
        if ((s.start or 0) < 0) or ((s.stop or 0) < 0) or ((s.step or 0) < 0):
            # islice() does not support negative start, stop and step. Make sure cache is full by iterating the full
            # query result, and then slice on the cache.
            return list(self.__iter__())[s]
        # Optimize by setting an exact offset and max_items value
        new_qs = self._copy_self()
        if s.start is not None and s.stop is not None:
            new_qs.offset = s.start
            new_qs.max_items = s.stop - s.start
        elif s.start is not None:
            new_qs.offset = s.start
        elif s.stop is not None:
            new_qs.max_items = s.stop
        if new_qs.page_size is None and new_qs.max_items is not None and new_qs.max_items < CHUNK_SIZE:
            new_qs.page_size = new_qs.max_items
        return islice(new_qs.__iter__(), None, None, s.step)

    def _item_yielder(self, iterable, item_func, id_only_func, changekey_only_func, id_and_changekey_func):
        # Transforms results from the server according to the given transform functions. Makes sure to pass on
        # Exception instances unaltered.
        if self.only_fields:
            has_non_attribute_fields = bool({f for f in self.only_fields if not f.field.is_attribute})
        else:
            has_non_attribute_fields = True
        if not has_non_attribute_fields:
            # _query() will return an iterator of (id, changekey) tuples
            if self._changekey_field not in self.only_fields:
                transform_func = id_only_func
            elif self._id_field not in self.only_fields:
                transform_func = changekey_only_func
            else:
                transform_func = id_and_changekey_func
            for i in iterable:
                if isinstance(i, Exception):
                    yield i
                    continue
                yield transform_func(*i)
            return
        for i in iterable:
            if isinstance(i, Exception):
                yield i
                continue
            yield item_func(i)

    def _as_items(self, iterable):
        from .items import Item
        return self._item_yielder(
            iterable=iterable,
            item_func=lambda i: i,
            id_only_func=lambda item_id, changekey: Item(id=item_id),
            changekey_only_func=lambda item_id, changekey: Item(changekey=changekey),
            id_and_changekey_func=lambda item_id, changekey: Item(id=item_id, changekey=changekey),
        )

    def _as_values(self, iterable):
        if not self.only_fields:
            raise ValueError('values() requires at least one field name')
        return self._item_yielder(
            iterable=iterable,
            item_func=lambda i: {f.path: _get_value_or_default(f, i) for f in self.only_fields},
            id_only_func=lambda item_id, changekey: {'id': item_id},
            changekey_only_func=lambda item_id, changekey: {'changekey': changekey},
            id_and_changekey_func=lambda item_id, changekey: {'id': item_id, 'changekey': changekey},
        )

    def _as_values_list(self, iterable):
        if not self.only_fields:
            raise ValueError('values_list() requires at least one field name')
        return self._item_yielder(
            iterable=iterable,
            item_func=lambda i: tuple(_get_value_or_default(f, i) for f in self.only_fields),
            id_only_func=lambda item_id, changekey: (item_id,),
            changekey_only_func=lambda item_id, changekey: (changekey,),
            id_and_changekey_func=lambda item_id, changekey: (item_id, changekey),
        )

    def _as_flat_values_list(self, iterable):
        if not self.only_fields or len(self.only_fields) != 1:
            raise ValueError('flat=True requires exactly one field name')
        return self._item_yielder(
            iterable=iterable,
            item_func=lambda i: _get_value_or_default(self.only_fields[0], i),
            id_only_func=lambda item_id, changekey: item_id,
            changekey_only_func=lambda item_id, changekey: changekey,
            id_and_changekey_func=None,  # Can never be called
        )

    ###############################
    #
    # Methods that support chaining
    #
    ###############################
    # Return copies of self, so this works as expected:
    #
    # foo_qs = my_folder.filter(...)
    # foo_qs.filter(foo='bar')
    # foo_qs.filter(foo='baz')  # Should not be affected by the previous statement
    #
    def all(self):
        """ """
        new_qs = self._copy_self()
        return new_qs

    def none(self):
        """ """
        new_qs = self._copy_self()
        new_qs.q = Q(conn_type=Q.NEVER)
        return new_qs

    def filter(self, *args, **kwargs):
        new_qs = self._copy_self()
        q = Q(*args, **kwargs)
        new_qs.q = new_qs.q & q
        return new_qs

    def exclude(self, *args, **kwargs):
        new_qs = self._copy_self()
        q = ~Q(*args, **kwargs)
        new_qs.q = new_qs.q & q
        return new_qs

    def people(self):
        """Change the queryset to search the folder for Personas instead of Items."""
        new_qs = self._copy_self()
        new_qs.request_type = self.PERSONA
        return new_qs

    def only(self, *args):
        """Fetch only the specified field names. All other item fields will be 'None'."""
        try:
            only_fields = tuple(self._get_field_path(arg) for arg in args)
        except ValueError as e:
            raise ValueError("%s in only()" % e.args[0])
        new_qs = self._copy_self()
        new_qs.only_fields = only_fields
        return new_qs

    def order_by(self, *args):
        """

        :return: The QuerySet in reverse order. EWS only supports server-side sorting on a single field. Sorting on
          multiple fields is implemented client-side and will therefore make the query greedy.
        """
        try:
            order_fields = tuple(self._get_field_order(arg) for arg in args)
        except ValueError as e:
            raise ValueError("%s in order_by()" % e.args[0])
        new_qs = self._copy_self()
        new_qs.order_fields = order_fields
        return new_qs

    def reverse(self):
        """Reverses the ordering of the queryset."""
        if not self.order_fields:
            raise ValueError('Reversing only makes sense if there are order_by fields')
        new_qs = self._copy_self()
        for f in new_qs.order_fields:
            f.reverse = not f.reverse
        return new_qs

    def values(self, *args):
        try:
            only_fields = tuple(self._get_field_path(arg) for arg in args)
        except ValueError as e:
            raise ValueError("%s in values()" % e.args[0])
        new_qs = self._copy_self()
        new_qs.only_fields = only_fields
        new_qs.return_format = self.VALUES
        return new_qs

    def values_list(self, *args, **kwargs):
        """Return the values of the specified field names as a list of lists. If called with flat=True and only one
        field name, returns a list of values.
        """
        flat = kwargs.pop('flat', False)
        if kwargs:
            raise AttributeError('Unknown kwargs: %s' % kwargs)
        if flat and len(args) != 1:
            raise ValueError('flat=True requires exactly one field name')
        try:
            only_fields = tuple(self._get_field_path(arg) for arg in args)
        except ValueError as e:
            raise ValueError("%s in values_list()" % e.args[0])
        new_qs = self._copy_self()
        new_qs.only_fields = only_fields
        new_qs.return_format = self.FLAT if flat else self.VALUES_LIST
        return new_qs

    def depth(self, depth):
        """Specify the search depth. Possible values are: SHALLOW, ASSOCIATED or DEEP.

        :param depth:
        """
        new_qs = self._copy_self()
        new_qs._depth = depth
        return new_qs

    def iterator(self):
        # Return an iterator over the results
        warnings.warn('QuerySet no longer caches results. .iterator() is a no-op.', DeprecationWarning, stacklevel=2)
        return self.__iter__()

    ###########################
    #
    # Methods that end chaining
    #
    ###########################

    def get(self, *args, **kwargs):
        """Assume the query will return exactly one item. Return that item."""
        if not args and set(kwargs) in ({'id'}, {'id', 'changekey'}):
            # We allow calling get(id=..., changekey=...) to get a single item, but only if exactly these two
            # kwargs are present.
            account = self.folder_collection.account
            item_id = self._id_field.field.clean(kwargs['id'], version=account.version)
            changekey = self._changekey_field.field.clean(kwargs.get('changekey'), version=account.version)
            items = list(account.fetch(ids=[(item_id, changekey)], only_fields=self.only_fields))
        else:
            new_qs = self.filter(*args, **kwargs)
            items = list(new_qs.__iter__())
        if not items:
            raise DoesNotExist()
        if len(items) != 1:
            raise MultipleObjectsReturned()
        return items[0]

    def count(self, page_size=1000):
        """Get the query count, with as little effort as possible 'page_size' is the number of items to
        fetch from the server per request. We're only fetching the IDs, so keep it high

        :param page_size:  (Default value = 1000)
        """
        new_qs = self._copy_self()
        new_qs.only_fields = ()
        new_qs.order_fields = None
        new_qs.return_format = self.NONE
        new_qs.page_size = page_size
        return len(list(new_qs.__iter__()))

    def exists(self):
        """Find out if the query contains any hits, with as little effort as possible."""
        new_qs = self._copy_self()
        new_qs.max_items = 1
        return new_qs.count(page_size=1) > 0

    def _id_only_copy_self(self):
        new_qs = self._copy_self()
        new_qs.only_fields = ()
        new_qs.order_fields = None
        new_qs.return_format = self.NONE
        return new_qs

    def delete(self, page_size=1000, **delete_kwargs):
        """Delete the items matching the query, with as little effort as possible. 'page_size' is the number of items
        to fetch and delete per request. We're only fetching the IDs, so keep it high.

        :param page_size:  (Default value = 1000)
        :param delete_kwargs:
        """
        ids = self._id_only_copy_self()
        ids.page_size = page_size
        return self.folder_collection.account.bulk_delete(
            ids=ids,
            chunk_size=page_size,
            **delete_kwargs
        )

    def send(self, page_size=1000, **send_kwargs):
        """Send the items matching the query, with as little effort as possible. 'page_size' is the number of items
        to fetch and send per request. We're only fetching the IDs, so keep it high.

        :param page_size:  (Default value = 1000)
        :param send_kwargs:
        """
        ids = self._id_only_copy_self()
        ids.page_size = page_size
        return self.folder_collection.account.bulk_send(
            ids=ids,
            chunk_size=page_size,
            **send_kwargs
        )

    def copy(self, to_folder, page_size=1000, **copy_kwargs):
        """Copy the items matching the query, with as little effort as possible. 'page_size' is the number of items
        to fetch and copy per request. We're only fetching the IDs, so keep it high.

        :param to_folder:
        :param page_size:  (Default value = 1000)
        :param copy_kwargs:
        """
        ids = self._id_only_copy_self()
        ids.page_size = page_size
        return self.folder_collection.account.bulk_copy(
            ids=ids,
            to_folder=to_folder,
            chunk_size=page_size,
            **copy_kwargs
        )

    def move(self, to_folder, page_size=1000):
        """Move the items matching the query, with as little effort as possible. 'page_size' is the number of items
        to fetch and move per request. We're only fetching the IDs, so keep it high.

        :param to_folder:
        :param page_size: (Default value = 1000)
        """
        ids = self._id_only_copy_self()
        ids.page_size = page_size
        return self.folder_collection.account.bulk_move(
            ids=ids,
            to_folder=to_folder,
            chunk_size=page_size,
        )

    def archive(self, to_folder, page_size=1000):
        """Archive the items matching the query, with as little effort as possible. 'page_size' is the number of items
        to fetch and move per request. We're only fetching the IDs, so keep it high.

        :param to_folder:
        :param page_size: (Default value = 1000)
        """
        ids = self._id_only_copy_self()
        ids.page_size = page_size
        return self.folder_collection.account.bulk_archive(
            ids=ids,
            to_folder=to_folder,
            chunk_size=page_size,
        )

    def mark_as_junk(self, page_size=1000, **mark_as_junk_kwargs):
        """Mark the items matching the query as junk, with as little effort as possible. 'page_size' is the number of
        items to fetch and mark per request. We're only fetching the IDs, so keep it high.

        :param page_size:  (Default value = 1000)
        :param mark_as_junk_kwargs:
        """
        ids = self._id_only_copy_self()
        ids.page_size = page_size
        return self.folder_collection.account.bulk_mark_as_junk(
            ids=ids,
            chunk_size=page_size,
            **mark_as_junk_kwargs
        )

    def __str__(self):
        fmt_args = [('q', str(self.q)), ('folders', '[%s]' % ', '.join(str(f) for f in self.folder_collection.folders))]
        return self.__class__.__name__ + '(%s)' % ', '.join('%s=%s' % (k, v) for k, v in fmt_args)


def _get_value_or_default(field, item):
    # When we request specific fields using .values() or .values_list(), the incoming item type may not have the field
    # we are requesting. Return None when this happens instead of raising an AttributeError.
    try:
        return field.get_value(item)
    except AttributeError:
        return None


def _get_sort_value_or_default(item, field_order):
    # Python can only sort values when <, > and = are implemented for the two types. Try as best we can to sort
    # items, even when the item may have a None value for the field in question, or when the item is an
    # Exception. In that case, we calculate a default value and sort all None values and exceptions as the default
    # value.
    if isinstance(item, Exception):
        return _default_field_value(field_order.field_path.field)
    val = field_order.field_path.get_sort_value(item)
    if val is None:
        return _default_field_value(field_order.field_path.field)
    return val


def _default_field_value(field):
    """Return the default value of a field. If the field does not have a default value, try creating an empty instance
    of the field value class. If that doesn't work, there's really nothing we can do about it; we'll raise an error.
    """
    return field.default or ([field.value_cls()] if field.is_list else field.value_cls())


def _rinse_item(i, fields_to_nullify):
    """Set fields in fields_to_nullify to None. Make sure to accept exceptions."""
    if isinstance(i, Exception):
        return i
    for f in fields_to_nullify:
        setattr(i, f.field.name, None)
    return i

Classes

class QuerySet (folder_collection, request_type='item')

A Django QuerySet-like class for querying items. Defers queries until the QuerySet is consumed. Supports chaining to build up complex queries.

Django QuerySet documentation: https://docs.djangoproject.com/en/dev/ref/models/querysets/

Expand source code
class QuerySet(SearchableMixIn):
    """A Django QuerySet-like class for querying items. Defers queries until the QuerySet is consumed. Supports chaining to
    build up complex queries.

    Django QuerySet documentation: https://docs.djangoproject.com/en/dev/ref/models/querysets/
    """

    VALUES = 'values'
    VALUES_LIST = 'values_list'
    FLAT = 'flat'
    NONE = 'none'
    RETURN_TYPES = (VALUES, VALUES_LIST, FLAT, NONE)

    ITEM = 'item'
    PERSONA = 'persona'
    REQUEST_TYPES = (ITEM, PERSONA)

    def __init__(self, folder_collection, request_type=ITEM):
        from .folders import FolderCollection
        if not isinstance(folder_collection, FolderCollection):
            raise ValueError("folder_collection value '%s' must be a FolderCollection instance" % folder_collection)
        self.folder_collection = folder_collection  # A FolderCollection instance
        if request_type not in self.REQUEST_TYPES:
            raise ValueError("'request_type' %r must be one of %s" % (request_type, self.REQUEST_TYPES))
        self.request_type = request_type
        self.q = Q()  # Default to no restrictions
        self.only_fields = None
        self.order_fields = None
        self.return_format = self.NONE
        self.calendar_view = None
        self.page_size = None
        self.max_items = None
        self.offset = 0
        self._depth = None

    def _copy_self(self):
        # When we copy a queryset where the cache has already been filled, we don't copy the cache. Thus, a copied
        # queryset will fetch results from the server again.
        #
        # All other behaviour would be awkward:
        #
        # qs = QuerySet(f).filter(foo='bar')
        # items = list(qs)
        # new_qs = qs.exclude(bar='baz')  # This should work, and should fetch from the server
        #
        if not isinstance(self.q, Q):
            raise ValueError("self.q value '%s' must be None or a Q instance" % self.q)
        if not isinstance(self.only_fields, (type(None), tuple)):
            raise ValueError("self.only_fields value '%s' must be None or a tuple" % self.only_fields)
        if not isinstance(self.order_fields, (type(None), tuple)):
            raise ValueError("self.order_fields value '%s' must be None or a tuple" % self.order_fields)
        if self.return_format not in self.RETURN_TYPES:
            raise ValueError("self.return_value '%s' must be one of %s" % (self.return_format, self.RETURN_TYPES))
        # Only mutable objects need to be deepcopied. Folder should be the same object
        new_qs = self.__class__(self.folder_collection, request_type=self.request_type)
        new_qs.q = deepcopy(self.q)
        new_qs.only_fields = self.only_fields
        new_qs.order_fields = None if self.order_fields is None else deepcopy(self.order_fields)
        new_qs.return_format = self.return_format
        new_qs.calendar_view = self.calendar_view
        new_qs.page_size = self.page_size
        new_qs.max_items = self.max_items
        new_qs.offset = self.offset
        new_qs._depth = self._depth
        return new_qs

    def _get_field_path(self, field_path):
        from .items import Persona
        if self.request_type == self.PERSONA:
            return FieldPath(field=Persona.get_field_by_fieldname(field_path))
        for folder in self.folder_collection:
            try:
                return FieldPath.from_string(field_path=field_path, folder=folder)
            except InvalidField:
                pass
        raise InvalidField("Unknown field path %r on folders %s" % (field_path, self.folder_collection.folders))

    def _get_field_order(self, field_path):
        from .items import Persona
        if self.request_type == self.PERSONA:
            return FieldOrder(
                field_path=FieldPath(field=Persona.get_field_by_fieldname(field_path.lstrip('-'))),
                reverse=field_path.startswith('-'),
            )
        for folder in self.folder_collection:
            try:
                return FieldOrder.from_string(field_path=field_path, folder=folder)
            except InvalidField:
                pass
        raise InvalidField("Unknown field path %r on folders %s" % (field_path, self.folder_collection.folders))

    @property
    def _id_field(self):
        return self._get_field_path('id')

    @property
    def _changekey_field(self):
        return self._get_field_path('changekey')

    def _additional_fields(self):
        if not isinstance(self.only_fields, tuple):
            raise ValueError("'only_fields' value %r must be a tuple" % self.only_fields)
        # Remove ItemId and ChangeKey. We get them unconditionally
        additional_fields = {f for f in self.only_fields if not f.field.is_attribute}
        if self.request_type != self.ITEM:
            return additional_fields

        # For CalendarItem items, we want to inject internal timezone fields into the requested fields.
        has_start = 'start' in {f.field.name for f in additional_fields}
        has_end = 'end' in {f.field.name for f in additional_fields}
        meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
        if self.folder_collection.account.version.build < EXCHANGE_2010:
            if has_start or has_end:
                additional_fields.add(FieldPath(field=meeting_tz_field))
        else:
            if has_start:
                additional_fields.add(FieldPath(field=start_tz_field))
            if has_end:
                additional_fields.add(FieldPath(field=end_tz_field))
        return additional_fields

    def _format_items(self, items, return_format):
        return {
            self.VALUES: self._as_values,
            self.VALUES_LIST: self._as_values_list,
            self.FLAT: self._as_flat_values_list,
            self.NONE: self._as_items,
        }[return_format](items)

    def _query(self):
        if self.only_fields is None:
            # We didn't restrict list of field paths. Get all fields from the server, including extended properties.
            if self.request_type == self.PERSONA:
                additional_fields = {}  # GetPersona doesn't take explicit fields. Don't bother calculating the list
                complex_fields_requested = True
            else:
                additional_fields = {FieldPath(field=f) for f in self.folder_collection.allowed_item_fields()}
                complex_fields_requested = True
        else:
            additional_fields = self._additional_fields()
            complex_fields_requested = any(f.field.is_complex for f in additional_fields)

        # EWS can do server-side sorting on multiple fields. A caveat is that server-side sorting is not supported
        # for calendar views. In this case, we do all the sorting client-side.
        if self.calendar_view:
            must_sort_clientside = bool(self.order_fields)
            order_fields = None
        else:
            must_sort_clientside = False
            order_fields = self.order_fields

        if must_sort_clientside:
            # Also fetch order_by fields that we only need for client-side sorting.
            extra_order_fields = {f.field_path for f in self.order_fields} - additional_fields
            if extra_order_fields:
                additional_fields.update(extra_order_fields)
        else:
            extra_order_fields = set()

        find_kwargs = dict(
            shape=ID_ONLY,  # Always use IdOnly here, because AllProperties doesn't actually get *all* properties
            depth=self._depth,
            additional_fields=additional_fields,
            order_fields=order_fields,
            page_size=self.page_size,
            max_items=self.max_items,
            offset=self.offset,
        )
        if self.request_type == self.PERSONA:
            if complex_fields_requested:
                find_kwargs['additional_fields'] = None
                items = self.folder_collection.account.fetch_personas(
                    ids=self.folder_collection.find_people(self.q, **find_kwargs)
                )
            else:
                if not additional_fields:
                    find_kwargs['additional_fields'] = None
                items = self.folder_collection.find_people(self.q, **find_kwargs)
        else:
            find_kwargs['calendar_view'] = self.calendar_view
            if complex_fields_requested:
                # The FindItem service does not support complex field types. Tell find_items() to return
                # (id, changekey) tuples, and pass that to fetch().
                find_kwargs['additional_fields'] = None
                items = self.folder_collection.account.fetch(
                    ids=self.folder_collection.find_items(self.q, **find_kwargs),
                    only_fields=additional_fields,
                    chunk_size=self.page_size,
                )
            else:
                if not additional_fields:
                    # If additional_fields is the empty set, we only requested ID and changekey fields. We can then
                    # take a shortcut by using (shape=ID_ONLY, additional_fields=None) to tell find_items() to return
                    # (id, changekey) tuples. We'll post-process those later.
                    find_kwargs['additional_fields'] = None
                items = self.folder_collection.find_items(self.q, **find_kwargs)

        if not must_sort_clientside:
            return items

        # Resort to client-side sorting of the order_by fields. This is greedy. Sorting in Python is stable, so when
        # sorting on multiple fields, we can just do a sort on each of the requested fields in reverse order. Reverse
        # each sort operation if the field was marked as such.
        for f in reversed(self.order_fields):
            try:
                items = sorted(items, key=lambda i: _get_sort_value_or_default(i, f), reverse=f.reverse)
            except TypeError as e:
                if 'unorderable types' not in e.args[0]:
                    raise
                raise ValueError((
                    "Cannot sort on field '%s'. The field has no default value defined, and there are either items "
                    "with None values for this field, or the query contains exception instances (original error: %s).")
                                 % (f.field_path, e))
        if not extra_order_fields:
            return items

        # Nullify the fields we only needed for sorting before returning
        return (_rinse_item(i, extra_order_fields) for i in items)

    def __iter__(self):
        # Fill cache if this is the first iteration. Return an iterator over the results. Make this non-greedy by
        # filling the cache while we are iterating.
        #
        if self.q.is_never():
            return

        log.debug('Initializing cache')
        yield from self._format_items(items=self._query(), return_format=self.return_format)

    """Do not implement __len__. The implementation of list() tries to preallocate memory by calling __len__ on the
    given sequence, before calling __iter__. If we implemented __len__, we would end up calling FindItems twice, once
    to get the result of self.count(), an once to return the actual result.

    Also, according to https://stackoverflow.com/questions/37189968/how-to-have-list-consume-iter-without-calling-len,
    a __len__ implementation should be cheap. That does not hold for self.count().

    def __len__(self):
        # This queryset has no cache yet. Call the optimized counting implementation
        return self.count()
    """

    def __getitem__(self, idx_or_slice):
        # Support indexing and slicing. This is non-greedy when possible (slicing start, stop and step are not negative,
        # and we're ordering on at most one field), and will only fill the cache if the entire query is iterated.
        if isinstance(idx_or_slice, int):
            return self._getitem_idx(idx_or_slice)
        return self._getitem_slice(idx_or_slice)

    def _getitem_idx(self, idx):
        if idx < 0:
            # Support negative indexes by reversing the queryset and negating the index value
            reverse_idx = -(idx+1)
            return self.reverse()[reverse_idx]
        # Optimize by setting an exact offset and fetching only 1 item
        new_qs = self._copy_self()
        new_qs.max_items = 1
        new_qs.page_size = 1
        new_qs.offset = idx
        # The iterator will return at most 1 item
        for item in new_qs.__iter__():
            return item
        raise IndexError()

    def _getitem_slice(self, s):
        if ((s.start or 0) < 0) or ((s.stop or 0) < 0) or ((s.step or 0) < 0):
            # islice() does not support negative start, stop and step. Make sure cache is full by iterating the full
            # query result, and then slice on the cache.
            return list(self.__iter__())[s]
        # Optimize by setting an exact offset and max_items value
        new_qs = self._copy_self()
        if s.start is not None and s.stop is not None:
            new_qs.offset = s.start
            new_qs.max_items = s.stop - s.start
        elif s.start is not None:
            new_qs.offset = s.start
        elif s.stop is not None:
            new_qs.max_items = s.stop
        if new_qs.page_size is None and new_qs.max_items is not None and new_qs.max_items < CHUNK_SIZE:
            new_qs.page_size = new_qs.max_items
        return islice(new_qs.__iter__(), None, None, s.step)

    def _item_yielder(self, iterable, item_func, id_only_func, changekey_only_func, id_and_changekey_func):
        # Transforms results from the server according to the given transform functions. Makes sure to pass on
        # Exception instances unaltered.
        if self.only_fields:
            has_non_attribute_fields = bool({f for f in self.only_fields if not f.field.is_attribute})
        else:
            has_non_attribute_fields = True
        if not has_non_attribute_fields:
            # _query() will return an iterator of (id, changekey) tuples
            if self._changekey_field not in self.only_fields:
                transform_func = id_only_func
            elif self._id_field not in self.only_fields:
                transform_func = changekey_only_func
            else:
                transform_func = id_and_changekey_func
            for i in iterable:
                if isinstance(i, Exception):
                    yield i
                    continue
                yield transform_func(*i)
            return
        for i in iterable:
            if isinstance(i, Exception):
                yield i
                continue
            yield item_func(i)

    def _as_items(self, iterable):
        from .items import Item
        return self._item_yielder(
            iterable=iterable,
            item_func=lambda i: i,
            id_only_func=lambda item_id, changekey: Item(id=item_id),
            changekey_only_func=lambda item_id, changekey: Item(changekey=changekey),
            id_and_changekey_func=lambda item_id, changekey: Item(id=item_id, changekey=changekey),
        )

    def _as_values(self, iterable):
        if not self.only_fields:
            raise ValueError('values() requires at least one field name')
        return self._item_yielder(
            iterable=iterable,
            item_func=lambda i: {f.path: _get_value_or_default(f, i) for f in self.only_fields},
            id_only_func=lambda item_id, changekey: {'id': item_id},
            changekey_only_func=lambda item_id, changekey: {'changekey': changekey},
            id_and_changekey_func=lambda item_id, changekey: {'id': item_id, 'changekey': changekey},
        )

    def _as_values_list(self, iterable):
        if not self.only_fields:
            raise ValueError('values_list() requires at least one field name')
        return self._item_yielder(
            iterable=iterable,
            item_func=lambda i: tuple(_get_value_or_default(f, i) for f in self.only_fields),
            id_only_func=lambda item_id, changekey: (item_id,),
            changekey_only_func=lambda item_id, changekey: (changekey,),
            id_and_changekey_func=lambda item_id, changekey: (item_id, changekey),
        )

    def _as_flat_values_list(self, iterable):
        if not self.only_fields or len(self.only_fields) != 1:
            raise ValueError('flat=True requires exactly one field name')
        return self._item_yielder(
            iterable=iterable,
            item_func=lambda i: _get_value_or_default(self.only_fields[0], i),
            id_only_func=lambda item_id, changekey: item_id,
            changekey_only_func=lambda item_id, changekey: changekey,
            id_and_changekey_func=None,  # Can never be called
        )

    ###############################
    #
    # Methods that support chaining
    #
    ###############################
    # Return copies of self, so this works as expected:
    #
    # foo_qs = my_folder.filter(...)
    # foo_qs.filter(foo='bar')
    # foo_qs.filter(foo='baz')  # Should not be affected by the previous statement
    #
    def all(self):
        """ """
        new_qs = self._copy_self()
        return new_qs

    def none(self):
        """ """
        new_qs = self._copy_self()
        new_qs.q = Q(conn_type=Q.NEVER)
        return new_qs

    def filter(self, *args, **kwargs):
        new_qs = self._copy_self()
        q = Q(*args, **kwargs)
        new_qs.q = new_qs.q & q
        return new_qs

    def exclude(self, *args, **kwargs):
        new_qs = self._copy_self()
        q = ~Q(*args, **kwargs)
        new_qs.q = new_qs.q & q
        return new_qs

    def people(self):
        """Change the queryset to search the folder for Personas instead of Items."""
        new_qs = self._copy_self()
        new_qs.request_type = self.PERSONA
        return new_qs

    def only(self, *args):
        """Fetch only the specified field names. All other item fields will be 'None'."""
        try:
            only_fields = tuple(self._get_field_path(arg) for arg in args)
        except ValueError as e:
            raise ValueError("%s in only()" % e.args[0])
        new_qs = self._copy_self()
        new_qs.only_fields = only_fields
        return new_qs

    def order_by(self, *args):
        """

        :return: The QuerySet in reverse order. EWS only supports server-side sorting on a single field. Sorting on
          multiple fields is implemented client-side and will therefore make the query greedy.
        """
        try:
            order_fields = tuple(self._get_field_order(arg) for arg in args)
        except ValueError as e:
            raise ValueError("%s in order_by()" % e.args[0])
        new_qs = self._copy_self()
        new_qs.order_fields = order_fields
        return new_qs

    def reverse(self):
        """Reverses the ordering of the queryset."""
        if not self.order_fields:
            raise ValueError('Reversing only makes sense if there are order_by fields')
        new_qs = self._copy_self()
        for f in new_qs.order_fields:
            f.reverse = not f.reverse
        return new_qs

    def values(self, *args):
        try:
            only_fields = tuple(self._get_field_path(arg) for arg in args)
        except ValueError as e:
            raise ValueError("%s in values()" % e.args[0])
        new_qs = self._copy_self()
        new_qs.only_fields = only_fields
        new_qs.return_format = self.VALUES
        return new_qs

    def values_list(self, *args, **kwargs):
        """Return the values of the specified field names as a list of lists. If called with flat=True and only one
        field name, returns a list of values.
        """
        flat = kwargs.pop('flat', False)
        if kwargs:
            raise AttributeError('Unknown kwargs: %s' % kwargs)
        if flat and len(args) != 1:
            raise ValueError('flat=True requires exactly one field name')
        try:
            only_fields = tuple(self._get_field_path(arg) for arg in args)
        except ValueError as e:
            raise ValueError("%s in values_list()" % e.args[0])
        new_qs = self._copy_self()
        new_qs.only_fields = only_fields
        new_qs.return_format = self.FLAT if flat else self.VALUES_LIST
        return new_qs

    def depth(self, depth):
        """Specify the search depth. Possible values are: SHALLOW, ASSOCIATED or DEEP.

        :param depth:
        """
        new_qs = self._copy_self()
        new_qs._depth = depth
        return new_qs

    def iterator(self):
        # Return an iterator over the results
        warnings.warn('QuerySet no longer caches results. .iterator() is a no-op.', DeprecationWarning, stacklevel=2)
        return self.__iter__()

    ###########################
    #
    # Methods that end chaining
    #
    ###########################

    def get(self, *args, **kwargs):
        """Assume the query will return exactly one item. Return that item."""
        if not args and set(kwargs) in ({'id'}, {'id', 'changekey'}):
            # We allow calling get(id=..., changekey=...) to get a single item, but only if exactly these two
            # kwargs are present.
            account = self.folder_collection.account
            item_id = self._id_field.field.clean(kwargs['id'], version=account.version)
            changekey = self._changekey_field.field.clean(kwargs.get('changekey'), version=account.version)
            items = list(account.fetch(ids=[(item_id, changekey)], only_fields=self.only_fields))
        else:
            new_qs = self.filter(*args, **kwargs)
            items = list(new_qs.__iter__())
        if not items:
            raise DoesNotExist()
        if len(items) != 1:
            raise MultipleObjectsReturned()
        return items[0]

    def count(self, page_size=1000):
        """Get the query count, with as little effort as possible 'page_size' is the number of items to
        fetch from the server per request. We're only fetching the IDs, so keep it high

        :param page_size:  (Default value = 1000)
        """
        new_qs = self._copy_self()
        new_qs.only_fields = ()
        new_qs.order_fields = None
        new_qs.return_format = self.NONE
        new_qs.page_size = page_size
        return len(list(new_qs.__iter__()))

    def exists(self):
        """Find out if the query contains any hits, with as little effort as possible."""
        new_qs = self._copy_self()
        new_qs.max_items = 1
        return new_qs.count(page_size=1) > 0

    def _id_only_copy_self(self):
        new_qs = self._copy_self()
        new_qs.only_fields = ()
        new_qs.order_fields = None
        new_qs.return_format = self.NONE
        return new_qs

    def delete(self, page_size=1000, **delete_kwargs):
        """Delete the items matching the query, with as little effort as possible. 'page_size' is the number of items
        to fetch and delete per request. We're only fetching the IDs, so keep it high.

        :param page_size:  (Default value = 1000)
        :param delete_kwargs:
        """
        ids = self._id_only_copy_self()
        ids.page_size = page_size
        return self.folder_collection.account.bulk_delete(
            ids=ids,
            chunk_size=page_size,
            **delete_kwargs
        )

    def send(self, page_size=1000, **send_kwargs):
        """Send the items matching the query, with as little effort as possible. 'page_size' is the number of items
        to fetch and send per request. We're only fetching the IDs, so keep it high.

        :param page_size:  (Default value = 1000)
        :param send_kwargs:
        """
        ids = self._id_only_copy_self()
        ids.page_size = page_size
        return self.folder_collection.account.bulk_send(
            ids=ids,
            chunk_size=page_size,
            **send_kwargs
        )

    def copy(self, to_folder, page_size=1000, **copy_kwargs):
        """Copy the items matching the query, with as little effort as possible. 'page_size' is the number of items
        to fetch and copy per request. We're only fetching the IDs, so keep it high.

        :param to_folder:
        :param page_size:  (Default value = 1000)
        :param copy_kwargs:
        """
        ids = self._id_only_copy_self()
        ids.page_size = page_size
        return self.folder_collection.account.bulk_copy(
            ids=ids,
            to_folder=to_folder,
            chunk_size=page_size,
            **copy_kwargs
        )

    def move(self, to_folder, page_size=1000):
        """Move the items matching the query, with as little effort as possible. 'page_size' is the number of items
        to fetch and move per request. We're only fetching the IDs, so keep it high.

        :param to_folder:
        :param page_size: (Default value = 1000)
        """
        ids = self._id_only_copy_self()
        ids.page_size = page_size
        return self.folder_collection.account.bulk_move(
            ids=ids,
            to_folder=to_folder,
            chunk_size=page_size,
        )

    def archive(self, to_folder, page_size=1000):
        """Archive the items matching the query, with as little effort as possible. 'page_size' is the number of items
        to fetch and move per request. We're only fetching the IDs, so keep it high.

        :param to_folder:
        :param page_size: (Default value = 1000)
        """
        ids = self._id_only_copy_self()
        ids.page_size = page_size
        return self.folder_collection.account.bulk_archive(
            ids=ids,
            to_folder=to_folder,
            chunk_size=page_size,
        )

    def mark_as_junk(self, page_size=1000, **mark_as_junk_kwargs):
        """Mark the items matching the query as junk, with as little effort as possible. 'page_size' is the number of
        items to fetch and mark per request. We're only fetching the IDs, so keep it high.

        :param page_size:  (Default value = 1000)
        :param mark_as_junk_kwargs:
        """
        ids = self._id_only_copy_self()
        ids.page_size = page_size
        return self.folder_collection.account.bulk_mark_as_junk(
            ids=ids,
            chunk_size=page_size,
            **mark_as_junk_kwargs
        )

    def __str__(self):
        fmt_args = [('q', str(self.q)), ('folders', '[%s]' % ', '.join(str(f) for f in self.folder_collection.folders))]
        return self.__class__.__name__ + '(%s)' % ', '.join('%s=%s' % (k, v) for k, v in fmt_args)

Ancestors

Class variables

var FLAT
var ITEM
var NONE
var PERSONA
var REQUEST_TYPES
var RETURN_TYPES
var VALUES
var VALUES_LIST

Methods

def all(self)
Expand source code
def all(self):
    """ """
    new_qs = self._copy_self()
    return new_qs
def archive(self, to_folder, page_size=1000)

Archive the items matching the query, with as little effort as possible. 'page_size' is the number of items to fetch and move per request. We're only fetching the IDs, so keep it high.

:param to_folder: :param page_size: (Default value = 1000)

Expand source code
def archive(self, to_folder, page_size=1000):
    """Archive the items matching the query, with as little effort as possible. 'page_size' is the number of items
    to fetch and move per request. We're only fetching the IDs, so keep it high.

    :param to_folder:
    :param page_size: (Default value = 1000)
    """
    ids = self._id_only_copy_self()
    ids.page_size = page_size
    return self.folder_collection.account.bulk_archive(
        ids=ids,
        to_folder=to_folder,
        chunk_size=page_size,
    )
def copy(self, to_folder, page_size=1000, **copy_kwargs)

Copy the items matching the query, with as little effort as possible. 'page_size' is the number of items to fetch and copy per request. We're only fetching the IDs, so keep it high.

:param to_folder: :param page_size: (Default value = 1000) :param copy_kwargs:

Expand source code
def copy(self, to_folder, page_size=1000, **copy_kwargs):
    """Copy the items matching the query, with as little effort as possible. 'page_size' is the number of items
    to fetch and copy per request. We're only fetching the IDs, so keep it high.

    :param to_folder:
    :param page_size:  (Default value = 1000)
    :param copy_kwargs:
    """
    ids = self._id_only_copy_self()
    ids.page_size = page_size
    return self.folder_collection.account.bulk_copy(
        ids=ids,
        to_folder=to_folder,
        chunk_size=page_size,
        **copy_kwargs
    )
def count(self, page_size=1000)

Get the query count, with as little effort as possible 'page_size' is the number of items to fetch from the server per request. We're only fetching the IDs, so keep it high

:param page_size: (Default value = 1000)

Expand source code
def count(self, page_size=1000):
    """Get the query count, with as little effort as possible 'page_size' is the number of items to
    fetch from the server per request. We're only fetching the IDs, so keep it high

    :param page_size:  (Default value = 1000)
    """
    new_qs = self._copy_self()
    new_qs.only_fields = ()
    new_qs.order_fields = None
    new_qs.return_format = self.NONE
    new_qs.page_size = page_size
    return len(list(new_qs.__iter__()))
def delete(self, page_size=1000, **delete_kwargs)

Delete the items matching the query, with as little effort as possible. 'page_size' is the number of items to fetch and delete per request. We're only fetching the IDs, so keep it high.

:param page_size: (Default value = 1000) :param delete_kwargs:

Expand source code
def delete(self, page_size=1000, **delete_kwargs):
    """Delete the items matching the query, with as little effort as possible. 'page_size' is the number of items
    to fetch and delete per request. We're only fetching the IDs, so keep it high.

    :param page_size:  (Default value = 1000)
    :param delete_kwargs:
    """
    ids = self._id_only_copy_self()
    ids.page_size = page_size
    return self.folder_collection.account.bulk_delete(
        ids=ids,
        chunk_size=page_size,
        **delete_kwargs
    )
def depth(self, depth)

Specify the search depth. Possible values are: SHALLOW, ASSOCIATED or DEEP.

:param depth:

Expand source code
def depth(self, depth):
    """Specify the search depth. Possible values are: SHALLOW, ASSOCIATED or DEEP.

    :param depth:
    """
    new_qs = self._copy_self()
    new_qs._depth = depth
    return new_qs
def exclude(self, *args, **kwargs)
Expand source code
def exclude(self, *args, **kwargs):
    new_qs = self._copy_self()
    q = ~Q(*args, **kwargs)
    new_qs.q = new_qs.q & q
    return new_qs
def exists(self)

Find out if the query contains any hits, with as little effort as possible.

Expand source code
def exists(self):
    """Find out if the query contains any hits, with as little effort as possible."""
    new_qs = self._copy_self()
    new_qs.max_items = 1
    return new_qs.count(page_size=1) > 0
def filter(self, *args, **kwargs)
Expand source code
def filter(self, *args, **kwargs):
    new_qs = self._copy_self()
    q = Q(*args, **kwargs)
    new_qs.q = new_qs.q & q
    return new_qs
def get(self, *args, **kwargs)

Assume the query will return exactly one item. Return that item.

Expand source code
def get(self, *args, **kwargs):
    """Assume the query will return exactly one item. Return that item."""
    if not args and set(kwargs) in ({'id'}, {'id', 'changekey'}):
        # We allow calling get(id=..., changekey=...) to get a single item, but only if exactly these two
        # kwargs are present.
        account = self.folder_collection.account
        item_id = self._id_field.field.clean(kwargs['id'], version=account.version)
        changekey = self._changekey_field.field.clean(kwargs.get('changekey'), version=account.version)
        items = list(account.fetch(ids=[(item_id, changekey)], only_fields=self.only_fields))
    else:
        new_qs = self.filter(*args, **kwargs)
        items = list(new_qs.__iter__())
    if not items:
        raise DoesNotExist()
    if len(items) != 1:
        raise MultipleObjectsReturned()
    return items[0]
def iterator(self)
Expand source code
def iterator(self):
    # Return an iterator over the results
    warnings.warn('QuerySet no longer caches results. .iterator() is a no-op.', DeprecationWarning, stacklevel=2)
    return self.__iter__()
def mark_as_junk(self, page_size=1000, **mark_as_junk_kwargs)

Mark the items matching the query as junk, with as little effort as possible. 'page_size' is the number of items to fetch and mark per request. We're only fetching the IDs, so keep it high.

:param page_size: (Default value = 1000) :param mark_as_junk_kwargs:

Expand source code
def mark_as_junk(self, page_size=1000, **mark_as_junk_kwargs):
    """Mark the items matching the query as junk, with as little effort as possible. 'page_size' is the number of
    items to fetch and mark per request. We're only fetching the IDs, so keep it high.

    :param page_size:  (Default value = 1000)
    :param mark_as_junk_kwargs:
    """
    ids = self._id_only_copy_self()
    ids.page_size = page_size
    return self.folder_collection.account.bulk_mark_as_junk(
        ids=ids,
        chunk_size=page_size,
        **mark_as_junk_kwargs
    )
def move(self, to_folder, page_size=1000)

Move the items matching the query, with as little effort as possible. 'page_size' is the number of items to fetch and move per request. We're only fetching the IDs, so keep it high.

:param to_folder: :param page_size: (Default value = 1000)

Expand source code
def move(self, to_folder, page_size=1000):
    """Move the items matching the query, with as little effort as possible. 'page_size' is the number of items
    to fetch and move per request. We're only fetching the IDs, so keep it high.

    :param to_folder:
    :param page_size: (Default value = 1000)
    """
    ids = self._id_only_copy_self()
    ids.page_size = page_size
    return self.folder_collection.account.bulk_move(
        ids=ids,
        to_folder=to_folder,
        chunk_size=page_size,
    )
def none(self)
Expand source code
def none(self):
    """ """
    new_qs = self._copy_self()
    new_qs.q = Q(conn_type=Q.NEVER)
    return new_qs
def only(self, *args)

Fetch only the specified field names. All other item fields will be 'None'.

Expand source code
def only(self, *args):
    """Fetch only the specified field names. All other item fields will be 'None'."""
    try:
        only_fields = tuple(self._get_field_path(arg) for arg in args)
    except ValueError as e:
        raise ValueError("%s in only()" % e.args[0])
    new_qs = self._copy_self()
    new_qs.only_fields = only_fields
    return new_qs
def order_by(self, *args)

:return: The QuerySet in reverse order. EWS only supports server-side sorting on a single field. Sorting on multiple fields is implemented client-side and will therefore make the query greedy.

Expand source code
def order_by(self, *args):
    """

    :return: The QuerySet in reverse order. EWS only supports server-side sorting on a single field. Sorting on
      multiple fields is implemented client-side and will therefore make the query greedy.
    """
    try:
        order_fields = tuple(self._get_field_order(arg) for arg in args)
    except ValueError as e:
        raise ValueError("%s in order_by()" % e.args[0])
    new_qs = self._copy_self()
    new_qs.order_fields = order_fields
    return new_qs
def people(self)

Change the queryset to search the folder for Personas instead of Items.

Expand source code
def people(self):
    """Change the queryset to search the folder for Personas instead of Items."""
    new_qs = self._copy_self()
    new_qs.request_type = self.PERSONA
    return new_qs
def reverse(self)

Reverses the ordering of the queryset.

Expand source code
def reverse(self):
    """Reverses the ordering of the queryset."""
    if not self.order_fields:
        raise ValueError('Reversing only makes sense if there are order_by fields')
    new_qs = self._copy_self()
    for f in new_qs.order_fields:
        f.reverse = not f.reverse
    return new_qs
def send(self, page_size=1000, **send_kwargs)

Send the items matching the query, with as little effort as possible. 'page_size' is the number of items to fetch and send per request. We're only fetching the IDs, so keep it high.

:param page_size: (Default value = 1000) :param send_kwargs:

Expand source code
def send(self, page_size=1000, **send_kwargs):
    """Send the items matching the query, with as little effort as possible. 'page_size' is the number of items
    to fetch and send per request. We're only fetching the IDs, so keep it high.

    :param page_size:  (Default value = 1000)
    :param send_kwargs:
    """
    ids = self._id_only_copy_self()
    ids.page_size = page_size
    return self.folder_collection.account.bulk_send(
        ids=ids,
        chunk_size=page_size,
        **send_kwargs
    )
def values(self, *args)
Expand source code
def values(self, *args):
    try:
        only_fields = tuple(self._get_field_path(arg) for arg in args)
    except ValueError as e:
        raise ValueError("%s in values()" % e.args[0])
    new_qs = self._copy_self()
    new_qs.only_fields = only_fields
    new_qs.return_format = self.VALUES
    return new_qs
def values_list(self, *args, **kwargs)

Return the values of the specified field names as a list of lists. If called with flat=True and only one field name, returns a list of values.

Expand source code
def values_list(self, *args, **kwargs):
    """Return the values of the specified field names as a list of lists. If called with flat=True and only one
    field name, returns a list of values.
    """
    flat = kwargs.pop('flat', False)
    if kwargs:
        raise AttributeError('Unknown kwargs: %s' % kwargs)
    if flat and len(args) != 1:
        raise ValueError('flat=True requires exactly one field name')
    try:
        only_fields = tuple(self._get_field_path(arg) for arg in args)
    except ValueError as e:
        raise ValueError("%s in values_list()" % e.args[0])
    new_qs = self._copy_self()
    new_qs.only_fields = only_fields
    new_qs.return_format = self.FLAT if flat else self.VALUES_LIST
    return new_qs
class SearchableMixIn

Implement a search API for inheritance.

Expand source code
class SearchableMixIn:
    """Implement a search API for inheritance."""

    @abc.abstractmethod
    def get(self, *args, **kwargs):
        pass

    @abc.abstractmethod
    def all(self):
        pass

    @abc.abstractmethod
    def none(self):
        pass

    @abc.abstractmethod
    def filter(self, *args, **kwargs):
        pass

    @abc.abstractmethod
    def exclude(self, *args, **kwargs):
        pass

    @abc.abstractmethod
    def people(self):
        pass

Subclasses

Methods

def all(self)
Expand source code
@abc.abstractmethod
def all(self):
    pass
def exclude(self, *args, **kwargs)
Expand source code
@abc.abstractmethod
def exclude(self, *args, **kwargs):
    pass
def filter(self, *args, **kwargs)
Expand source code
@abc.abstractmethod
def filter(self, *args, **kwargs):
    pass
def get(self, *args, **kwargs)
Expand source code
@abc.abstractmethod
def get(self, *args, **kwargs):
    pass
def none(self)
Expand source code
@abc.abstractmethod
def none(self):
    pass
def people(self)
Expand source code
@abc.abstractmethod
def people(self):
    pass
exchangelib-4.6.1/docs/exchangelib/recurrence.html000066400000000000000000003112751414601472700222360ustar00rootroot00000000000000 exchangelib.recurrence API documentation

Module exchangelib.recurrence

Expand source code
import logging

from .fields import IntegerField, EnumField, EnumListField, DateOrDateTimeField, DateTimeField, EWSElementField, \
    IdElementField, MONTHS, WEEK_NUMBERS, WEEKDAYS
from .properties import EWSElement, IdChangeKeyMixIn, ItemId, EWSMeta

log = logging.getLogger(__name__)


def _month_to_str(month):
    return MONTHS[month-1] if isinstance(month, int) else month


def _weekday_to_str(weekday):
    return WEEKDAYS[weekday - 1] if isinstance(weekday, int) else weekday


def _week_number_to_str(week_number):
    return WEEK_NUMBERS[week_number - 1] if isinstance(week_number, int) else week_number


class Pattern(EWSElement, metaclass=EWSMeta):
    """Base class for all classes implementing recurring pattern elements."""


class Regeneration(Pattern, metaclass=EWSMeta):
    """Base class for all classes implementing recurring regeneration elements."""


class AbsoluteYearlyPattern(Pattern):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/absoluteyearlyrecurrence
    """

    ELEMENT_NAME = 'AbsoluteYearlyRecurrence'

    # The day of month of an occurrence, in range 1 -> 31. If a particular month has less days than the day_of_month
    # value, the last day in the month is assumed
    day_of_month = IntegerField(field_uri='DayOfMonth', min=1, max=31, is_required=True)
    # The month of the year, from 1 - 12
    month = EnumField(field_uri='Month', enum=MONTHS, is_required=True)

    def __str__(self):
        return 'Occurs on day %s of %s' % (self.day_of_month, _month_to_str(self.month))


class RelativeYearlyPattern(Pattern):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/relativeyearlyrecurrence
    """

    ELEMENT_NAME = 'RelativeYearlyRecurrence'

    # The weekday of the occurrence, as a valid ISO 8601 weekday number in range 1 -> 7 (1 being Monday).
    # Alternatively, the weekday can be one of the DAY (or 8), WEEK_DAY (or 9) or WEEKEND_DAY (or 10) consts which
    # is interpreted as the first day, weekday, or weekend day in the month, respectively.
    weekday = EnumField(field_uri='DaysOfWeek', enum=WEEKDAYS, is_required=True)
    # Week number of the month, in range 1 -> 5. If 5 is specified, this assumes the last week of the month for
    # months that have only 4 weeks
    week_number = EnumField(field_uri='DayOfWeekIndex', enum=WEEK_NUMBERS, is_required=True)
    # The month of the year, from 1 - 12
    month = EnumField(field_uri='Month', enum=MONTHS, is_required=True)

    def __str__(self):
        return 'Occurs on weekday %s in the %s week of %s' % (
            _weekday_to_str(self.weekday),
            _week_number_to_str(self.week_number),
            _month_to_str(self.month)
        )


class AbsoluteMonthlyPattern(Pattern):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/absolutemonthlyrecurrence
    """

    ELEMENT_NAME = 'AbsoluteMonthlyRecurrence'

    # Interval, in months, in range 1 -> 99
    interval = IntegerField(field_uri='Interval', min=1, max=99, is_required=True)
    # The day of month of an occurrence, in range 1 -> 31. If a particular month has less days than the day_of_month
    # value, the last day in the month is assumed
    day_of_month = IntegerField(field_uri='DayOfMonth', min=1, max=31, is_required=True)

    def __str__(self):
        return 'Occurs on day %s of every %s month(s)' % (self.day_of_month, self.interval)


class RelativeMonthlyPattern(Pattern):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/relativemonthlyrecurrence
    """

    ELEMENT_NAME = 'RelativeMonthlyRecurrence'

    # Interval, in months, in range 1 -> 99
    interval = IntegerField(field_uri='Interval', min=1, max=99, is_required=True)
    # The weekday of the occurrence, as a valid ISO 8601 weekday number in range 1 -> 7 (1 being Monday).
    # Alternatively, the weekday can be one of the DAY (or 8), WEEK_DAY (or 9) or WEEKEND_DAY (or 10) consts which
    # is interpreted as the first day, weekday, or weekend day in the month, respectively.
    weekday = EnumField(field_uri='DaysOfWeek', enum=WEEKDAYS, is_required=True)
    # Week number of the month, in range 1 -> 5. If 5 is specified, this assumes the last week of the month for
    # months that have only 4 weeks.
    week_number = EnumField(field_uri='DayOfWeekIndex', enum=WEEK_NUMBERS, is_required=True)

    def __str__(self):
        return 'Occurs on weekday %s in the %s week of every %s month(s)' % (
            _weekday_to_str(self.weekday),
            _week_number_to_str(self.week_number),
            self.interval
        )


class WeeklyPattern(Pattern):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/weeklyrecurrence"""

    ELEMENT_NAME = 'WeeklyRecurrence'

    # Interval, in weeks, in range 1 -> 99
    interval = IntegerField(field_uri='Interval', min=1, max=99, is_required=True)
    # List of valid ISO 8601 weekdays, as list of numbers in range 1 -> 7 (1 being Monday)
    weekdays = EnumListField(field_uri='DaysOfWeek', enum=WEEKDAYS, is_required=True)
    # The first day of the week. Defaults to Monday
    first_day_of_week = EnumField(field_uri='FirstDayOfWeek', enum=WEEKDAYS, default=1, is_required=True)

    def __str__(self):
        if isinstance(self.weekdays, str):
            weekdays = [self.weekdays]
        elif isinstance(self.weekdays, int):
            weekdays = [_weekday_to_str(self.weekdays)]
        else:
            weekdays = [_weekday_to_str(i) for i in self.weekdays]
        return 'Occurs on weekdays %s of every %s week(s) where the first day of the week is %s' % (
            ', '.join(weekdays), self.interval, _weekday_to_str(self.first_day_of_week)
        )


class DailyPattern(Pattern):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/dailyrecurrence"""

    ELEMENT_NAME = 'DailyRecurrence'

    # Interval, in days, in range 1 -> 999
    interval = IntegerField(field_uri='Interval', min=1, max=999, is_required=True)

    def __str__(self):
        return 'Occurs every %s day(s)' % self.interval


class YearlyRegeneration(Regeneration):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/yearlyregeneration"""

    ELEMENT_NAME = 'YearlyRegeneration'

    # Interval, in years
    interval = IntegerField(field_uri='Interval', min=1, is_required=True)

    def __str__(self):
        return 'Regenerates every %s year(s)' % self.interval


class MonthlyRegeneration(Regeneration):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/monthlyregeneration"""

    ELEMENT_NAME = 'MonthlyRegeneration'

    # Interval, in months
    interval = IntegerField(field_uri='Interval', min=1, is_required=True)

    def __str__(self):
        return 'Regenerates every %s month(s)' % self.interval


class WeeklyRegeneration(Regeneration):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/weeklyregeneration"""

    ELEMENT_NAME = 'WeeklyRegeneration'

    # Interval, in weeks
    interval = IntegerField(field_uri='Interval', min=1, is_required=True)

    def __str__(self):
        return 'Regenerates every %s week(s)' % self.interval


class DailyRegeneration(Regeneration):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/dailyregeneration"""

    ELEMENT_NAME = 'DailyRegeneration'

    # Interval, in days
    interval = IntegerField(field_uri='Interval', min=1, is_required=True)

    def __str__(self):
        return 'Regenerates every %s day(s)' % self.interval


class Boundary(EWSElement, metaclass=EWSMeta):
    """Base class for all classes implementing recurring boundary elements."""


class NoEndPattern(Boundary):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/noendrecurrence"""

    ELEMENT_NAME = 'NoEndRecurrence'

    # Start date, as EWSDate or EWSDateTime
    start = DateOrDateTimeField(field_uri='StartDate', is_required=True)

    def __str__(self):
        return 'Starts on %s' % self.start


class EndDatePattern(Boundary):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/enddaterecurrence"""

    ELEMENT_NAME = 'EndDateRecurrence'

    # Start date, as EWSDate or EWSDateTime
    start = DateOrDateTimeField(field_uri='StartDate', is_required=True)
    # End date, as EWSDate
    end = DateOrDateTimeField(field_uri='EndDate', is_required=True)

    def __str__(self):
        return 'Starts on %s, ends on %s' % (self.start, self.end)


class NumberedPattern(Boundary):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/numberedrecurrence"""

    ELEMENT_NAME = 'NumberedRecurrence'

    # Start date, as EWSDate or EWSDateTime
    start = DateOrDateTimeField(field_uri='StartDate', is_required=True)
    # The number of occurrences in this pattern, in range 1 -> 999
    number = IntegerField(field_uri='NumberOfOccurrences', min=1, max=999, is_required=True)

    def __str__(self):
        return 'Starts on %s and occurs %s times' % (self.start, self.number)


class Occurrence(IdChangeKeyMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/occurrence"""

    ELEMENT_NAME = 'Occurrence'
    ID_ELEMENT_CLS = ItemId

    _id = IdElementField(field_uri='ItemId', value_cls=ID_ELEMENT_CLS)
    # The modified start time of the item, as EWSDateTime
    start = DateTimeField(field_uri='Start')
    # The modified end time of the item, as EWSDateTime
    end = DateTimeField(field_uri='End')
    # The original start time of the item, as EWSDateTime
    original_start = DateTimeField(field_uri='OriginalStart')


# Container elements:
# 'ModifiedOccurrences'
# 'DeletedOccurrences'


class FirstOccurrence(Occurrence):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/firstoccurrence"""

    ELEMENT_NAME = 'FirstOccurrence'


class LastOccurrence(Occurrence):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/lastoccurrence"""

    ELEMENT_NAME = 'LastOccurrence'


class DeletedOccurrence(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deletedoccurrence"""

    ELEMENT_NAME = 'DeletedOccurrence'

    # The modified start time of the item, as EWSDateTime
    start = DateTimeField(field_uri='Start')


PATTERN_CLASSES = AbsoluteYearlyPattern, RelativeYearlyPattern, AbsoluteMonthlyPattern, RelativeMonthlyPattern, \
                   WeeklyPattern, DailyPattern
REGENERATION_CLASSES = YearlyRegeneration, MonthlyRegeneration, WeeklyRegeneration, DailyRegeneration
BOUNDARY_CLASSES = NoEndPattern, EndDatePattern, NumberedPattern


class Recurrence(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/recurrence-recurrencetype
    """

    ELEMENT_NAME = 'Recurrence'
    PATTERN_CLASSES = PATTERN_CLASSES

    pattern = EWSElementField(value_cls=Pattern)
    boundary = EWSElementField(value_cls=Boundary)

    def __init__(self, **kwargs):
        # Allow specifying a start, end and/or number as a shortcut to creating a boundary
        start = kwargs.pop('start', None)
        end = kwargs.pop('end', None)
        number = kwargs.pop('number', None)
        if any([start, end, number]):
            if 'boundary' in kwargs:
                raise ValueError("'boundary' is not allowed in combination with 'start', 'end' or 'number'")
            if start and not end and not number:
                kwargs['boundary'] = NoEndPattern(start=start)
            elif start and end and not number:
                kwargs['boundary'] = EndDatePattern(start=start, end=end)
            elif start and number and not end:
                kwargs['boundary'] = NumberedPattern(start=start, number=number)
            else:
                raise ValueError("Unsupported 'start', 'end', 'number' combination")
        super().__init__(**kwargs)

    @classmethod
    def from_xml(cls, elem, account):
        for pattern_cls in cls.PATTERN_CLASSES:
            pattern_elem = elem.find(pattern_cls.response_tag())
            if pattern_elem is None:
                continue
            pattern = pattern_cls.from_xml(elem=pattern_elem, account=account)
            break
        else:
            pattern = None
        for boundary_cls in BOUNDARY_CLASSES:
            boundary_elem = elem.find(boundary_cls.response_tag())
            if boundary_elem is None:
                continue
            boundary = boundary_cls.from_xml(elem=boundary_elem, account=account)
            break
        else:
            boundary = None
        return cls(pattern=pattern, boundary=boundary)

    def __str__(self):
        return 'Pattern: %s, Boundary: %s' % (self.pattern, self.boundary)


class TaskRecurrence(Recurrence):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/recurrence-taskrecurrencetype
    """

    PATTERN_CLASSES = PATTERN_CLASSES + REGENERATION_CLASSES

Classes

class AbsoluteMonthlyPattern (**kwargs)
Expand source code
class AbsoluteMonthlyPattern(Pattern):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/absolutemonthlyrecurrence
    """

    ELEMENT_NAME = 'AbsoluteMonthlyRecurrence'

    # Interval, in months, in range 1 -> 99
    interval = IntegerField(field_uri='Interval', min=1, max=99, is_required=True)
    # The day of month of an occurrence, in range 1 -> 31. If a particular month has less days than the day_of_month
    # value, the last day in the month is assumed
    day_of_month = IntegerField(field_uri='DayOfMonth', min=1, max=31, is_required=True)

    def __str__(self):
        return 'Occurs on day %s of every %s month(s)' % (self.day_of_month, self.interval)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var day_of_month
var interval

Inherited members

class AbsoluteYearlyPattern (**kwargs)
Expand source code
class AbsoluteYearlyPattern(Pattern):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/absoluteyearlyrecurrence
    """

    ELEMENT_NAME = 'AbsoluteYearlyRecurrence'

    # The day of month of an occurrence, in range 1 -> 31. If a particular month has less days than the day_of_month
    # value, the last day in the month is assumed
    day_of_month = IntegerField(field_uri='DayOfMonth', min=1, max=31, is_required=True)
    # The month of the year, from 1 - 12
    month = EnumField(field_uri='Month', enum=MONTHS, is_required=True)

    def __str__(self):
        return 'Occurs on day %s of %s' % (self.day_of_month, _month_to_str(self.month))

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var day_of_month
var month

Inherited members

class Boundary (**kwargs)

Base class for all classes implementing recurring boundary elements.

Expand source code
class Boundary(EWSElement, metaclass=EWSMeta):
    """Base class for all classes implementing recurring boundary elements."""

Ancestors

Subclasses

Inherited members

class DailyPattern (**kwargs)
Expand source code
class DailyPattern(Pattern):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/dailyrecurrence"""

    ELEMENT_NAME = 'DailyRecurrence'

    # Interval, in days, in range 1 -> 999
    interval = IntegerField(field_uri='Interval', min=1, max=999, is_required=True)

    def __str__(self):
        return 'Occurs every %s day(s)' % self.interval

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var interval

Inherited members

class DailyRegeneration (**kwargs)
Expand source code
class DailyRegeneration(Regeneration):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/dailyregeneration"""

    ELEMENT_NAME = 'DailyRegeneration'

    # Interval, in days
    interval = IntegerField(field_uri='Interval', min=1, is_required=True)

    def __str__(self):
        return 'Regenerates every %s day(s)' % self.interval

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var interval

Inherited members

class DeletedOccurrence (**kwargs)
Expand source code
class DeletedOccurrence(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deletedoccurrence"""

    ELEMENT_NAME = 'DeletedOccurrence'

    # The modified start time of the item, as EWSDateTime
    start = DateTimeField(field_uri='Start')

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var start

Inherited members

class EndDatePattern (**kwargs)
Expand source code
class EndDatePattern(Boundary):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/enddaterecurrence"""

    ELEMENT_NAME = 'EndDateRecurrence'

    # Start date, as EWSDate or EWSDateTime
    start = DateOrDateTimeField(field_uri='StartDate', is_required=True)
    # End date, as EWSDate
    end = DateOrDateTimeField(field_uri='EndDate', is_required=True)

    def __str__(self):
        return 'Starts on %s, ends on %s' % (self.start, self.end)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var end
var start

Inherited members

class FirstOccurrence (**kwargs)
Expand source code
class FirstOccurrence(Occurrence):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/firstoccurrence"""

    ELEMENT_NAME = 'FirstOccurrence'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class LastOccurrence (**kwargs)
Expand source code
class LastOccurrence(Occurrence):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/lastoccurrence"""

    ELEMENT_NAME = 'LastOccurrence'

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class MonthlyRegeneration (**kwargs)
Expand source code
class MonthlyRegeneration(Regeneration):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/monthlyregeneration"""

    ELEMENT_NAME = 'MonthlyRegeneration'

    # Interval, in months
    interval = IntegerField(field_uri='Interval', min=1, is_required=True)

    def __str__(self):
        return 'Regenerates every %s month(s)' % self.interval

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var interval

Inherited members

class NoEndPattern (**kwargs)
Expand source code
class NoEndPattern(Boundary):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/noendrecurrence"""

    ELEMENT_NAME = 'NoEndRecurrence'

    # Start date, as EWSDate or EWSDateTime
    start = DateOrDateTimeField(field_uri='StartDate', is_required=True)

    def __str__(self):
        return 'Starts on %s' % self.start

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var start

Inherited members

class NumberedPattern (**kwargs)
Expand source code
class NumberedPattern(Boundary):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/numberedrecurrence"""

    ELEMENT_NAME = 'NumberedRecurrence'

    # Start date, as EWSDate or EWSDateTime
    start = DateOrDateTimeField(field_uri='StartDate', is_required=True)
    # The number of occurrences in this pattern, in range 1 -> 999
    number = IntegerField(field_uri='NumberOfOccurrences', min=1, max=999, is_required=True)

    def __str__(self):
        return 'Starts on %s and occurs %s times' % (self.start, self.number)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var number
var start

Inherited members

class Occurrence (**kwargs)
Expand source code
class Occurrence(IdChangeKeyMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/occurrence"""

    ELEMENT_NAME = 'Occurrence'
    ID_ELEMENT_CLS = ItemId

    _id = IdElementField(field_uri='ItemId', value_cls=ID_ELEMENT_CLS)
    # The modified start time of the item, as EWSDateTime
    start = DateTimeField(field_uri='Start')
    # The modified end time of the item, as EWSDateTime
    end = DateTimeField(field_uri='End')
    # The original start time of the item, as EWSDateTime
    original_start = DateTimeField(field_uri='OriginalStart')

Ancestors

Subclasses

Class variables

var ELEMENT_NAME
var FIELDS
var ID_ELEMENT_CLS

'id' and 'changekey' are UUIDs generated by Exchange.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/itemid

Instance variables

var end
var original_start
var start

Inherited members

class Pattern (**kwargs)

Base class for all classes implementing recurring pattern elements.

Expand source code
class Pattern(EWSElement, metaclass=EWSMeta):
    """Base class for all classes implementing recurring pattern elements."""

Ancestors

Subclasses

Inherited members

class Recurrence (**kwargs)
Expand source code
class Recurrence(EWSElement):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/recurrence-recurrencetype
    """

    ELEMENT_NAME = 'Recurrence'
    PATTERN_CLASSES = PATTERN_CLASSES

    pattern = EWSElementField(value_cls=Pattern)
    boundary = EWSElementField(value_cls=Boundary)

    def __init__(self, **kwargs):
        # Allow specifying a start, end and/or number as a shortcut to creating a boundary
        start = kwargs.pop('start', None)
        end = kwargs.pop('end', None)
        number = kwargs.pop('number', None)
        if any([start, end, number]):
            if 'boundary' in kwargs:
                raise ValueError("'boundary' is not allowed in combination with 'start', 'end' or 'number'")
            if start and not end and not number:
                kwargs['boundary'] = NoEndPattern(start=start)
            elif start and end and not number:
                kwargs['boundary'] = EndDatePattern(start=start, end=end)
            elif start and number and not end:
                kwargs['boundary'] = NumberedPattern(start=start, number=number)
            else:
                raise ValueError("Unsupported 'start', 'end', 'number' combination")
        super().__init__(**kwargs)

    @classmethod
    def from_xml(cls, elem, account):
        for pattern_cls in cls.PATTERN_CLASSES:
            pattern_elem = elem.find(pattern_cls.response_tag())
            if pattern_elem is None:
                continue
            pattern = pattern_cls.from_xml(elem=pattern_elem, account=account)
            break
        else:
            pattern = None
        for boundary_cls in BOUNDARY_CLASSES:
            boundary_elem = elem.find(boundary_cls.response_tag())
            if boundary_elem is None:
                continue
            boundary = boundary_cls.from_xml(elem=boundary_elem, account=account)
            break
        else:
            boundary = None
        return cls(pattern=pattern, boundary=boundary)

    def __str__(self):
        return 'Pattern: %s, Boundary: %s' % (self.pattern, self.boundary)

Ancestors

Subclasses

Class variables

var ELEMENT_NAME
var FIELDS
var PATTERN_CLASSES

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    for pattern_cls in cls.PATTERN_CLASSES:
        pattern_elem = elem.find(pattern_cls.response_tag())
        if pattern_elem is None:
            continue
        pattern = pattern_cls.from_xml(elem=pattern_elem, account=account)
        break
    else:
        pattern = None
    for boundary_cls in BOUNDARY_CLASSES:
        boundary_elem = elem.find(boundary_cls.response_tag())
        if boundary_elem is None:
            continue
        boundary = boundary_cls.from_xml(elem=boundary_elem, account=account)
        break
    else:
        boundary = None
    return cls(pattern=pattern, boundary=boundary)

Instance variables

var boundary
var pattern

Inherited members

class Regeneration (**kwargs)

Base class for all classes implementing recurring regeneration elements.

Expand source code
class Regeneration(Pattern, metaclass=EWSMeta):
    """Base class for all classes implementing recurring regeneration elements."""

Ancestors

Subclasses

Inherited members

class RelativeMonthlyPattern (**kwargs)
Expand source code
class RelativeMonthlyPattern(Pattern):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/relativemonthlyrecurrence
    """

    ELEMENT_NAME = 'RelativeMonthlyRecurrence'

    # Interval, in months, in range 1 -> 99
    interval = IntegerField(field_uri='Interval', min=1, max=99, is_required=True)
    # The weekday of the occurrence, as a valid ISO 8601 weekday number in range 1 -> 7 (1 being Monday).
    # Alternatively, the weekday can be one of the DAY (or 8), WEEK_DAY (or 9) or WEEKEND_DAY (or 10) consts which
    # is interpreted as the first day, weekday, or weekend day in the month, respectively.
    weekday = EnumField(field_uri='DaysOfWeek', enum=WEEKDAYS, is_required=True)
    # Week number of the month, in range 1 -> 5. If 5 is specified, this assumes the last week of the month for
    # months that have only 4 weeks.
    week_number = EnumField(field_uri='DayOfWeekIndex', enum=WEEK_NUMBERS, is_required=True)

    def __str__(self):
        return 'Occurs on weekday %s in the %s week of every %s month(s)' % (
            _weekday_to_str(self.weekday),
            _week_number_to_str(self.week_number),
            self.interval
        )

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var interval
var week_number
var weekday

Inherited members

class RelativeYearlyPattern (**kwargs)
Expand source code
class RelativeYearlyPattern(Pattern):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/relativeyearlyrecurrence
    """

    ELEMENT_NAME = 'RelativeYearlyRecurrence'

    # The weekday of the occurrence, as a valid ISO 8601 weekday number in range 1 -> 7 (1 being Monday).
    # Alternatively, the weekday can be one of the DAY (or 8), WEEK_DAY (or 9) or WEEKEND_DAY (or 10) consts which
    # is interpreted as the first day, weekday, or weekend day in the month, respectively.
    weekday = EnumField(field_uri='DaysOfWeek', enum=WEEKDAYS, is_required=True)
    # Week number of the month, in range 1 -> 5. If 5 is specified, this assumes the last week of the month for
    # months that have only 4 weeks
    week_number = EnumField(field_uri='DayOfWeekIndex', enum=WEEK_NUMBERS, is_required=True)
    # The month of the year, from 1 - 12
    month = EnumField(field_uri='Month', enum=MONTHS, is_required=True)

    def __str__(self):
        return 'Occurs on weekday %s in the %s week of %s' % (
            _weekday_to_str(self.weekday),
            _week_number_to_str(self.week_number),
            _month_to_str(self.month)
        )

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var month
var week_number
var weekday

Inherited members

class TaskRecurrence (**kwargs)
Expand source code
class TaskRecurrence(Recurrence):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/recurrence-taskrecurrencetype
    """

    PATTERN_CLASSES = PATTERN_CLASSES + REGENERATION_CLASSES

Ancestors

Class variables

var PATTERN_CLASSES

Inherited members

class WeeklyPattern (**kwargs)
Expand source code
class WeeklyPattern(Pattern):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/weeklyrecurrence"""

    ELEMENT_NAME = 'WeeklyRecurrence'

    # Interval, in weeks, in range 1 -> 99
    interval = IntegerField(field_uri='Interval', min=1, max=99, is_required=True)
    # List of valid ISO 8601 weekdays, as list of numbers in range 1 -> 7 (1 being Monday)
    weekdays = EnumListField(field_uri='DaysOfWeek', enum=WEEKDAYS, is_required=True)
    # The first day of the week. Defaults to Monday
    first_day_of_week = EnumField(field_uri='FirstDayOfWeek', enum=WEEKDAYS, default=1, is_required=True)

    def __str__(self):
        if isinstance(self.weekdays, str):
            weekdays = [self.weekdays]
        elif isinstance(self.weekdays, int):
            weekdays = [_weekday_to_str(self.weekdays)]
        else:
            weekdays = [_weekday_to_str(i) for i in self.weekdays]
        return 'Occurs on weekdays %s of every %s week(s) where the first day of the week is %s' % (
            ', '.join(weekdays), self.interval, _weekday_to_str(self.first_day_of_week)
        )

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var first_day_of_week
var interval
var weekdays

Inherited members

class WeeklyRegeneration (**kwargs)
Expand source code
class WeeklyRegeneration(Regeneration):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/weeklyregeneration"""

    ELEMENT_NAME = 'WeeklyRegeneration'

    # Interval, in weeks
    interval = IntegerField(field_uri='Interval', min=1, is_required=True)

    def __str__(self):
        return 'Regenerates every %s week(s)' % self.interval

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var interval

Inherited members

class YearlyRegeneration (**kwargs)
Expand source code
class YearlyRegeneration(Regeneration):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/yearlyregeneration"""

    ELEMENT_NAME = 'YearlyRegeneration'

    # Interval, in years
    interval = IntegerField(field_uri='Interval', min=1, is_required=True)

    def __str__(self):
        return 'Regenerates every %s year(s)' % self.interval

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var interval

Inherited members

exchangelib-4.6.1/docs/exchangelib/restriction.html000066400000000000000000002467431414601472700224550ustar00rootroot00000000000000 exchangelib.restriction API documentation

Module exchangelib.restriction

Expand source code
import logging
from collections import OrderedDict
from copy import copy

from .fields import InvalidField, FieldPath, DateTimeBackedDateField
from .util import create_element, xml_to_str, value_to_xml_text, is_iterable
from .version import EXCHANGE_2010

log = logging.getLogger(__name__)


class Q:
    """A class with an API similar to Django Q objects. Used to implemnt advanced filtering logic."""

    # Connection types
    AND = 'AND'
    OR = 'OR'
    NOT = 'NOT'
    NEVER = 'NEVER'  # This is not specified by EWS. We use it for queries that will never match, e.g. 'foo__in=()'
    CONN_TYPES = {AND, OR, NOT, NEVER}

    # EWS Operators
    EQ = '=='
    NE = '!='
    GT = '>'
    GTE = '>='
    LT = '<'
    LTE = '<='
    EXACT = 'exact'
    IEXACT = 'iexact'
    CONTAINS = 'contains'
    ICONTAINS = 'icontains'
    STARTSWITH = 'startswith'
    ISTARTSWITH = 'istartswith'
    EXISTS = 'exists'
    OP_TYPES = {EQ, NE, GT, GTE, LT, LTE, EXACT, IEXACT, CONTAINS, ICONTAINS, STARTSWITH, ISTARTSWITH, EXISTS}
    CONTAINS_OPS = {EXACT, IEXACT, CONTAINS, ICONTAINS, STARTSWITH, ISTARTSWITH}

    # Valid lookups
    LOOKUP_RANGE = 'range'
    LOOKUP_IN = 'in'
    LOOKUP_NOT = 'not'
    LOOKUP_GT = 'gt'
    LOOKUP_GTE = 'gte'
    LOOKUP_LT = 'lt'
    LOOKUP_LTE = 'lte'
    LOOKUP_EXACT = 'exact'
    LOOKUP_IEXACT = 'iexact'
    LOOKUP_CONTAINS = 'contains'
    LOOKUP_ICONTAINS = 'icontains'
    LOOKUP_STARTSWITH = 'startswith'
    LOOKUP_ISTARTSWITH = 'istartswith'
    LOOKUP_EXISTS = 'exists'
    LOOKUP_TYPES = {LOOKUP_RANGE, LOOKUP_IN, LOOKUP_NOT, LOOKUP_GT, LOOKUP_GTE, LOOKUP_LT, LOOKUP_LTE, LOOKUP_EXACT,
                    LOOKUP_IEXACT, LOOKUP_CONTAINS, LOOKUP_ICONTAINS, LOOKUP_STARTSWITH, LOOKUP_ISTARTSWITH,
                    LOOKUP_EXISTS}

    __slots__ = 'conn_type', 'field_path', 'op', 'value', 'children', 'query_string'

    def __init__(self, *args, **kwargs):
        self.conn_type = kwargs.pop('conn_type', self.AND)

        self.field_path = None  # Name of the field we want to filter on
        self.op = None
        self.value = None
        self.query_string = None

        # Parsing of args and kwargs may require child elements
        self.children = []

        # Check for query string as the only argument
        if not kwargs and len(args) == 1 and isinstance(args[0], str):
            self.query_string = args[0]
            args = ()

        # Parse args which must now be Q objects
        for q in args:
            if not isinstance(q, self.__class__):
                raise ValueError("Non-keyword arg %r must be a Q instance" % q)
        self.children.extend(args)

        # Parse keyword args and extract the filter
        is_single_kwarg = len(args) == 0 and len(kwargs) == 1
        for key, value in kwargs.items():
            self.children.extend(
                self._get_children_from_kwarg(key=key, value=value, is_single_kwarg=is_single_kwarg)
            )

        # Simplify this object
        self.reduce()

        # Final sanity check
        self._check_integrity()

    def _get_children_from_kwarg(self, key, value, is_single_kwarg=False):
        """Generate Q objects corresponding to a single keyword argument. Make this a leaf if there are no children to
        generate.
        """
        key_parts = key.rsplit('__', 1)
        if len(key_parts) == 2 and key_parts[1] in self.LOOKUP_TYPES:
            # This is a kwarg with a lookup at the end
            field_path, lookup = key_parts
            if lookup == self.LOOKUP_EXISTS:
                # value=True will fall through to further processing
                if not value:
                    return [~self.__class__(**{key: True})]

            if lookup == self.LOOKUP_RANGE:
                # EWS doesn't have a 'range' operator. Emulate 'foo__range=(1, 2)' as 'foo__gte=1 and foo__lte=2'
                # (both values inclusive).
                if len(value) != 2:
                    raise ValueError("Value of lookup '%s' must have exactly 2 elements" % key)
                return [
                    self.__class__(**{'%s__gte' % field_path: value[0]}),
                    self.__class__(**{'%s__lte' % field_path: value[1]}),
                ]

            # Filtering on list types is a bit quirky. The only lookup type I have found to work is:
            #
            #     item:Categories == 'foo' AND item:Categories == 'bar' AND ...
            #
            #     item:Categories == 'foo' OR item:Categories == 'bar' OR ...
            #
            # The former returns items that have all these categories, but maybe also others. The latter returns
            # items that have at least one of these categories. This translates to the 'contains' and 'in' lookups,
            # respectively. Both versions are case-insensitive.
            #
            # Exact matching and case-sensitive or partial-string matching is not possible since that requires the
            # 'Contains' element which only supports matching on string elements, not arrays.
            #
            # Exact matching of categories (i.e. match ['a', 'b'] but not ['a', 'b', 'c']) could be implemented by
            # post-processing items by fetch the categories field unconditionally and removing the items that don't
            # have an exact match.
            if lookup == self.LOOKUP_IN:
                # EWS doesn't have an '__in' operator. Allow '__in' lookups on list and non-list field types,
                # specifying a list value. We'll emulate it as a set of OR'ed exact matches.
                if not is_iterable(value, generators_allowed=True):
                    raise ValueError("Value for lookup %r must be a list" % key)
                children = [self.__class__(**{field_path: v}) for v in value]
                if not children:
                    # This is an '__in' operator with an empty list as the value. We interpret it to mean "is foo
                    # contained in the empty set?" which is always false. Mark this Q object as such.
                    return [self.__class__(conn_type=self.NEVER)]
                return [self.__class__(*children, conn_type=self.OR)]

            if lookup == self.LOOKUP_CONTAINS and is_iterable(value, generators_allowed=True):
                # A '__contains' lookup with an list as the value ony makes sense for list fields, since exact match
                # on multiple distinct values will always fail for single-value fields.
                #
                # An empty list as value is allowed. We interpret it to mean "are all values in the empty set contained
                # in foo?" which is always true.
                children = [self.__class__(**{field_path: v}) for v in value]
                return [self.__class__(*children, conn_type=self.AND)]

            try:
                op = self._lookup_to_op(lookup)
            except KeyError:
                raise ValueError("Lookup '%s' is not supported (called as '%s=%r')" % (lookup, key, value))
        else:
            field_path, op = key, self.EQ

        if not is_single_kwarg:
            return [self.__class__(**{key: value})]

        # This is a single-kwarg Q object with a lookup that requires a single value. Make this a leaf
        self.field_path = field_path
        self.op = op
        self.value = value
        return []

    def reduce(self):
        """Simplify this object, if possible."""
        self._reduce_children()
        self._promote()

    def _reduce_children(self):
        """Look at the children of this object and remove unnecessary items."""
        children = self.children
        if any((isinstance(a, self.__class__) and a.is_never()) for a in children):
            # We have at least one 'never' arg
            if self.conn_type == self.AND:
                # Remove all other args since nothing we AND together with a 'never' arg can change the result
                children = [self.__class__(conn_type=self.NEVER)]
            elif self.conn_type == self.OR:
                # Remove all 'never' args because all other args will decide the result. Keep one 'never' arg in case
                # all args are 'never' args.
                children = [a for a in children if not (isinstance(a, self.__class__) and a.is_never())]
                if not children:
                    children = [self.__class__(conn_type=self.NEVER)]
            elif self.conn_type == self.NOT:
                # Let's interpret 'not never' to mean 'always'. Remove all 'never' args
                children = [a for a in children if not (isinstance(a, self.__class__) and a.is_never())]

        # Remove any empty Q elements in args before proceeding
        children = [a for a in children if not (isinstance(a, self.__class__) and a.is_empty())]
        self.children = children

    def _promote(self):
        """When we only have one child and no expression on ourselves, we are a no-op. Flatten by taking over the only
        child.
        """
        if len(self.children) != 1 or self.field_path is not None or self.conn_type == self.NOT:
            return

        q = self.children[0]
        self.conn_type = q.conn_type
        self.field_path = q.field_path
        self.op = q.op
        self.value = q.value
        self.query_string = q.query_string
        self.children = q.children

    def clean(self, version):
        """Do some basic checks on the attributes, using a generic folder. to_xml() does a really good job of
        validating. There's no reason to replicate much of that here.
        """
        from .folders import Folder
        self.to_xml(folders=[Folder()], version=version, applies_to=Restriction.ITEMS)

    @classmethod
    def _lookup_to_op(cls, lookup):
        return {
            cls.LOOKUP_NOT: cls.NE,
            cls.LOOKUP_GT: cls.GT,
            cls.LOOKUP_GTE: cls.GTE,
            cls.LOOKUP_LT: cls.LT,
            cls.LOOKUP_LTE: cls.LTE,
            cls.LOOKUP_EXACT: cls.EXACT,
            cls.LOOKUP_IEXACT: cls.IEXACT,
            cls.LOOKUP_CONTAINS: cls.CONTAINS,
            cls.LOOKUP_ICONTAINS: cls.ICONTAINS,
            cls.LOOKUP_STARTSWITH: cls.STARTSWITH,
            cls.LOOKUP_ISTARTSWITH: cls.ISTARTSWITH,
            cls.LOOKUP_EXISTS: cls.EXISTS,
        }[lookup]

    @classmethod
    def _conn_to_xml(cls, conn_type):
        xml_tag_map = {
            cls.AND: 't:And',
            cls.OR: 't:Or',
            cls.NOT: 't:Not',
        }
        return create_element(xml_tag_map[conn_type])

    @classmethod
    def _op_to_xml(cls, op):
        xml_tag_map = {
            cls.EQ: 't:IsEqualTo',
            cls.NE: 't:IsNotEqualTo',
            cls.GTE: 't:IsGreaterThanOrEqualTo',
            cls.LTE: 't:IsLessThanOrEqualTo',
            cls.LT: 't:IsLessThan',
            cls.GT: 't:IsGreaterThan',
            cls.EXISTS: 't:Exists',
        }
        if op in xml_tag_map:
            return create_element(xml_tag_map[op])
        valid_ops = cls.EXACT, cls.IEXACT, cls.CONTAINS, cls.ICONTAINS, cls.STARTSWITH, cls.ISTARTSWITH
        if op not in valid_ops:
            raise ValueError("'op' %s must be one of %s" % (op, valid_ops))

        # For description of Contains attribute values, see
        #     https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/contains
        #
        # Possible ContainmentMode values:
        #     FullString, Prefixed, Substring, PrefixOnWords, ExactPhrase
        # Django lookups have no equivalent of PrefixOnWords and ExactPhrase (and I'm unsure how they actually
        # work).
        #
        # EWS has no equivalent of '__endswith' or '__iendswith'. That could be emulated using '__contains' and
        # '__icontains' and filtering results afterwards in Python. But it could be inefficient because we might be
        # fetching and discarding a lot of non-matching items, plus we would need to always fetch the field we're
        # matching on, to be able to do the filtering. I think it's better to leave this to the consumer, i.e.:
        #
        # items = [i for i in fld.filter(subject__contains=suffix) if i.subject.endswith(suffix)]
        # items = [i for i in fld.filter(subject__icontains=suffix) if i.subject.lower().endswith(suffix.lower())]
        #
        # Possible ContainmentComparison values (there are more, but the rest are "To be removed"):
        #     Exact, IgnoreCase, IgnoreNonSpacingCharacters, IgnoreCaseAndNonSpacingCharacters
        # I'm unsure about non-spacing characters, but as I read
        #    https://en.wikipedia.org/wiki/Graphic_character#Spacing_and_non-spacing_characters
        # we shouldn't ignore them ('a' would match both 'a' and 'å', the latter having a non-spacing character).
        if op in {cls.EXACT, cls.IEXACT}:
            match_mode = 'FullString'
        elif op in (cls.CONTAINS, cls.ICONTAINS):
            match_mode = 'Substring'
        elif op in (cls.STARTSWITH, cls.ISTARTSWITH):
            match_mode = 'Prefixed'
        else:
            raise ValueError('Unsupported op: %s' % op)
        if op in (cls.IEXACT, cls.ICONTAINS, cls.ISTARTSWITH):
            compare_mode = 'IgnoreCase'
        else:
            compare_mode = 'Exact'
        return create_element(
            't:Contains',
            attrs=OrderedDict([
                ('ContainmentMode', match_mode),
                ('ContainmentComparison', compare_mode),
            ])
        )

    def is_leaf(self):
        return not self.children

    def is_empty(self):
        """Return True if this object is without any restrictions at all."""
        return self.is_leaf() and self.field_path is None and self.query_string is None and self.conn_type != self.NEVER

    def is_never(self):
        """Return True if this object has a restriction that will never match anything."""
        return self.conn_type == self.NEVER

    def expr(self):
        if self.is_empty():
            return None
        if self.is_never():
            return self.NEVER
        if self.query_string:
            return self.query_string
        if self.is_leaf():
            expr = '%s %s %r' % (self.field_path, self.op, self.value)
        else:
            # Sort children by field name so we get stable output (for easier testing). Children should never be empty.
            expr = (' %s ' % (self.AND if self.conn_type == self.NOT else self.conn_type)).join(
                (c.expr() if c.is_leaf() or c.conn_type == self.NOT else '(%s)' % c.expr())
                for c in sorted(self.children, key=lambda i: i.field_path or '')
            )
        if self.conn_type == self.NOT:
            # Add the NOT operator. Put children in parens if there is more than one child.
            if self.is_leaf() or len(self.children) == 1:
                return self.conn_type + ' %s' % expr
            return self.conn_type + ' (%s)' % expr
        return expr

    def to_xml(self, folders, version, applies_to):
        if self.query_string:
            self._check_integrity()
            if version.build < EXCHANGE_2010:
                raise NotImplementedError('QueryString filtering is only supported for Exchange 2010 servers and later')
            elem = create_element('m:QueryString')
            elem.text = self.query_string
            return elem
        # Translate this Q object to a valid Restriction XML tree
        elem = self.xml_elem(folders=folders, version=version, applies_to=applies_to)
        if elem is None:
            return None
        restriction = create_element('m:Restriction')
        restriction.append(elem)
        return restriction

    def _check_integrity(self):
        if self.is_empty():
            return
        if self.conn_type == self.NEVER:
            if any([self.field_path, self.op, self.value, self.children]):
                raise ValueError("'never' queries cannot be combined with other settings")
            return
        if self.query_string:
            if any([self.field_path, self.op, self.value, self.children]):
                raise ValueError('Query strings cannot be combined with other settings')
            return
        if self.conn_type not in self.CONN_TYPES:
            raise ValueError("'conn_type' %s must be one of %s" % (self.conn_type, self.CONN_TYPES))
        if not self.is_leaf():
            for q in self.children:
                if q.query_string and len(self.children) > 1:
                    raise ValueError(
                        'A query string cannot be combined with other restrictions'
                    )
            return
        if not self.field_path:
            raise ValueError("'field_path' must be set")
        if self.op not in self.OP_TYPES:
            raise ValueError("'op' %s must be one of %s" % (self.op, self.OP_TYPES))
        if self.op == self.EXISTS and self.value is not True:
            raise ValueError("'value' must be True when operator is EXISTS")
        if self.value is None:
            raise ValueError('Value for filter on field path "%s" cannot be None' % self.field_path)
        if is_iterable(self.value, generators_allowed=True):
            raise ValueError(
                'Value %r for filter on field path "%s" must be a single value' % (self.value, self.field_path)
            )

    def _validate_field_path(self, field_path, folder, applies_to, version):
        from .indexed_properties import MultiFieldIndexedElement
        if applies_to == Restriction.FOLDERS:
            # This is a restriction on Folder fields
            folder.validate_field(field=field_path.field, version=version)
        else:
            folder.validate_item_field(field=field_path.field, version=version)
        if not field_path.field.is_searchable:
            raise ValueError("EWS does not support filtering on field '%s'" % field_path.field.name)
        if field_path.subfield and not field_path.subfield.is_searchable:
            raise ValueError("EWS does not support filtering on subfield '%s'" % field_path.subfield.name)
        if issubclass(field_path.field.value_cls, MultiFieldIndexedElement) and not field_path.subfield:
            raise ValueError("Field path '%s' must contain a subfield" % self.field_path)

    def _get_field_path(self, folders, applies_to, version):
        # Convert the string field path to a real FieldPath object. The path is validated using the given folders.
        for folder in folders:
            try:
                if applies_to == Restriction.FOLDERS:
                    # This is a restriction on Folder fields
                    field = folder.get_field_by_fieldname(fieldname=self.field_path)
                    field_path = FieldPath(field=field)
                else:
                    field_path = FieldPath.from_string(field_path=self.field_path, folder=folder)
            except ValueError:
                continue
            self._validate_field_path(field_path=field_path, folder=folder, applies_to=applies_to, version=version)
            break
        else:
            raise InvalidField("Unknown field path %r on folders %s" % (self.field_path, folders))
        return field_path

    def _get_clean_value(self, field_path, version):
        if self.op == self.EXISTS:
            return None
        clean_field = field_path.subfield if (field_path.subfield and field_path.label) else field_path.field
        if clean_field.is_list:
            # __contains and __in are implemented as multiple leaves, with one value per leaf. clean() on list fields
            # only works on lists, so clean a one-element list.
            return clean_field.clean(value=[self.value], version=version)[0]
        return clean_field.clean(value=self.value, version=version)

    def xml_elem(self, folders, version, applies_to):
        # Recursively build an XML tree structure of this Q object. If this is an empty leaf (the equivalent of Q()),
        # return None.
        from .indexed_properties import SingleFieldIndexedElement
        # Don't check self.value just yet. We want to return error messages on the field path first, and then the value.
        # This is done in _get_field_path() and _get_clean_value(), respectively.
        self._check_integrity()
        if self.is_empty():
            return None
        if self.is_never():
            raise ValueError("EWS does not support 'never' queries")
        if self.is_leaf():
            elem = self._op_to_xml(self.op)
            field_path = self._get_field_path(folders, applies_to=applies_to, version=version)
            clean_value = self._get_clean_value(field_path=field_path, version=version)
            if issubclass(field_path.field.value_cls, SingleFieldIndexedElement) and not field_path.label:
                # We allow a filter shortcut of e.g. email_addresses__contains=EmailAddress(label='Foo', ...) instead of
                # email_addresses__Foo_email_address=.... Set FieldPath label now so we can generate the field_uri.
                field_path.label = clean_value.label
            elif isinstance(field_path.field, DateTimeBackedDateField):
                # We need to convert to datetime
                clean_value = field_path.field.date_to_datetime(clean_value)
            elem.append(field_path.to_xml())
            constant = create_element('t:Constant')
            if self.op != self.EXISTS:
                # Use .set() to not fill up the create_element() cache with unique values
                constant.set('Value', value_to_xml_text(clean_value))
                if self.op in self.CONTAINS_OPS:
                    elem.append(constant)
                else:
                    uriorconst = create_element('t:FieldURIOrConstant')
                    uriorconst.append(constant)
                    elem.append(uriorconst)
        elif len(self.children) == 1:
            # We have only one child
            elem = self.children[0].xml_elem(folders=folders, version=version, applies_to=applies_to)
        else:
            # We have multiple children. If conn_type is NOT, then group children with AND. We'll add the NOT later
            elem = self._conn_to_xml(self.AND if self.conn_type == self.NOT else self.conn_type)
            # Sort children by field name so we get stable output (for easier testing). Children should never be empty
            for c in sorted(self.children, key=lambda i: i.field_path or ''):
                elem.append(c.xml_elem(folders=folders, version=version, applies_to=applies_to))
        if elem is None:
            return None  # Should not be necessary, but play safe
        if self.conn_type == self.NOT:
            # Encapsulate everything in the NOT element
            not_elem = self._conn_to_xml(self.conn_type)
            not_elem.append(elem)
            return not_elem
        return elem

    def __and__(self, other):
        # & operator. Return a new Q with two children and conn_type AND
        return self.__class__(self, other, conn_type=self.AND)

    def __or__(self, other):
        # | operator. Return a new Q with two children and conn_type OR
        return self.__class__(self, other, conn_type=self.OR)

    def __invert__(self):
        # ~ operator. If op has an inverse, change op. Else return a new Q with conn_type NOT
        if self.conn_type == self.NOT:
            # This is NOT NOT. Change to AND
            new = copy(self)
            new.conn_type = self.AND
            new.reduce()
            return new
        if self.is_leaf():
            inverse_ops = {
                self.EQ: self.NE,
                self.NE: self.EQ,
                self.GT: self.LTE,
                self.GTE: self.LT,
                self.LT: self.GTE,
                self.LTE: self.GT,
            }
            try:
                new = copy(self)
                new.op = inverse_ops[self.op]
                new.reduce()
                return new
            except KeyError:
                pass
        return self.__class__(self, conn_type=self.NOT)

    def __eq__(self, other):
        return repr(self) == repr(other)

    def __hash__(self):
        return hash(repr(self))

    def __str__(self):
        return self.expr() or 'Q()'

    def __repr__(self):
        if self.is_leaf():
            if self.query_string:
                return self.__class__.__name__ + '(%r)' % self.query_string
            if self.is_never():
                return self.__class__.__name__ + '(conn_type=%r)' % (self.conn_type)
            return self.__class__.__name__ + '(%s %s %r)' % (self.field_path, self.op, self.value)
        sorted_children = tuple(sorted(self.children, key=lambda i: i.field_path or ''))
        if self.conn_type == self.NOT or len(self.children) > 1:
            return self.__class__.__name__ + repr((self.conn_type,) + sorted_children)
        return self.__class__.__name__ + repr(sorted_children)


class Restriction:
    """Implement an EWS Restriction type."""

    # The type of item the restriction applies to
    FOLDERS = 'folders'
    ITEMS = 'items'
    RESTRICTION_TYPES = (FOLDERS, ITEMS)

    def __init__(self, q, folders, applies_to):
        if not isinstance(q, Q):
            raise ValueError("'q' value %r must be a Q instance" % q)
        if q.is_empty():
            raise ValueError("Q object must not be empty")
        from .folders import BaseFolder
        for folder in folders:
            if not isinstance(folder, BaseFolder):
                raise ValueError("'folder' value %r must be a Folder instance" % folder)
        if applies_to not in self.RESTRICTION_TYPES:
            raise ValueError("'applies_to' must be one of %s" % (self.RESTRICTION_TYPES,))
        self.q = q
        self.folders = folders
        self.applies_to = applies_to

    def to_xml(self, version):
        return self.q.to_xml(folders=self.folders, version=version, applies_to=self.applies_to)

    def __str__(self):
        """Print the XML syntax tree."""
        return xml_to_str(self.to_xml(version=self.folders[0].account.version))

Classes

class Q (*args, **kwargs)

A class with an API similar to Django Q objects. Used to implemnt advanced filtering logic.

Expand source code
class Q:
    """A class with an API similar to Django Q objects. Used to implemnt advanced filtering logic."""

    # Connection types
    AND = 'AND'
    OR = 'OR'
    NOT = 'NOT'
    NEVER = 'NEVER'  # This is not specified by EWS. We use it for queries that will never match, e.g. 'foo__in=()'
    CONN_TYPES = {AND, OR, NOT, NEVER}

    # EWS Operators
    EQ = '=='
    NE = '!='
    GT = '>'
    GTE = '>='
    LT = '<'
    LTE = '<='
    EXACT = 'exact'
    IEXACT = 'iexact'
    CONTAINS = 'contains'
    ICONTAINS = 'icontains'
    STARTSWITH = 'startswith'
    ISTARTSWITH = 'istartswith'
    EXISTS = 'exists'
    OP_TYPES = {EQ, NE, GT, GTE, LT, LTE, EXACT, IEXACT, CONTAINS, ICONTAINS, STARTSWITH, ISTARTSWITH, EXISTS}
    CONTAINS_OPS = {EXACT, IEXACT, CONTAINS, ICONTAINS, STARTSWITH, ISTARTSWITH}

    # Valid lookups
    LOOKUP_RANGE = 'range'
    LOOKUP_IN = 'in'
    LOOKUP_NOT = 'not'
    LOOKUP_GT = 'gt'
    LOOKUP_GTE = 'gte'
    LOOKUP_LT = 'lt'
    LOOKUP_LTE = 'lte'
    LOOKUP_EXACT = 'exact'
    LOOKUP_IEXACT = 'iexact'
    LOOKUP_CONTAINS = 'contains'
    LOOKUP_ICONTAINS = 'icontains'
    LOOKUP_STARTSWITH = 'startswith'
    LOOKUP_ISTARTSWITH = 'istartswith'
    LOOKUP_EXISTS = 'exists'
    LOOKUP_TYPES = {LOOKUP_RANGE, LOOKUP_IN, LOOKUP_NOT, LOOKUP_GT, LOOKUP_GTE, LOOKUP_LT, LOOKUP_LTE, LOOKUP_EXACT,
                    LOOKUP_IEXACT, LOOKUP_CONTAINS, LOOKUP_ICONTAINS, LOOKUP_STARTSWITH, LOOKUP_ISTARTSWITH,
                    LOOKUP_EXISTS}

    __slots__ = 'conn_type', 'field_path', 'op', 'value', 'children', 'query_string'

    def __init__(self, *args, **kwargs):
        self.conn_type = kwargs.pop('conn_type', self.AND)

        self.field_path = None  # Name of the field we want to filter on
        self.op = None
        self.value = None
        self.query_string = None

        # Parsing of args and kwargs may require child elements
        self.children = []

        # Check for query string as the only argument
        if not kwargs and len(args) == 1 and isinstance(args[0], str):
            self.query_string = args[0]
            args = ()

        # Parse args which must now be Q objects
        for q in args:
            if not isinstance(q, self.__class__):
                raise ValueError("Non-keyword arg %r must be a Q instance" % q)
        self.children.extend(args)

        # Parse keyword args and extract the filter
        is_single_kwarg = len(args) == 0 and len(kwargs) == 1
        for key, value in kwargs.items():
            self.children.extend(
                self._get_children_from_kwarg(key=key, value=value, is_single_kwarg=is_single_kwarg)
            )

        # Simplify this object
        self.reduce()

        # Final sanity check
        self._check_integrity()

    def _get_children_from_kwarg(self, key, value, is_single_kwarg=False):
        """Generate Q objects corresponding to a single keyword argument. Make this a leaf if there are no children to
        generate.
        """
        key_parts = key.rsplit('__', 1)
        if len(key_parts) == 2 and key_parts[1] in self.LOOKUP_TYPES:
            # This is a kwarg with a lookup at the end
            field_path, lookup = key_parts
            if lookup == self.LOOKUP_EXISTS:
                # value=True will fall through to further processing
                if not value:
                    return [~self.__class__(**{key: True})]

            if lookup == self.LOOKUP_RANGE:
                # EWS doesn't have a 'range' operator. Emulate 'foo__range=(1, 2)' as 'foo__gte=1 and foo__lte=2'
                # (both values inclusive).
                if len(value) != 2:
                    raise ValueError("Value of lookup '%s' must have exactly 2 elements" % key)
                return [
                    self.__class__(**{'%s__gte' % field_path: value[0]}),
                    self.__class__(**{'%s__lte' % field_path: value[1]}),
                ]

            # Filtering on list types is a bit quirky. The only lookup type I have found to work is:
            #
            #     item:Categories == 'foo' AND item:Categories == 'bar' AND ...
            #
            #     item:Categories == 'foo' OR item:Categories == 'bar' OR ...
            #
            # The former returns items that have all these categories, but maybe also others. The latter returns
            # items that have at least one of these categories. This translates to the 'contains' and 'in' lookups,
            # respectively. Both versions are case-insensitive.
            #
            # Exact matching and case-sensitive or partial-string matching is not possible since that requires the
            # 'Contains' element which only supports matching on string elements, not arrays.
            #
            # Exact matching of categories (i.e. match ['a', 'b'] but not ['a', 'b', 'c']) could be implemented by
            # post-processing items by fetch the categories field unconditionally and removing the items that don't
            # have an exact match.
            if lookup == self.LOOKUP_IN:
                # EWS doesn't have an '__in' operator. Allow '__in' lookups on list and non-list field types,
                # specifying a list value. We'll emulate it as a set of OR'ed exact matches.
                if not is_iterable(value, generators_allowed=True):
                    raise ValueError("Value for lookup %r must be a list" % key)
                children = [self.__class__(**{field_path: v}) for v in value]
                if not children:
                    # This is an '__in' operator with an empty list as the value. We interpret it to mean "is foo
                    # contained in the empty set?" which is always false. Mark this Q object as such.
                    return [self.__class__(conn_type=self.NEVER)]
                return [self.__class__(*children, conn_type=self.OR)]

            if lookup == self.LOOKUP_CONTAINS and is_iterable(value, generators_allowed=True):
                # A '__contains' lookup with an list as the value ony makes sense for list fields, since exact match
                # on multiple distinct values will always fail for single-value fields.
                #
                # An empty list as value is allowed. We interpret it to mean "are all values in the empty set contained
                # in foo?" which is always true.
                children = [self.__class__(**{field_path: v}) for v in value]
                return [self.__class__(*children, conn_type=self.AND)]

            try:
                op = self._lookup_to_op(lookup)
            except KeyError:
                raise ValueError("Lookup '%s' is not supported (called as '%s=%r')" % (lookup, key, value))
        else:
            field_path, op = key, self.EQ

        if not is_single_kwarg:
            return [self.__class__(**{key: value})]

        # This is a single-kwarg Q object with a lookup that requires a single value. Make this a leaf
        self.field_path = field_path
        self.op = op
        self.value = value
        return []

    def reduce(self):
        """Simplify this object, if possible."""
        self._reduce_children()
        self._promote()

    def _reduce_children(self):
        """Look at the children of this object and remove unnecessary items."""
        children = self.children
        if any((isinstance(a, self.__class__) and a.is_never()) for a in children):
            # We have at least one 'never' arg
            if self.conn_type == self.AND:
                # Remove all other args since nothing we AND together with a 'never' arg can change the result
                children = [self.__class__(conn_type=self.NEVER)]
            elif self.conn_type == self.OR:
                # Remove all 'never' args because all other args will decide the result. Keep one 'never' arg in case
                # all args are 'never' args.
                children = [a for a in children if not (isinstance(a, self.__class__) and a.is_never())]
                if not children:
                    children = [self.__class__(conn_type=self.NEVER)]
            elif self.conn_type == self.NOT:
                # Let's interpret 'not never' to mean 'always'. Remove all 'never' args
                children = [a for a in children if not (isinstance(a, self.__class__) and a.is_never())]

        # Remove any empty Q elements in args before proceeding
        children = [a for a in children if not (isinstance(a, self.__class__) and a.is_empty())]
        self.children = children

    def _promote(self):
        """When we only have one child and no expression on ourselves, we are a no-op. Flatten by taking over the only
        child.
        """
        if len(self.children) != 1 or self.field_path is not None or self.conn_type == self.NOT:
            return

        q = self.children[0]
        self.conn_type = q.conn_type
        self.field_path = q.field_path
        self.op = q.op
        self.value = q.value
        self.query_string = q.query_string
        self.children = q.children

    def clean(self, version):
        """Do some basic checks on the attributes, using a generic folder. to_xml() does a really good job of
        validating. There's no reason to replicate much of that here.
        """
        from .folders import Folder
        self.to_xml(folders=[Folder()], version=version, applies_to=Restriction.ITEMS)

    @classmethod
    def _lookup_to_op(cls, lookup):
        return {
            cls.LOOKUP_NOT: cls.NE,
            cls.LOOKUP_GT: cls.GT,
            cls.LOOKUP_GTE: cls.GTE,
            cls.LOOKUP_LT: cls.LT,
            cls.LOOKUP_LTE: cls.LTE,
            cls.LOOKUP_EXACT: cls.EXACT,
            cls.LOOKUP_IEXACT: cls.IEXACT,
            cls.LOOKUP_CONTAINS: cls.CONTAINS,
            cls.LOOKUP_ICONTAINS: cls.ICONTAINS,
            cls.LOOKUP_STARTSWITH: cls.STARTSWITH,
            cls.LOOKUP_ISTARTSWITH: cls.ISTARTSWITH,
            cls.LOOKUP_EXISTS: cls.EXISTS,
        }[lookup]

    @classmethod
    def _conn_to_xml(cls, conn_type):
        xml_tag_map = {
            cls.AND: 't:And',
            cls.OR: 't:Or',
            cls.NOT: 't:Not',
        }
        return create_element(xml_tag_map[conn_type])

    @classmethod
    def _op_to_xml(cls, op):
        xml_tag_map = {
            cls.EQ: 't:IsEqualTo',
            cls.NE: 't:IsNotEqualTo',
            cls.GTE: 't:IsGreaterThanOrEqualTo',
            cls.LTE: 't:IsLessThanOrEqualTo',
            cls.LT: 't:IsLessThan',
            cls.GT: 't:IsGreaterThan',
            cls.EXISTS: 't:Exists',
        }
        if op in xml_tag_map:
            return create_element(xml_tag_map[op])
        valid_ops = cls.EXACT, cls.IEXACT, cls.CONTAINS, cls.ICONTAINS, cls.STARTSWITH, cls.ISTARTSWITH
        if op not in valid_ops:
            raise ValueError("'op' %s must be one of %s" % (op, valid_ops))

        # For description of Contains attribute values, see
        #     https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/contains
        #
        # Possible ContainmentMode values:
        #     FullString, Prefixed, Substring, PrefixOnWords, ExactPhrase
        # Django lookups have no equivalent of PrefixOnWords and ExactPhrase (and I'm unsure how they actually
        # work).
        #
        # EWS has no equivalent of '__endswith' or '__iendswith'. That could be emulated using '__contains' and
        # '__icontains' and filtering results afterwards in Python. But it could be inefficient because we might be
        # fetching and discarding a lot of non-matching items, plus we would need to always fetch the field we're
        # matching on, to be able to do the filtering. I think it's better to leave this to the consumer, i.e.:
        #
        # items = [i for i in fld.filter(subject__contains=suffix) if i.subject.endswith(suffix)]
        # items = [i for i in fld.filter(subject__icontains=suffix) if i.subject.lower().endswith(suffix.lower())]
        #
        # Possible ContainmentComparison values (there are more, but the rest are "To be removed"):
        #     Exact, IgnoreCase, IgnoreNonSpacingCharacters, IgnoreCaseAndNonSpacingCharacters
        # I'm unsure about non-spacing characters, but as I read
        #    https://en.wikipedia.org/wiki/Graphic_character#Spacing_and_non-spacing_characters
        # we shouldn't ignore them ('a' would match both 'a' and 'å', the latter having a non-spacing character).
        if op in {cls.EXACT, cls.IEXACT}:
            match_mode = 'FullString'
        elif op in (cls.CONTAINS, cls.ICONTAINS):
            match_mode = 'Substring'
        elif op in (cls.STARTSWITH, cls.ISTARTSWITH):
            match_mode = 'Prefixed'
        else:
            raise ValueError('Unsupported op: %s' % op)
        if op in (cls.IEXACT, cls.ICONTAINS, cls.ISTARTSWITH):
            compare_mode = 'IgnoreCase'
        else:
            compare_mode = 'Exact'
        return create_element(
            't:Contains',
            attrs=OrderedDict([
                ('ContainmentMode', match_mode),
                ('ContainmentComparison', compare_mode),
            ])
        )

    def is_leaf(self):
        return not self.children

    def is_empty(self):
        """Return True if this object is without any restrictions at all."""
        return self.is_leaf() and self.field_path is None and self.query_string is None and self.conn_type != self.NEVER

    def is_never(self):
        """Return True if this object has a restriction that will never match anything."""
        return self.conn_type == self.NEVER

    def expr(self):
        if self.is_empty():
            return None
        if self.is_never():
            return self.NEVER
        if self.query_string:
            return self.query_string
        if self.is_leaf():
            expr = '%s %s %r' % (self.field_path, self.op, self.value)
        else:
            # Sort children by field name so we get stable output (for easier testing). Children should never be empty.
            expr = (' %s ' % (self.AND if self.conn_type == self.NOT else self.conn_type)).join(
                (c.expr() if c.is_leaf() or c.conn_type == self.NOT else '(%s)' % c.expr())
                for c in sorted(self.children, key=lambda i: i.field_path or '')
            )
        if self.conn_type == self.NOT:
            # Add the NOT operator. Put children in parens if there is more than one child.
            if self.is_leaf() or len(self.children) == 1:
                return self.conn_type + ' %s' % expr
            return self.conn_type + ' (%s)' % expr
        return expr

    def to_xml(self, folders, version, applies_to):
        if self.query_string:
            self._check_integrity()
            if version.build < EXCHANGE_2010:
                raise NotImplementedError('QueryString filtering is only supported for Exchange 2010 servers and later')
            elem = create_element('m:QueryString')
            elem.text = self.query_string
            return elem
        # Translate this Q object to a valid Restriction XML tree
        elem = self.xml_elem(folders=folders, version=version, applies_to=applies_to)
        if elem is None:
            return None
        restriction = create_element('m:Restriction')
        restriction.append(elem)
        return restriction

    def _check_integrity(self):
        if self.is_empty():
            return
        if self.conn_type == self.NEVER:
            if any([self.field_path, self.op, self.value, self.children]):
                raise ValueError("'never' queries cannot be combined with other settings")
            return
        if self.query_string:
            if any([self.field_path, self.op, self.value, self.children]):
                raise ValueError('Query strings cannot be combined with other settings')
            return
        if self.conn_type not in self.CONN_TYPES:
            raise ValueError("'conn_type' %s must be one of %s" % (self.conn_type, self.CONN_TYPES))
        if not self.is_leaf():
            for q in self.children:
                if q.query_string and len(self.children) > 1:
                    raise ValueError(
                        'A query string cannot be combined with other restrictions'
                    )
            return
        if not self.field_path:
            raise ValueError("'field_path' must be set")
        if self.op not in self.OP_TYPES:
            raise ValueError("'op' %s must be one of %s" % (self.op, self.OP_TYPES))
        if self.op == self.EXISTS and self.value is not True:
            raise ValueError("'value' must be True when operator is EXISTS")
        if self.value is None:
            raise ValueError('Value for filter on field path "%s" cannot be None' % self.field_path)
        if is_iterable(self.value, generators_allowed=True):
            raise ValueError(
                'Value %r for filter on field path "%s" must be a single value' % (self.value, self.field_path)
            )

    def _validate_field_path(self, field_path, folder, applies_to, version):
        from .indexed_properties import MultiFieldIndexedElement
        if applies_to == Restriction.FOLDERS:
            # This is a restriction on Folder fields
            folder.validate_field(field=field_path.field, version=version)
        else:
            folder.validate_item_field(field=field_path.field, version=version)
        if not field_path.field.is_searchable:
            raise ValueError("EWS does not support filtering on field '%s'" % field_path.field.name)
        if field_path.subfield and not field_path.subfield.is_searchable:
            raise ValueError("EWS does not support filtering on subfield '%s'" % field_path.subfield.name)
        if issubclass(field_path.field.value_cls, MultiFieldIndexedElement) and not field_path.subfield:
            raise ValueError("Field path '%s' must contain a subfield" % self.field_path)

    def _get_field_path(self, folders, applies_to, version):
        # Convert the string field path to a real FieldPath object. The path is validated using the given folders.
        for folder in folders:
            try:
                if applies_to == Restriction.FOLDERS:
                    # This is a restriction on Folder fields
                    field = folder.get_field_by_fieldname(fieldname=self.field_path)
                    field_path = FieldPath(field=field)
                else:
                    field_path = FieldPath.from_string(field_path=self.field_path, folder=folder)
            except ValueError:
                continue
            self._validate_field_path(field_path=field_path, folder=folder, applies_to=applies_to, version=version)
            break
        else:
            raise InvalidField("Unknown field path %r on folders %s" % (self.field_path, folders))
        return field_path

    def _get_clean_value(self, field_path, version):
        if self.op == self.EXISTS:
            return None
        clean_field = field_path.subfield if (field_path.subfield and field_path.label) else field_path.field
        if clean_field.is_list:
            # __contains and __in are implemented as multiple leaves, with one value per leaf. clean() on list fields
            # only works on lists, so clean a one-element list.
            return clean_field.clean(value=[self.value], version=version)[0]
        return clean_field.clean(value=self.value, version=version)

    def xml_elem(self, folders, version, applies_to):
        # Recursively build an XML tree structure of this Q object. If this is an empty leaf (the equivalent of Q()),
        # return None.
        from .indexed_properties import SingleFieldIndexedElement
        # Don't check self.value just yet. We want to return error messages on the field path first, and then the value.
        # This is done in _get_field_path() and _get_clean_value(), respectively.
        self._check_integrity()
        if self.is_empty():
            return None
        if self.is_never():
            raise ValueError("EWS does not support 'never' queries")
        if self.is_leaf():
            elem = self._op_to_xml(self.op)
            field_path = self._get_field_path(folders, applies_to=applies_to, version=version)
            clean_value = self._get_clean_value(field_path=field_path, version=version)
            if issubclass(field_path.field.value_cls, SingleFieldIndexedElement) and not field_path.label:
                # We allow a filter shortcut of e.g. email_addresses__contains=EmailAddress(label='Foo', ...) instead of
                # email_addresses__Foo_email_address=.... Set FieldPath label now so we can generate the field_uri.
                field_path.label = clean_value.label
            elif isinstance(field_path.field, DateTimeBackedDateField):
                # We need to convert to datetime
                clean_value = field_path.field.date_to_datetime(clean_value)
            elem.append(field_path.to_xml())
            constant = create_element('t:Constant')
            if self.op != self.EXISTS:
                # Use .set() to not fill up the create_element() cache with unique values
                constant.set('Value', value_to_xml_text(clean_value))
                if self.op in self.CONTAINS_OPS:
                    elem.append(constant)
                else:
                    uriorconst = create_element('t:FieldURIOrConstant')
                    uriorconst.append(constant)
                    elem.append(uriorconst)
        elif len(self.children) == 1:
            # We have only one child
            elem = self.children[0].xml_elem(folders=folders, version=version, applies_to=applies_to)
        else:
            # We have multiple children. If conn_type is NOT, then group children with AND. We'll add the NOT later
            elem = self._conn_to_xml(self.AND if self.conn_type == self.NOT else self.conn_type)
            # Sort children by field name so we get stable output (for easier testing). Children should never be empty
            for c in sorted(self.children, key=lambda i: i.field_path or ''):
                elem.append(c.xml_elem(folders=folders, version=version, applies_to=applies_to))
        if elem is None:
            return None  # Should not be necessary, but play safe
        if self.conn_type == self.NOT:
            # Encapsulate everything in the NOT element
            not_elem = self._conn_to_xml(self.conn_type)
            not_elem.append(elem)
            return not_elem
        return elem

    def __and__(self, other):
        # & operator. Return a new Q with two children and conn_type AND
        return self.__class__(self, other, conn_type=self.AND)

    def __or__(self, other):
        # | operator. Return a new Q with two children and conn_type OR
        return self.__class__(self, other, conn_type=self.OR)

    def __invert__(self):
        # ~ operator. If op has an inverse, change op. Else return a new Q with conn_type NOT
        if self.conn_type == self.NOT:
            # This is NOT NOT. Change to AND
            new = copy(self)
            new.conn_type = self.AND
            new.reduce()
            return new
        if self.is_leaf():
            inverse_ops = {
                self.EQ: self.NE,
                self.NE: self.EQ,
                self.GT: self.LTE,
                self.GTE: self.LT,
                self.LT: self.GTE,
                self.LTE: self.GT,
            }
            try:
                new = copy(self)
                new.op = inverse_ops[self.op]
                new.reduce()
                return new
            except KeyError:
                pass
        return self.__class__(self, conn_type=self.NOT)

    def __eq__(self, other):
        return repr(self) == repr(other)

    def __hash__(self):
        return hash(repr(self))

    def __str__(self):
        return self.expr() or 'Q()'

    def __repr__(self):
        if self.is_leaf():
            if self.query_string:
                return self.__class__.__name__ + '(%r)' % self.query_string
            if self.is_never():
                return self.__class__.__name__ + '(conn_type=%r)' % (self.conn_type)
            return self.__class__.__name__ + '(%s %s %r)' % (self.field_path, self.op, self.value)
        sorted_children = tuple(sorted(self.children, key=lambda i: i.field_path or ''))
        if self.conn_type == self.NOT or len(self.children) > 1:
            return self.__class__.__name__ + repr((self.conn_type,) + sorted_children)
        return self.__class__.__name__ + repr(sorted_children)

Class variables

var AND
var CONN_TYPES
var CONTAINS
var CONTAINS_OPS
var EQ
var EXACT
var EXISTS
var GT
var GTE
var ICONTAINS
var IEXACT
var ISTARTSWITH
var LOOKUP_CONTAINS
var LOOKUP_EXACT
var LOOKUP_EXISTS
var LOOKUP_GT
var LOOKUP_GTE
var LOOKUP_ICONTAINS
var LOOKUP_IEXACT
var LOOKUP_IN
var LOOKUP_ISTARTSWITH
var LOOKUP_LT
var LOOKUP_LTE
var LOOKUP_NOT
var LOOKUP_RANGE
var LOOKUP_STARTSWITH
var LOOKUP_TYPES
var LT
var LTE
var NE
var NEVER
var NOT
var OP_TYPES
var OR
var STARTSWITH

Instance variables

var children

Return an attribute of instance, which is of type owner.

var conn_type

Return an attribute of instance, which is of type owner.

var field_path

Return an attribute of instance, which is of type owner.

var op

Return an attribute of instance, which is of type owner.

var query_string

Return an attribute of instance, which is of type owner.

var value

Return an attribute of instance, which is of type owner.

Methods

def clean(self, version)

Do some basic checks on the attributes, using a generic folder. to_xml() does a really good job of validating. There's no reason to replicate much of that here.

Expand source code
def clean(self, version):
    """Do some basic checks on the attributes, using a generic folder. to_xml() does a really good job of
    validating. There's no reason to replicate much of that here.
    """
    from .folders import Folder
    self.to_xml(folders=[Folder()], version=version, applies_to=Restriction.ITEMS)
def expr(self)
Expand source code
def expr(self):
    if self.is_empty():
        return None
    if self.is_never():
        return self.NEVER
    if self.query_string:
        return self.query_string
    if self.is_leaf():
        expr = '%s %s %r' % (self.field_path, self.op, self.value)
    else:
        # Sort children by field name so we get stable output (for easier testing). Children should never be empty.
        expr = (' %s ' % (self.AND if self.conn_type == self.NOT else self.conn_type)).join(
            (c.expr() if c.is_leaf() or c.conn_type == self.NOT else '(%s)' % c.expr())
            for c in sorted(self.children, key=lambda i: i.field_path or '')
        )
    if self.conn_type == self.NOT:
        # Add the NOT operator. Put children in parens if there is more than one child.
        if self.is_leaf() or len(self.children) == 1:
            return self.conn_type + ' %s' % expr
        return self.conn_type + ' (%s)' % expr
    return expr
def is_empty(self)

Return True if this object is without any restrictions at all.

Expand source code
def is_empty(self):
    """Return True if this object is without any restrictions at all."""
    return self.is_leaf() and self.field_path is None and self.query_string is None and self.conn_type != self.NEVER
def is_leaf(self)
Expand source code
def is_leaf(self):
    return not self.children
def is_never(self)

Return True if this object has a restriction that will never match anything.

Expand source code
def is_never(self):
    """Return True if this object has a restriction that will never match anything."""
    return self.conn_type == self.NEVER
def reduce(self)

Simplify this object, if possible.

Expand source code
def reduce(self):
    """Simplify this object, if possible."""
    self._reduce_children()
    self._promote()
def to_xml(self, folders, version, applies_to)
Expand source code
def to_xml(self, folders, version, applies_to):
    if self.query_string:
        self._check_integrity()
        if version.build < EXCHANGE_2010:
            raise NotImplementedError('QueryString filtering is only supported for Exchange 2010 servers and later')
        elem = create_element('m:QueryString')
        elem.text = self.query_string
        return elem
    # Translate this Q object to a valid Restriction XML tree
    elem = self.xml_elem(folders=folders, version=version, applies_to=applies_to)
    if elem is None:
        return None
    restriction = create_element('m:Restriction')
    restriction.append(elem)
    return restriction
def xml_elem(self, folders, version, applies_to)
Expand source code
def xml_elem(self, folders, version, applies_to):
    # Recursively build an XML tree structure of this Q object. If this is an empty leaf (the equivalent of Q()),
    # return None.
    from .indexed_properties import SingleFieldIndexedElement
    # Don't check self.value just yet. We want to return error messages on the field path first, and then the value.
    # This is done in _get_field_path() and _get_clean_value(), respectively.
    self._check_integrity()
    if self.is_empty():
        return None
    if self.is_never():
        raise ValueError("EWS does not support 'never' queries")
    if self.is_leaf():
        elem = self._op_to_xml(self.op)
        field_path = self._get_field_path(folders, applies_to=applies_to, version=version)
        clean_value = self._get_clean_value(field_path=field_path, version=version)
        if issubclass(field_path.field.value_cls, SingleFieldIndexedElement) and not field_path.label:
            # We allow a filter shortcut of e.g. email_addresses__contains=EmailAddress(label='Foo', ...) instead of
            # email_addresses__Foo_email_address=.... Set FieldPath label now so we can generate the field_uri.
            field_path.label = clean_value.label
        elif isinstance(field_path.field, DateTimeBackedDateField):
            # We need to convert to datetime
            clean_value = field_path.field.date_to_datetime(clean_value)
        elem.append(field_path.to_xml())
        constant = create_element('t:Constant')
        if self.op != self.EXISTS:
            # Use .set() to not fill up the create_element() cache with unique values
            constant.set('Value', value_to_xml_text(clean_value))
            if self.op in self.CONTAINS_OPS:
                elem.append(constant)
            else:
                uriorconst = create_element('t:FieldURIOrConstant')
                uriorconst.append(constant)
                elem.append(uriorconst)
    elif len(self.children) == 1:
        # We have only one child
        elem = self.children[0].xml_elem(folders=folders, version=version, applies_to=applies_to)
    else:
        # We have multiple children. If conn_type is NOT, then group children with AND. We'll add the NOT later
        elem = self._conn_to_xml(self.AND if self.conn_type == self.NOT else self.conn_type)
        # Sort children by field name so we get stable output (for easier testing). Children should never be empty
        for c in sorted(self.children, key=lambda i: i.field_path or ''):
            elem.append(c.xml_elem(folders=folders, version=version, applies_to=applies_to))
    if elem is None:
        return None  # Should not be necessary, but play safe
    if self.conn_type == self.NOT:
        # Encapsulate everything in the NOT element
        not_elem = self._conn_to_xml(self.conn_type)
        not_elem.append(elem)
        return not_elem
    return elem
class Restriction (q, folders, applies_to)

Implement an EWS Restriction type.

Expand source code
class Restriction:
    """Implement an EWS Restriction type."""

    # The type of item the restriction applies to
    FOLDERS = 'folders'
    ITEMS = 'items'
    RESTRICTION_TYPES = (FOLDERS, ITEMS)

    def __init__(self, q, folders, applies_to):
        if not isinstance(q, Q):
            raise ValueError("'q' value %r must be a Q instance" % q)
        if q.is_empty():
            raise ValueError("Q object must not be empty")
        from .folders import BaseFolder
        for folder in folders:
            if not isinstance(folder, BaseFolder):
                raise ValueError("'folder' value %r must be a Folder instance" % folder)
        if applies_to not in self.RESTRICTION_TYPES:
            raise ValueError("'applies_to' must be one of %s" % (self.RESTRICTION_TYPES,))
        self.q = q
        self.folders = folders
        self.applies_to = applies_to

    def to_xml(self, version):
        return self.q.to_xml(folders=self.folders, version=version, applies_to=self.applies_to)

    def __str__(self):
        """Print the XML syntax tree."""
        return xml_to_str(self.to_xml(version=self.folders[0].account.version))

Class variables

var FOLDERS
var ITEMS
var RESTRICTION_TYPES

Methods

def to_xml(self, version)
Expand source code
def to_xml(self, version):
    return self.q.to_xml(folders=self.folders, version=version, applies_to=self.applies_to)
exchangelib-4.6.1/docs/exchangelib/services/000077500000000000000000000000001414601472700210255ustar00rootroot00000000000000exchangelib-4.6.1/docs/exchangelib/services/archive_item.html000066400000000000000000000347101414601472700243570ustar00rootroot00000000000000 exchangelib.services.archive_item API documentation

Module exchangelib.services.archive_item

Expand source code
from .common import EWSAccountService, create_folder_ids_element, create_item_ids_element
from ..util import create_element, MNS
from ..version import EXCHANGE_2013


class ArchiveItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/archiveitem-operation"""

    SERVICE_NAME = 'ArchiveItem'
    element_container_name = '{%s}Items' % MNS
    supported_from = EXCHANGE_2013

    def call(self, items, to_folder):
        """Move a list of items to a specific folder in the archive mailbox.

        :param items: a list of (id, changekey) tuples or Item objects
        :param to_folder:

        :return: None
        """
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, to_folder=to_folder))

    def _elems_to_objs(self, elems):
        from ..items import Item
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Item.id_from_xml(elem)

    def get_payload(self, items, to_folder):
        archiveitem = create_element('m:%s' % self.SERVICE_NAME)
        folder_id = create_folder_ids_element(tag='m:ArchiveSourceFolderId', folders=[to_folder],
                                              version=self.account.version)
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        archiveitem.append(folder_id)
        archiveitem.append(item_ids)
        return archiveitem

Classes

class ArchiveItem (*args, **kwargs)
Expand source code
class ArchiveItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/archiveitem-operation"""

    SERVICE_NAME = 'ArchiveItem'
    element_container_name = '{%s}Items' % MNS
    supported_from = EXCHANGE_2013

    def call(self, items, to_folder):
        """Move a list of items to a specific folder in the archive mailbox.

        :param items: a list of (id, changekey) tuples or Item objects
        :param to_folder:

        :return: None
        """
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, to_folder=to_folder))

    def _elems_to_objs(self, elems):
        from ..items import Item
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Item.id_from_xml(elem)

    def get_payload(self, items, to_folder):
        archiveitem = create_element('m:%s' % self.SERVICE_NAME)
        folder_id = create_folder_ids_element(tag='m:ArchiveSourceFolderId', folders=[to_folder],
                                              version=self.account.version)
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        archiveitem.append(folder_id)
        archiveitem.append(item_ids)
        return archiveitem

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var supported_from

Methods

def call(self, items, to_folder)

Move a list of items to a specific folder in the archive mailbox.

:param items: a list of (id, changekey) tuples or Item objects :param to_folder:

:return: None

Expand source code
def call(self, items, to_folder):
    """Move a list of items to a specific folder in the archive mailbox.

    :param items: a list of (id, changekey) tuples or Item objects
    :param to_folder:

    :return: None
    """
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, to_folder=to_folder))
def get_payload(self, items, to_folder)
Expand source code
def get_payload(self, items, to_folder):
    archiveitem = create_element('m:%s' % self.SERVICE_NAME)
    folder_id = create_folder_ids_element(tag='m:ArchiveSourceFolderId', folders=[to_folder],
                                          version=self.account.version)
    item_ids = create_item_ids_element(items=items, version=self.account.version)
    archiveitem.append(folder_id)
    archiveitem.append(item_ids)
    return archiveitem

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/common.html000066400000000000000000003417361414601472700232210ustar00rootroot00000000000000 exchangelib.services.common API documentation

Module exchangelib.services.common

Expand source code
import abc
import logging
import traceback
from itertools import chain

from .. import errors
from ..credentials import IMPERSONATION, OAuth2Credentials
from ..errors import EWSWarning, TransportError, SOAPError, ErrorTimeoutExpired, ErrorBatchProcessingStopped, \
    ErrorQuotaExceeded, ErrorCannotDeleteObject, ErrorCreateItemAccessDenied, ErrorFolderNotFound, \
    ErrorNonExistentMailbox, ErrorMailboxStoreUnavailable, ErrorImpersonateUserDenied, ErrorInternalServerError, \
    ErrorInternalServerTransientError, ErrorNoRespondingCASInDestinationSite, ErrorImpersonationFailed, \
    ErrorMailboxMoveInProgress, ErrorAccessDenied, ErrorConnectionFailed, RateLimitError, ErrorServerBusy, \
    ErrorTooManyObjectsOpened, ErrorInvalidLicense, ErrorInvalidSchemaVersionForMailboxVersion, \
    ErrorInvalidServerVersion, ErrorItemNotFound, ErrorADUnavailable, ErrorInvalidChangeKey, \
    ErrorItemSave, ErrorInvalidIdMalformed, ErrorMessageSizeExceeded, UnauthorizedError, \
    ErrorCannotDeleteTaskOccurrence, ErrorMimeContentConversionFailed, ErrorRecurrenceHasNoOccurrence, \
    ErrorNoPublicFolderReplicaAvailable, MalformedResponseError, ErrorExceededConnectionCount, \
    SessionPoolMinSizeReached, ErrorIncorrectSchemaVersion, ErrorInvalidRequest, ErrorCorruptData, \
    ErrorCannotEmptyFolder, ErrorDeleteDistinguishedFolder, ErrorInvalidSubscription, ErrorInvalidWatermark, \
    ErrorInvalidSyncStateData, ErrorNameResolutionNoResults, ErrorNameResolutionMultipleResults, \
    ErrorConnectionFailedTransientError
from ..properties import FieldURI, IndexedFieldURI, ExtendedFieldURI, ExceptionFieldURI, ItemId
from ..transport import wrap
from ..util import chunkify, create_element, add_xml_child, get_xml_attr, to_xml, post_ratelimited, \
    xml_to_str, set_xml_value, SOAPNS, TNS, MNS, ENS, ParseError, DummyResponse
from ..version import API_VERSIONS, Version

log = logging.getLogger(__name__)

CHUNK_SIZE = 100  # A default chunk size for all services

KNOWN_EXCEPTIONS = (
    ErrorAccessDenied,
    ErrorADUnavailable,
    ErrorBatchProcessingStopped,
    ErrorCannotDeleteObject,
    ErrorCannotEmptyFolder,
    ErrorConnectionFailed,
    ErrorConnectionFailedTransientError,
    ErrorCreateItemAccessDenied,
    ErrorDeleteDistinguishedFolder,
    ErrorExceededConnectionCount,
    ErrorFolderNotFound,
    ErrorImpersonateUserDenied,
    ErrorImpersonationFailed,
    ErrorInternalServerError,
    ErrorInternalServerTransientError,
    ErrorInvalidChangeKey,
    ErrorInvalidLicense,
    ErrorInvalidSubscription,
    ErrorInvalidSyncStateData,
    ErrorInvalidWatermark,
    ErrorItemNotFound,
    ErrorMailboxMoveInProgress,
    ErrorMailboxStoreUnavailable,
    ErrorNameResolutionMultipleResults,
    ErrorNameResolutionNoResults,
    ErrorNonExistentMailbox,
    ErrorNoPublicFolderReplicaAvailable,
    ErrorNoRespondingCASInDestinationSite,
    ErrorQuotaExceeded,
    ErrorTimeoutExpired,
    RateLimitError,
    UnauthorizedError,
)


class EWSService(metaclass=abc.ABCMeta):
    """Base class for all EWS services."""

    SERVICE_NAME = None  # The name of the SOAP service
    element_container_name = None  # The name of the XML element wrapping the collection of returned items
    paging_container_name = None  # The name of the element that contains paging information and the paged results
    returns_elements = True  # If False, the service does not return response elements, just the RsponseCode status
    # Return exception instance instead of raising exceptions for the following errors when contained in an element
    ERRORS_TO_CATCH_IN_RESPONSE = (
        EWSWarning, ErrorCannotDeleteObject, ErrorInvalidChangeKey, ErrorItemNotFound, ErrorItemSave,
        ErrorInvalidIdMalformed, ErrorMessageSizeExceeded, ErrorCannotDeleteTaskOccurrence,
        ErrorMimeContentConversionFailed, ErrorRecurrenceHasNoOccurrence, ErrorCorruptData
    )
    # Similarly, define the warnings we want to return unraised
    WARNINGS_TO_CATCH_IN_RESPONSE = ErrorBatchProcessingStopped
    # Define the warnings we want to ignore, to let response processing proceed
    WARNINGS_TO_IGNORE_IN_RESPONSE = ()
    # The exception type to raise when all attempted API versions failed
    NO_VALID_SERVER_VERSIONS = ErrorInvalidServerVersion
    # Marks the version from which the service was introduced
    supported_from = None
    # Marks services that support paging of requested items
    supports_paging = False
    # Marks services that need affinity to the backend server
    prefer_affinity = False

    def __init__(self, protocol, chunk_size=None, timeout=None):
        self.chunk_size = chunk_size or CHUNK_SIZE  # The number of items to send in a single request
        if not isinstance(self.chunk_size, int):
            raise ValueError("'chunk_size' %r must be an integer" % chunk_size)
        if self.chunk_size < 1:
            raise ValueError("'chunk_size' must be a positive number")
        if self.supported_from and protocol.version.build < self.supported_from:
            raise NotImplementedError(
                '%r is only supported on %r and later' % (self.SERVICE_NAME, self.supported_from.fullname())
            )
        self.protocol = protocol
        # Allow a service to override the default protocol timeout. Useful for streaming services
        self.timeout = timeout
        # Controls whether the HTTP request should be streaming or fetch everything at once
        self.streaming = False
        # Streaming connection variables
        self._streaming_session = None
        self._streaming_response = None

    # The following two methods are the minimum required to be implemented by subclasses, but the name and number of
    # kwargs differs between services. Therefore, we cannot make these methods abstract.

    # @abc.abstractmethod
    # def call(self, **kwargs):
    #     """Defines the arguments required by the service. Arguments are basic Python types or EWSElement objects.
    #     Returns either XML objects or EWSElement objects.
    #     """"
    #     pass

    # @abc.abstractmethod
    # def get_payload(self, **kwargs):
    #     """Using the arguments from .call(), return the payload expected by the service, as an XML object. The XML
    #     object should consist of a SERVICE_NAME element and everything within that.
    #     """
    #     pass

    def get(self, expect_result=True, **kwargs):
        """Like .call(), but expects exactly one result from the server, or zero when expect_result=False, or either
        zero or one when expect_result=None. Returns either one object or None.

        :param expect_result: None, True, or False
        :param kwargs: Same as arguments for .call()
        :return: Same as .call(), but returns either None or exactly one item
        """
        res = list(self.call(**kwargs))
        # Raise any errors
        for r in res:
            if isinstance(r, Exception):
                raise r
        if expect_result is None and not res:
            # Allow empty result
            return None
        if expect_result is False:
            if res:
                raise ValueError('Expected result length 0, but got %r' % res)
            return None
        if len(res) != 1:
            raise ValueError('Expected result length 1, but got %r' % res)
        return res[0]

    def parse(self, xml):
        """Used mostly for testing, when we want to parse static XML data."""
        resp = DummyResponse(url=None, headers=None, request_headers=None, content=xml)
        _, body = self._get_soap_parts(response=resp)
        return self._elems_to_objs(self._get_elements_in_response(response=self._get_soap_messages(body=body)))

    def _elems_to_objs(self, elems):
        """Takes a generator of XML elements and exceptions. Returns the equivalent Python objects (or exceptions)."""
        raise NotImplementedError()

    @property
    def _version_hint(self):
        # We may be here due to version guessing in Protocol.version, so we can't use the self.protocol.version property
        return self.protocol.config.version

    @_version_hint.setter
    def _version_hint(self, value):
        self.protocol.config.version = value

    def _extra_headers(self, session):
        headers = {}
        if self.prefer_affinity:
            headers['X-PreferServerAffinity'] = 'True'
        for cookie in session.cookies:
            if cookie.name == 'X-BackEndCookie':
                headers['X-BackEndOverrideCookie'] = cookie.value
        return headers

    @property
    def _account_to_impersonate(self):
        if isinstance(self.protocol.credentials, OAuth2Credentials):
            return self.protocol.credentials.identity
        return None

    @property
    def _timezone(self):
        return None

    def _response_generator(self, payload):
        """Send the payload to the server, and return the response.

        :param payload: payload as an XML object
        :return: the response, as XML objects
        """
        response = self._get_response_xml(payload=payload)
        if self.supports_paging:
            return (self._get_page(message) for message in response)
        return self._get_elements_in_response(response=response)

    def _chunked_get_elements(self, payload_func, items, **kwargs):
        """Yield elements in a response. Like ._get_elements(), but chop items into suitable chunks and send multiple
        requests.

        :param payload_func: A reference to .payload()
        :param items: An iterable of items (messages, folders, etc.) to process
        :param kwargs: Same as arguments for .call(), except for the 'items' argument
        :return: Same as ._get_elements()
        """
        for i, chunk in enumerate(chunkify(items, self.chunk_size), start=1):
            log.debug('Processing chunk %s containing %s items', i, len(chunk))
            yield from self._get_elements(payload=payload_func(chunk, **kwargs))

    def stop_streaming(self):
        if self._streaming_response:
            self._streaming_response.close()  # Release memory
            self._streaming_response = None
        if self._streaming_session:
            self.protocol.release_session(self._streaming_session)
            self._streaming_session = None

    def _get_elements(self, payload):
        """Send the payload to be sent and parsed. Handles and re-raise exceptions that are not meant to be returned
        to the caller as exception objects. Retry the request according to the retry policy.
        """
        while True:
            try:
                # Create a generator over the response elements so exceptions in response elements are also raised
                # here and can be handled.
                yield from self._response_generator(payload=payload)
                return
            except ErrorServerBusy as e:
                self._handle_backoff(e)
                continue
            except KNOWN_EXCEPTIONS:
                # These are known and understood, and don't require a backtrace.
                raise
            except (ErrorTooManyObjectsOpened, ErrorTimeoutExpired) as e:
                # ErrorTooManyObjectsOpened means there are too many connections to the Exchange database. This is very
                # often a symptom of sending too many requests.
                #
                # ErrorTimeoutExpired can be caused by a busy server, or by overly large requests. Start by lowering the
                # session count. This is done by downstream code.
                if isinstance(e, ErrorTimeoutExpired) and self.protocol.session_pool_size <= 1:
                    # We're already as low as we can go, so downstream cannot limit the session count to put less load
                    # on the server. We don't have a way of lowering the page size of requests from
                    # this part of the code yet. Let the user handle this.
                    raise e

                # Re-raise as an ErrorServerBusy with a default delay of 5 minutes
                raise ErrorServerBusy('Reraised from %s(%s)' % (e.__class__.__name__, e))
            except Exception:
                # This may run from a thread pool, which obfuscates the stack trace. Print trace immediately.
                account = self.account if isinstance(self, EWSAccountService) else None
                log.warning('Account %s: Exception in _get_elements: %s', account, traceback.format_exc(20))
                raise
            finally:
                if self.streaming:
                    self.stop_streaming()

    def _get_response(self, payload, api_version):
        """Send the actual HTTP request and get the response."""
        session = self.protocol.get_session()
        self._streaming_session, self._streaming_response = None, None
        r, session = post_ratelimited(
            protocol=self.protocol,
            session=session,
            url=self.protocol.service_endpoint,
            headers=self._extra_headers(session),
            data=wrap(
                content=payload,
                api_version=api_version,
                account_to_impersonate=self._account_to_impersonate,
                timezone=self._timezone,
            ),
            allow_redirects=False,
            stream=self.streaming,
            timeout=self.timeout or self.protocol.TIMEOUT,
        )
        if self.streaming:
            # We con only release the session when we have fully consumed the response. Save session and response
            # objects for later.
            self._streaming_session, self._streaming_response = session, r
        else:
            self.protocol.release_session(session)
        return r

    @property
    def _api_versions_to_try(self):
        # Put the hint first in the list, and then all other versions except the hint, from newest to oldest
        return [self._version_hint.api_version] + [v for v in API_VERSIONS if v != self._version_hint.api_version]

    def _get_response_xml(self, payload, **parse_opts):
        """Send the payload to the server and return relevant elements from the result. Several things happen here:
          * The payload is wrapped in SOAP headers and sent to the server
          * The Exchange API version is negotiated and stored in the protocol object
          * Connection errors are handled and possibly reraised as ErrorServerBusy
          * SOAP errors are raised
          * EWS errors are raised, or passed on to the caller

        :param payload: The request payload, as an XML object
        :return: A generator of XML objects or None if the service does not return a result
        """
        # Microsoft really doesn't want to make our lives easy. The server may report one version in our initial version
        # guessing tango, but then the server may decide that any arbitrary legacy backend server may actually process
        # the request for an account. Prepare to handle version-related errors and set the server version per-account.
        log.debug('Calling service %s', self.SERVICE_NAME)
        for api_version in self._api_versions_to_try:
            log.debug('Trying API version %s', api_version)
            r = self._get_response(payload=payload, api_version=api_version)
            if self.streaming:
                # Let 'requests' decode raw data automatically
                r.raw.decode_content = True
            try:
                header, body = self._get_soap_parts(response=r, **parse_opts)
            except Exception:
                r.close()  # Release memory
                raise
            # The body may contain error messages from Exchange, but we still want to collect version info
            if header is not None:
                self._update_api_version(api_version=api_version, header=header, **parse_opts)
            try:
                return self._get_soap_messages(body=body, **parse_opts)
            except (ErrorInvalidServerVersion, ErrorIncorrectSchemaVersion, ErrorInvalidRequest,
                    ErrorInvalidSchemaVersionForMailboxVersion):
                # The guessed server version is wrong. Try the next version
                log.debug('API version %s was invalid', api_version)
                continue
            except ErrorExceededConnectionCount as e:
                # This indicates that the connecting user has too many open TCP connections to the server. Decrease
                # our session pool size.
                try:
                    self.protocol.decrease_poolsize()
                    continue
                except SessionPoolMinSizeReached:
                    # We're already as low as we can go. Let the user handle this.
                    raise e
            finally:
                if not self.streaming:
                    # In streaming mode, we may not have accessed the raw stream yet. Caller must handle this.
                    r.close()  # Release memory

        raise self.NO_VALID_SERVER_VERSIONS('Tried versions %s but all were invalid' % self._api_versions_to_try)

    def _handle_backoff(self, e):
        """Take a request from the server to back off and checks the retry policy for what to do. Re-raise the
        exception if conditions are not met.

        :param e: An ErrorServerBusy instance
        :return:
        """
        log.debug('Got ErrorServerBusy (back off %s seconds)', e.back_off)
        # ErrorServerBusy is very often a symptom of sending too many requests. Scale back connections if possible.
        try:
            self.protocol.decrease_poolsize()
        except SessionPoolMinSizeReached:
            pass
        if self.protocol.retry_policy.fail_fast:
            raise e
        self.protocol.retry_policy.back_off(e.back_off)
        # We'll warn about this later if we actually need to sleep

    def _update_api_version(self, api_version, header, **parse_opts):
        """Parse the server version contained in SOAP headers and update the version hint stored by the caller, if
        necessary.
        """
        try:
            head_version = Version.from_soap_header(requested_api_version=api_version, header=header)
        except TransportError as te:
            log.debug('Failed to update version info (%s)', te)
            return
        if self._version_hint == head_version:
            # Nothing to do
            return
        log.debug('Found new version (%s -> %s)', self._version_hint, head_version)
        # The api_version that worked was different than our hint, or we never got a build version. Store the working
        # version.
        self._version_hint = head_version

    @classmethod
    def _response_tag(cls):
        """Return the name of the element containing the service response."""
        return '{%s}%sResponse' % (MNS, cls.SERVICE_NAME)

    @staticmethod
    def _response_messages_tag():
        """Return the name of the element containing service response messages."""
        return '{%s}ResponseMessages' % MNS

    @classmethod
    def _response_message_tag(cls):
        """Return the name of the element of a single response message."""
        return '{%s}%sResponseMessage' % (MNS, cls.SERVICE_NAME)

    @classmethod
    def _get_soap_parts(cls, response, **parse_opts):
        """Split the SOAP response into its headers an body elements."""
        try:
            root = to_xml(response.iter_content())
        except ParseError as e:
            raise SOAPError('Bad SOAP response: %s' % e)
        header = root.find('{%s}Header' % SOAPNS)
        if header is None:
            # This is normal when the response contains SOAP-level errors
            log.debug('No header in XML response')
        body = root.find('{%s}Body' % SOAPNS)
        if body is None:
            raise MalformedResponseError('No Body element in SOAP response')
        return header, body

    def _get_soap_messages(self, body, **parse_opts):
        """Return the elements in the response containing the response messages. Raises any SOAP exceptions."""
        response = body.find(self._response_tag())
        if response is None:
            fault = body.find('{%s}Fault' % SOAPNS)
            if fault is None:
                raise SOAPError(
                    'Unknown SOAP response (expected %s or Fault): %s' % (self._response_tag(), xml_to_str(body))
                )
            self._raise_soap_errors(fault=fault)  # Will throw SOAPError or custom EWS error
        response_messages = response.find(self._response_messages_tag())
        if response_messages is None:
            # Result isn't delivered in a list of FooResponseMessages, but directly in the FooResponse. Consumers expect
            # a list, so return a list
            return [response]
        return response_messages.findall(self._response_message_tag())

    @classmethod
    def _raise_soap_errors(cls, fault):
        """Parse error messages contained in SOAP headers and raise as exceptions defined in this package."""
        # Fault: See http://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383507
        faultcode = get_xml_attr(fault, 'faultcode')
        faultstring = get_xml_attr(fault, 'faultstring')
        faultactor = get_xml_attr(fault, 'faultactor')
        detail = fault.find('detail')
        if detail is not None:
            code, msg = None, ''
            if detail.find('{%s}ResponseCode' % ENS) is not None:
                code = get_xml_attr(detail, '{%s}ResponseCode' % ENS).strip()
            if detail.find('{%s}Message' % ENS) is not None:
                msg = get_xml_attr(detail, '{%s}Message' % ENS).strip()
            msg_xml = detail.find('{%s}MessageXml' % TNS)  # Crazy. Here, it's in the TNS namespace
            if code == 'ErrorServerBusy':
                back_off = None
                try:
                    value = msg_xml.find('{%s}Value' % TNS)
                    if value.get('Name') == 'BackOffMilliseconds':
                        back_off = int(value.text) / 1000.0  # Convert to seconds
                except (TypeError, AttributeError):
                    pass
                raise ErrorServerBusy(msg, back_off=back_off)
            if code == 'ErrorSchemaValidation' and msg_xml is not None:
                violation = get_xml_attr(msg_xml, '{%s}Violation' % TNS)
                if violation is not None:
                    msg = '%s %s' % (msg, violation)
            try:
                raise vars(errors)[code](msg)
            except KeyError:
                detail = '%s: code: %s msg: %s (%s)' % (cls.SERVICE_NAME, code, msg, xml_to_str(detail))
        try:
            raise vars(errors)[faultcode](faultstring)
        except KeyError:
            pass
        raise SOAPError('SOAP error code: %s string: %s actor: %s detail: %s' % (
            faultcode, faultstring, faultactor, detail))

    def _get_element_container(self, message, name=None):
        """Return the XML element in a response element that contains the elements we want the service to return. For
        example, in a GetFolder response, 'message' is the GetFolderResponseMessage element, and we return the 'Folders'
        element:

        <m:GetFolderResponseMessage ResponseClass="Success">
          <m:ResponseCode>NoError</m:ResponseCode>
          <m:Folders>
            <t:Folder>
              <t:FolderId Id="AQApA=" ChangeKey="AQAAAB" />
              [...]
            </t:Folder>
          </m:Folders>
        </m:GetFolderResponseMessage>

        Some service responses don't have a containing element for the returned elements ('name' is None). In
        that case, we return the 'SomeServiceResponseMessage' element.

        If the response contains a warning or an error message, we raise the relevant exception, unless the error class
        is contained in WARNINGS_TO_CATCH_IN_RESPONSE or ERRORS_TO_CATCH_IN_RESPONSE, in which case we return the
        exception instance.
        """
        # ResponseClass is an XML attribute of various SomeServiceResponseMessage elements: Possible values are:
        # Success, Warning, Error. See e.g.
        # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/finditemresponsemessage
        response_class = message.get('ResponseClass')
        # ResponseCode, MessageText: See
        # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/responsecode
        response_code = get_xml_attr(message, '{%s}ResponseCode' % MNS)
        if response_class == 'Success' and response_code == 'NoError':
            if not name:
                return message
            container = message.find(name)
            if container is None:
                raise MalformedResponseError('No %s elements in ResponseMessage (%s)' % (name, xml_to_str(message)))
            return container
        if response_code == 'NoError':
            return True
        # Raise any non-acceptable errors in the container, or return the container or the acceptable exception instance
        msg_text = get_xml_attr(message, '{%s}MessageText' % MNS)
        msg_xml = message.find('{%s}MessageXml' % MNS)
        if response_class == 'Warning':
            try:
                raise self._get_exception(code=response_code, text=msg_text, msg_xml=msg_xml)
            except self.WARNINGS_TO_CATCH_IN_RESPONSE as e:
                return e
            except self.WARNINGS_TO_IGNORE_IN_RESPONSE as e:
                log.warning(str(e))
                container = message.find(name)
                if container is None:
                    raise MalformedResponseError('No %s elements in ResponseMessage (%s)' % (name, xml_to_str(message)))
                return container
        # rspclass == 'Error', or 'Success' and not 'NoError'
        try:
            raise self._get_exception(code=response_code, text=msg_text, msg_xml=msg_xml)
        except self.ERRORS_TO_CATCH_IN_RESPONSE as e:
            return e

    @staticmethod
    def _get_exception(code, text, msg_xml):
        """Parse error messages contained in EWS responses and raise as exceptions defined in this package."""
        if not code:
            return TransportError('Empty ResponseCode in ResponseMessage (MessageText: %s, MessageXml: %s)' % (
                text, msg_xml))
        if msg_xml is not None:
            # If this is an ErrorInvalidPropertyRequest error, the xml may contain a specific FieldURI
            for elem_cls in (FieldURI, IndexedFieldURI, ExtendedFieldURI, ExceptionFieldURI):
                elem = msg_xml.find(elem_cls.response_tag())
                if elem is not None:
                    field_uri = elem_cls.from_xml(elem, account=None)
                    text += ' (field: %s)' % field_uri
                    break

            # If this is an ErrorInvalidValueForProperty error, the xml may contain the name and value of the property
            if code == 'ErrorInvalidValueForProperty':
                msg_parts = {}
                for elem in msg_xml.findall('{%s}Value' % TNS):
                    key, val = elem.get('Name'), elem.text
                    if key:
                        msg_parts[key] = val
                if msg_parts:
                    text += ' (%s)' % ', '.join('%s: %s' % (k, v) for k, v in msg_parts.items())

            # If this is an ErrorInternalServerError error, the xml may contain a more specific error code
            inner_code, inner_text = None, None
            for value_elem in msg_xml.findall('{%s}Value' % TNS):
                name = value_elem.get('Name')
                if name == 'InnerErrorResponseCode':
                    inner_code = value_elem.text
                elif name == 'InnerErrorMessageText':
                    inner_text = value_elem.text
            if inner_code:
                try:
                    # Raise the error as the inner error code
                    return vars(errors)[inner_code]('%s (raised from: %s(%r))' % (inner_text, code, text))
                except KeyError:
                    # Inner code is unknown to us. Just append to the original text
                    text += ' (inner error: %s(%r))' % (inner_code, inner_text)
        try:
            # Raise the error corresponding to the ResponseCode
            return vars(errors)[code](text)
        except KeyError:
            # Should not happen
            return TransportError('Unknown ResponseCode in ResponseMessage: %s (MessageText: %s, MessageXml: %s)' % (
                    code, text, msg_xml))

    def _get_elements_in_response(self, response):
        """Take a list of 'SomeServiceResponseMessage' elements and return the elements in each response message that
        we want the service to return. With e.g. 'CreateItem', we get a list of 'CreateItemResponseMessage' elements
        and return the 'Message' elements.

        <m:CreateItemResponseMessage ResponseClass="Success">
          <m:ResponseCode>NoError</m:ResponseCode>
          <m:Items>
            <t:Message>
              <t:ItemId Id="AQApA=" ChangeKey="AQAAAB"/>
            </t:Message>
          </m:Items>
        </m:CreateItemResponseMessage>
        <m:CreateItemResponseMessage ResponseClass="Success">
          <m:ResponseCode>NoError</m:ResponseCode>
          <m:Items>
            <t:Message>
              <t:ItemId Id="AQApB=" ChangeKey="AQAAAC"/>
            </t:Message>
          </m:Items>
        </m:CreateItemResponseMessage>

        :param response: a list of 'SomeServiceResponseMessage' XML objects
        :return: a generator of items as returned by '_get_elements_in_container()
        """
        for msg in response:
            container_or_exc = self._get_element_container(message=msg, name=self.element_container_name)
            if isinstance(container_or_exc, (bool, Exception)):
                yield container_or_exc
            else:
                for c in self._get_elements_in_container(container=container_or_exc):
                    yield c

    @classmethod
    def _get_elements_in_container(cls, container):
        """Return a list of response elements from an XML response element container. With e.g.
        'CreateItem', 'Items' is the container element and we return the 'Message' child elements:

          <m:Items>
            <t:Message>
              <t:ItemId Id="AQApA=" ChangeKey="AQAAAB"/>
            </t:Message>
          </m:Items>

        If the service does not return response elements, return True to indicate the status. Errors have already been
        raised.
        """
        if cls.returns_elements:
            return list(container)
        return [True]

    def _get_elems_from_page(self, elem, max_items, total_item_count):
        container = elem.find(self.element_container_name)
        if container is None:
            raise MalformedResponseError('No %s elements in ResponseMessage (%s)' % (
                self.element_container_name, xml_to_str(elem)))
        for e in self._get_elements_in_container(container=container):
            if max_items and total_item_count >= max_items:
                # No need to continue. Break out of elements loop
                log.debug("'max_items' count reached (elements)")
                break
            yield e

    def _get_pages(self, payload_func, kwargs, expected_message_count):
        """Request a page, or a list of pages if multiple collections are pages in a single request. Return each
        page.
        """
        payload = payload_func(**kwargs)
        page_elems = list(self._get_elements(payload=payload))
        if len(page_elems) != expected_message_count:
            raise MalformedResponseError(
                "Expected %s items in 'response', got %s" % (expected_message_count, len(page_elems))
            )
        return page_elems

    @staticmethod
    def _get_next_offset(paging_infos):
        next_offsets = {p['next_offset'] for p in paging_infos if p['next_offset'] is not None}
        if not next_offsets:
            # Paging is done for all messages
            return None
        # We cannot guarantee that all messages that have a next_offset also have the *same* next_offset. This is
        # because the collections that we are iterating may change while iterating. We'll do our best but we cannot
        # guarantee 100% consistency when large collections are simultaneously being changed on the server.
        #
        # It's not possible to supply a per-folder offset when iterating multiple folders, so we'll just have to
        # choose something that is most likely to work. Select the lowest of all the values to at least make sure
        # we don't miss any items, although we may then get duplicates ¯\_(ツ)_/¯
        if len(next_offsets) > 1:
            log.warning('Inconsistent next_offset values: %r. Using lowest value', next_offsets)
        return min(next_offsets)

    def _paged_call(self, payload_func, max_items, folders, **kwargs):
        """Call a service that supports paging requests. Return a generator over all response items. Keeps track of
        all paging-related counters.
        """
        paging_infos = {f: dict(item_count=0, next_offset=None) for f in folders}
        common_next_offset = kwargs['offset']
        total_item_count = 0
        while True:
            if not paging_infos:
                # Paging is done for all folders
                break
            log.debug('Getting page at offset %s (max_items %s)', common_next_offset, max_items)
            kwargs['offset'] = common_next_offset
            kwargs['folders'] = paging_infos.keys()  # Only request the paging of the remaining folders.
            pages = self._get_pages(payload_func, kwargs, len(paging_infos))
            for (page, next_offset), (f, paging_info) in zip(pages, list(paging_infos.items())):
                paging_info['next_offset'] = next_offset
                if isinstance(page, Exception):
                    # Assume this folder no longer works. Don't attempt to page it again.
                    log.debug('Exception occurred for folder %s. Removing.', f)
                    del paging_infos[f]
                    yield page
                    continue
                if page is not None:
                    for elem in self._get_elems_from_page(page, max_items, total_item_count):
                        paging_info['item_count'] += 1
                        total_item_count += 1
                        yield elem
                    if max_items and total_item_count >= max_items:
                        # No need to continue. Break out of inner loop
                        log.debug("'max_items' count reached (inner)")
                        break
                if not paging_info['next_offset']:
                    # Paging is done for this folder. Don't attempt to page it again.
                    log.debug('Paging has completed for folder %s. Removing.', f)
                    del paging_infos[f]
                    continue
                log.debug('Folder %s still has items', f)
                # Check sanity of paging offsets, but don't fail. When we are iterating huge collections that take a
                # long time to complete, the collection may change while we are iterating. This can affect the
                # 'next_offset' value and make it inconsistent with the number of already collected items.
                # We may have a mismatch if we stopped early due to reaching 'max_items'.
                if paging_info['next_offset'] != paging_info['item_count'] and (
                    not max_items or total_item_count < max_items
                ):
                    log.warning('Unexpected next offset: %s -> %s. Maybe the server-side collection has changed?',
                                paging_info['item_count'], paging_info['next_offset'])
            # Also break out of outer loop
            if max_items and total_item_count >= max_items:
                log.debug("'max_items' count reached (outer)")
                break
            common_next_offset = self._get_next_offset(paging_infos.values())
            if common_next_offset is None:
                # Paging is done for all folders
                break

    @staticmethod
    def _get_paging_values(elem):
        """Read paging information from the paging container element."""
        offset_attr = elem.get('IndexedPagingOffset')
        next_offset = None if offset_attr is None else int(offset_attr)
        item_count = int(elem.get('TotalItemsInView'))
        is_last_page = elem.get('IncludesLastItemInRange').lower() in ('true', '0')
        log.debug('Got page with offset %s, item_count %s, last_page %s', next_offset, item_count, is_last_page)
        # Clean up contradictory paging values
        if next_offset is None and not is_last_page:
            log.debug("Not last page in range, but server didn't send a page offset. Assuming first page")
            next_offset = 1
        if next_offset is not None and is_last_page:
            if next_offset != item_count:
                log.debug("Last page in range, but we still got an offset. Assuming paging has completed")
            next_offset = None
        if not item_count and not is_last_page:
            log.debug("Not last page in range, but also no items left. Assuming paging has completed")
            next_offset = None
        if item_count and next_offset == 0:
            log.debug("Non-zero offset, but also no items left. Assuming paging has completed")
            next_offset = None
        return item_count, next_offset

    def _get_page(self, message):
        """Get a single page from a request message, and return the container and next offset."""
        paging_elem = self._get_element_container(message=message, name=self.paging_container_name)
        if isinstance(paging_elem, Exception):
            return paging_elem, None
        item_count, next_offset = self._get_paging_values(paging_elem)
        if not item_count:
            paging_elem = None
        return paging_elem, next_offset


class EWSAccountService(EWSService, metaclass=abc.ABCMeta):
    """Base class for services that act on items concerning a single Mailbox on the server."""

    NO_VALID_SERVER_VERSIONS = ErrorInvalidSchemaVersionForMailboxVersion

    def __init__(self, *args, **kwargs):
        self.account = kwargs.pop('account')
        kwargs['protocol'] = self.account.protocol
        super().__init__(*args, **kwargs)

    @property
    def _version_hint(self):
        return self.account.version

    @_version_hint.setter
    def _version_hint(self, value):
        self.account.version = value

    def _extra_headers(self, *args, **kwargs):
        headers = super()._extra_headers(*args, **kwargs)
        # See
        # https://blogs.msdn.microsoft.com/webdav_101/2015/05/11/best-practices-ews-authentication-and-access-issues/
        headers['X-AnchorMailbox'] = self.account.primary_smtp_address
        return headers

    @property
    def _account_to_impersonate(self):
        if self.account.access_type == IMPERSONATION:
            return self.account.identity
        return None

    @property
    def _timezone(self):
        return self.account.default_timezone


def to_item_id(item, item_cls, version):
    # Coerce a tuple, dict or object to an 'item_cls' instance. Used to create [Parent][Item|Folder]Id instances from a
    # variety of input.
    if isinstance(item, item_cls):
        # Allow any subclass of item_cls, e.g. OccurrenceItemId when ItemId is passed
        return item
    from ..folders import BaseFolder
    from ..items import BaseItem
    if isinstance(item, (BaseFolder, BaseItem)):
        return item.to_id_xml(version=version)
    if isinstance(item, (tuple, list)):
        return item_cls(*item)
    if isinstance(item, dict):
        return item_cls(**item)
    return item_cls(item.id, item.changekey)


def create_shape_element(tag, shape, additional_fields, version):
    shape_elem = create_element(tag)
    add_xml_child(shape_elem, 't:BaseShape', shape)
    if additional_fields:
        additional_properties = create_element('t:AdditionalProperties')
        expanded_fields = chain(*(f.expand(version=version) for f in additional_fields))
        # 'path' is insufficient to consistently sort additional properties. For example, we have both
        # 'contacts:Companies' and 'task:Companies' with path 'companies'. Sort by both 'field_uri' and 'path'.
        # Extended properties do not have a 'field_uri' value.
        set_xml_value(additional_properties, sorted(
            expanded_fields,
            key=lambda f: (getattr(f.field, 'field_uri', ''), f.path)
        ), version=version)
        shape_elem.append(additional_properties)
    return shape_elem


def create_folder_ids_element(tag, folders, version):
    from ..folders import FolderId
    folder_ids = create_element(tag)
    for folder in folders:
        if not isinstance(folder, FolderId):
            folder = to_item_id(folder, FolderId, version=version)
        set_xml_value(folder_ids, folder, version=version)
    if not len(folder_ids):
        raise ValueError('"folders" must not be empty')
    return folder_ids


def create_item_ids_element(items, version, tag='m:ItemIds'):
    item_ids = create_element(tag)
    for item in items:
        set_xml_value(item_ids, to_item_id(item, ItemId, version=version), version=version)
    if not len(item_ids):
        raise ValueError('"items" must not be empty')
    return item_ids


def create_attachment_ids_element(items, version):
    from ..attachments import AttachmentId
    attachment_ids = create_element('m:AttachmentIds')
    for item in items:
        attachment_id = item if isinstance(item, AttachmentId) else AttachmentId(id=item)
        set_xml_value(attachment_ids, attachment_id, version=version)
    if not len(attachment_ids):
        raise ValueError('"items" must not be empty')
    return attachment_ids


def parse_folder_elem(elem, folder, account):
    from ..folders import BaseFolder, Folder, DistinguishedFolderId, RootOfHierarchy
    if isinstance(elem, Exception):
        return elem
    if isinstance(folder, RootOfHierarchy):
        f = folder.from_xml(elem=elem, account=folder.account)
    elif isinstance(folder, Folder):
        f = folder.from_xml_with_root(elem=elem, root=folder.root)
    elif isinstance(folder, DistinguishedFolderId):
        # We don't know the root, so assume account.root.
        folder_cls = None
        for cls in account.root.WELLKNOWN_FOLDERS:
            if cls.DISTINGUISHED_FOLDER_ID == folder.id:
                folder_cls = cls
                break
        if not folder_cls:
            raise ValueError('Unknown distinguished folder ID: %s' % folder.id)
        f = folder_cls.from_xml_with_root(elem=elem, root=account.root)
    else:
        # 'folder' is a generic FolderId instance. We don't know the root so assume account.root.
        f = Folder.from_xml_with_root(elem=elem, root=account.root)
    if isinstance(folder, DistinguishedFolderId):
        f.is_distinguished = True
    elif isinstance(folder, BaseFolder) and folder.is_distinguished:
        f.is_distinguished = True
    return f

Functions

def create_attachment_ids_element(items, version)
Expand source code
def create_attachment_ids_element(items, version):
    from ..attachments import AttachmentId
    attachment_ids = create_element('m:AttachmentIds')
    for item in items:
        attachment_id = item if isinstance(item, AttachmentId) else AttachmentId(id=item)
        set_xml_value(attachment_ids, attachment_id, version=version)
    if not len(attachment_ids):
        raise ValueError('"items" must not be empty')
    return attachment_ids
def create_folder_ids_element(tag, folders, version)
Expand source code
def create_folder_ids_element(tag, folders, version):
    from ..folders import FolderId
    folder_ids = create_element(tag)
    for folder in folders:
        if not isinstance(folder, FolderId):
            folder = to_item_id(folder, FolderId, version=version)
        set_xml_value(folder_ids, folder, version=version)
    if not len(folder_ids):
        raise ValueError('"folders" must not be empty')
    return folder_ids
def create_item_ids_element(items, version, tag='m:ItemIds')
Expand source code
def create_item_ids_element(items, version, tag='m:ItemIds'):
    item_ids = create_element(tag)
    for item in items:
        set_xml_value(item_ids, to_item_id(item, ItemId, version=version), version=version)
    if not len(item_ids):
        raise ValueError('"items" must not be empty')
    return item_ids
def create_shape_element(tag, shape, additional_fields, version)
Expand source code
def create_shape_element(tag, shape, additional_fields, version):
    shape_elem = create_element(tag)
    add_xml_child(shape_elem, 't:BaseShape', shape)
    if additional_fields:
        additional_properties = create_element('t:AdditionalProperties')
        expanded_fields = chain(*(f.expand(version=version) for f in additional_fields))
        # 'path' is insufficient to consistently sort additional properties. For example, we have both
        # 'contacts:Companies' and 'task:Companies' with path 'companies'. Sort by both 'field_uri' and 'path'.
        # Extended properties do not have a 'field_uri' value.
        set_xml_value(additional_properties, sorted(
            expanded_fields,
            key=lambda f: (getattr(f.field, 'field_uri', ''), f.path)
        ), version=version)
        shape_elem.append(additional_properties)
    return shape_elem
def parse_folder_elem(elem, folder, account)
Expand source code
def parse_folder_elem(elem, folder, account):
    from ..folders import BaseFolder, Folder, DistinguishedFolderId, RootOfHierarchy
    if isinstance(elem, Exception):
        return elem
    if isinstance(folder, RootOfHierarchy):
        f = folder.from_xml(elem=elem, account=folder.account)
    elif isinstance(folder, Folder):
        f = folder.from_xml_with_root(elem=elem, root=folder.root)
    elif isinstance(folder, DistinguishedFolderId):
        # We don't know the root, so assume account.root.
        folder_cls = None
        for cls in account.root.WELLKNOWN_FOLDERS:
            if cls.DISTINGUISHED_FOLDER_ID == folder.id:
                folder_cls = cls
                break
        if not folder_cls:
            raise ValueError('Unknown distinguished folder ID: %s' % folder.id)
        f = folder_cls.from_xml_with_root(elem=elem, root=account.root)
    else:
        # 'folder' is a generic FolderId instance. We don't know the root so assume account.root.
        f = Folder.from_xml_with_root(elem=elem, root=account.root)
    if isinstance(folder, DistinguishedFolderId):
        f.is_distinguished = True
    elif isinstance(folder, BaseFolder) and folder.is_distinguished:
        f.is_distinguished = True
    return f
def to_item_id(item, item_cls, version)
Expand source code
def to_item_id(item, item_cls, version):
    # Coerce a tuple, dict or object to an 'item_cls' instance. Used to create [Parent][Item|Folder]Id instances from a
    # variety of input.
    if isinstance(item, item_cls):
        # Allow any subclass of item_cls, e.g. OccurrenceItemId when ItemId is passed
        return item
    from ..folders import BaseFolder
    from ..items import BaseItem
    if isinstance(item, (BaseFolder, BaseItem)):
        return item.to_id_xml(version=version)
    if isinstance(item, (tuple, list)):
        return item_cls(*item)
    if isinstance(item, dict):
        return item_cls(**item)
    return item_cls(item.id, item.changekey)

Classes

class EWSAccountService (*args, **kwargs)

Base class for services that act on items concerning a single Mailbox on the server.

Expand source code
class EWSAccountService(EWSService, metaclass=abc.ABCMeta):
    """Base class for services that act on items concerning a single Mailbox on the server."""

    NO_VALID_SERVER_VERSIONS = ErrorInvalidSchemaVersionForMailboxVersion

    def __init__(self, *args, **kwargs):
        self.account = kwargs.pop('account')
        kwargs['protocol'] = self.account.protocol
        super().__init__(*args, **kwargs)

    @property
    def _version_hint(self):
        return self.account.version

    @_version_hint.setter
    def _version_hint(self, value):
        self.account.version = value

    def _extra_headers(self, *args, **kwargs):
        headers = super()._extra_headers(*args, **kwargs)
        # See
        # https://blogs.msdn.microsoft.com/webdav_101/2015/05/11/best-practices-ews-authentication-and-access-issues/
        headers['X-AnchorMailbox'] = self.account.primary_smtp_address
        return headers

    @property
    def _account_to_impersonate(self):
        if self.account.access_type == IMPERSONATION:
            return self.account.identity
        return None

    @property
    def _timezone(self):
        return self.account.default_timezone

Ancestors

Subclasses

Inherited members

class EWSService (protocol, chunk_size=None, timeout=None)

Base class for all EWS services.

Expand source code
class EWSService(metaclass=abc.ABCMeta):
    """Base class for all EWS services."""

    SERVICE_NAME = None  # The name of the SOAP service
    element_container_name = None  # The name of the XML element wrapping the collection of returned items
    paging_container_name = None  # The name of the element that contains paging information and the paged results
    returns_elements = True  # If False, the service does not return response elements, just the RsponseCode status
    # Return exception instance instead of raising exceptions for the following errors when contained in an element
    ERRORS_TO_CATCH_IN_RESPONSE = (
        EWSWarning, ErrorCannotDeleteObject, ErrorInvalidChangeKey, ErrorItemNotFound, ErrorItemSave,
        ErrorInvalidIdMalformed, ErrorMessageSizeExceeded, ErrorCannotDeleteTaskOccurrence,
        ErrorMimeContentConversionFailed, ErrorRecurrenceHasNoOccurrence, ErrorCorruptData
    )
    # Similarly, define the warnings we want to return unraised
    WARNINGS_TO_CATCH_IN_RESPONSE = ErrorBatchProcessingStopped
    # Define the warnings we want to ignore, to let response processing proceed
    WARNINGS_TO_IGNORE_IN_RESPONSE = ()
    # The exception type to raise when all attempted API versions failed
    NO_VALID_SERVER_VERSIONS = ErrorInvalidServerVersion
    # Marks the version from which the service was introduced
    supported_from = None
    # Marks services that support paging of requested items
    supports_paging = False
    # Marks services that need affinity to the backend server
    prefer_affinity = False

    def __init__(self, protocol, chunk_size=None, timeout=None):
        self.chunk_size = chunk_size or CHUNK_SIZE  # The number of items to send in a single request
        if not isinstance(self.chunk_size, int):
            raise ValueError("'chunk_size' %r must be an integer" % chunk_size)
        if self.chunk_size < 1:
            raise ValueError("'chunk_size' must be a positive number")
        if self.supported_from and protocol.version.build < self.supported_from:
            raise NotImplementedError(
                '%r is only supported on %r and later' % (self.SERVICE_NAME, self.supported_from.fullname())
            )
        self.protocol = protocol
        # Allow a service to override the default protocol timeout. Useful for streaming services
        self.timeout = timeout
        # Controls whether the HTTP request should be streaming or fetch everything at once
        self.streaming = False
        # Streaming connection variables
        self._streaming_session = None
        self._streaming_response = None

    # The following two methods are the minimum required to be implemented by subclasses, but the name and number of
    # kwargs differs between services. Therefore, we cannot make these methods abstract.

    # @abc.abstractmethod
    # def call(self, **kwargs):
    #     """Defines the arguments required by the service. Arguments are basic Python types or EWSElement objects.
    #     Returns either XML objects or EWSElement objects.
    #     """"
    #     pass

    # @abc.abstractmethod
    # def get_payload(self, **kwargs):
    #     """Using the arguments from .call(), return the payload expected by the service, as an XML object. The XML
    #     object should consist of a SERVICE_NAME element and everything within that.
    #     """
    #     pass

    def get(self, expect_result=True, **kwargs):
        """Like .call(), but expects exactly one result from the server, or zero when expect_result=False, or either
        zero or one when expect_result=None. Returns either one object or None.

        :param expect_result: None, True, or False
        :param kwargs: Same as arguments for .call()
        :return: Same as .call(), but returns either None or exactly one item
        """
        res = list(self.call(**kwargs))
        # Raise any errors
        for r in res:
            if isinstance(r, Exception):
                raise r
        if expect_result is None and not res:
            # Allow empty result
            return None
        if expect_result is False:
            if res:
                raise ValueError('Expected result length 0, but got %r' % res)
            return None
        if len(res) != 1:
            raise ValueError('Expected result length 1, but got %r' % res)
        return res[0]

    def parse(self, xml):
        """Used mostly for testing, when we want to parse static XML data."""
        resp = DummyResponse(url=None, headers=None, request_headers=None, content=xml)
        _, body = self._get_soap_parts(response=resp)
        return self._elems_to_objs(self._get_elements_in_response(response=self._get_soap_messages(body=body)))

    def _elems_to_objs(self, elems):
        """Takes a generator of XML elements and exceptions. Returns the equivalent Python objects (or exceptions)."""
        raise NotImplementedError()

    @property
    def _version_hint(self):
        # We may be here due to version guessing in Protocol.version, so we can't use the self.protocol.version property
        return self.protocol.config.version

    @_version_hint.setter
    def _version_hint(self, value):
        self.protocol.config.version = value

    def _extra_headers(self, session):
        headers = {}
        if self.prefer_affinity:
            headers['X-PreferServerAffinity'] = 'True'
        for cookie in session.cookies:
            if cookie.name == 'X-BackEndCookie':
                headers['X-BackEndOverrideCookie'] = cookie.value
        return headers

    @property
    def _account_to_impersonate(self):
        if isinstance(self.protocol.credentials, OAuth2Credentials):
            return self.protocol.credentials.identity
        return None

    @property
    def _timezone(self):
        return None

    def _response_generator(self, payload):
        """Send the payload to the server, and return the response.

        :param payload: payload as an XML object
        :return: the response, as XML objects
        """
        response = self._get_response_xml(payload=payload)
        if self.supports_paging:
            return (self._get_page(message) for message in response)
        return self._get_elements_in_response(response=response)

    def _chunked_get_elements(self, payload_func, items, **kwargs):
        """Yield elements in a response. Like ._get_elements(), but chop items into suitable chunks and send multiple
        requests.

        :param payload_func: A reference to .payload()
        :param items: An iterable of items (messages, folders, etc.) to process
        :param kwargs: Same as arguments for .call(), except for the 'items' argument
        :return: Same as ._get_elements()
        """
        for i, chunk in enumerate(chunkify(items, self.chunk_size), start=1):
            log.debug('Processing chunk %s containing %s items', i, len(chunk))
            yield from self._get_elements(payload=payload_func(chunk, **kwargs))

    def stop_streaming(self):
        if self._streaming_response:
            self._streaming_response.close()  # Release memory
            self._streaming_response = None
        if self._streaming_session:
            self.protocol.release_session(self._streaming_session)
            self._streaming_session = None

    def _get_elements(self, payload):
        """Send the payload to be sent and parsed. Handles and re-raise exceptions that are not meant to be returned
        to the caller as exception objects. Retry the request according to the retry policy.
        """
        while True:
            try:
                # Create a generator over the response elements so exceptions in response elements are also raised
                # here and can be handled.
                yield from self._response_generator(payload=payload)
                return
            except ErrorServerBusy as e:
                self._handle_backoff(e)
                continue
            except KNOWN_EXCEPTIONS:
                # These are known and understood, and don't require a backtrace.
                raise
            except (ErrorTooManyObjectsOpened, ErrorTimeoutExpired) as e:
                # ErrorTooManyObjectsOpened means there are too many connections to the Exchange database. This is very
                # often a symptom of sending too many requests.
                #
                # ErrorTimeoutExpired can be caused by a busy server, or by overly large requests. Start by lowering the
                # session count. This is done by downstream code.
                if isinstance(e, ErrorTimeoutExpired) and self.protocol.session_pool_size <= 1:
                    # We're already as low as we can go, so downstream cannot limit the session count to put less load
                    # on the server. We don't have a way of lowering the page size of requests from
                    # this part of the code yet. Let the user handle this.
                    raise e

                # Re-raise as an ErrorServerBusy with a default delay of 5 minutes
                raise ErrorServerBusy('Reraised from %s(%s)' % (e.__class__.__name__, e))
            except Exception:
                # This may run from a thread pool, which obfuscates the stack trace. Print trace immediately.
                account = self.account if isinstance(self, EWSAccountService) else None
                log.warning('Account %s: Exception in _get_elements: %s', account, traceback.format_exc(20))
                raise
            finally:
                if self.streaming:
                    self.stop_streaming()

    def _get_response(self, payload, api_version):
        """Send the actual HTTP request and get the response."""
        session = self.protocol.get_session()
        self._streaming_session, self._streaming_response = None, None
        r, session = post_ratelimited(
            protocol=self.protocol,
            session=session,
            url=self.protocol.service_endpoint,
            headers=self._extra_headers(session),
            data=wrap(
                content=payload,
                api_version=api_version,
                account_to_impersonate=self._account_to_impersonate,
                timezone=self._timezone,
            ),
            allow_redirects=False,
            stream=self.streaming,
            timeout=self.timeout or self.protocol.TIMEOUT,
        )
        if self.streaming:
            # We con only release the session when we have fully consumed the response. Save session and response
            # objects for later.
            self._streaming_session, self._streaming_response = session, r
        else:
            self.protocol.release_session(session)
        return r

    @property
    def _api_versions_to_try(self):
        # Put the hint first in the list, and then all other versions except the hint, from newest to oldest
        return [self._version_hint.api_version] + [v for v in API_VERSIONS if v != self._version_hint.api_version]

    def _get_response_xml(self, payload, **parse_opts):
        """Send the payload to the server and return relevant elements from the result. Several things happen here:
          * The payload is wrapped in SOAP headers and sent to the server
          * The Exchange API version is negotiated and stored in the protocol object
          * Connection errors are handled and possibly reraised as ErrorServerBusy
          * SOAP errors are raised
          * EWS errors are raised, or passed on to the caller

        :param payload: The request payload, as an XML object
        :return: A generator of XML objects or None if the service does not return a result
        """
        # Microsoft really doesn't want to make our lives easy. The server may report one version in our initial version
        # guessing tango, but then the server may decide that any arbitrary legacy backend server may actually process
        # the request for an account. Prepare to handle version-related errors and set the server version per-account.
        log.debug('Calling service %s', self.SERVICE_NAME)
        for api_version in self._api_versions_to_try:
            log.debug('Trying API version %s', api_version)
            r = self._get_response(payload=payload, api_version=api_version)
            if self.streaming:
                # Let 'requests' decode raw data automatically
                r.raw.decode_content = True
            try:
                header, body = self._get_soap_parts(response=r, **parse_opts)
            except Exception:
                r.close()  # Release memory
                raise
            # The body may contain error messages from Exchange, but we still want to collect version info
            if header is not None:
                self._update_api_version(api_version=api_version, header=header, **parse_opts)
            try:
                return self._get_soap_messages(body=body, **parse_opts)
            except (ErrorInvalidServerVersion, ErrorIncorrectSchemaVersion, ErrorInvalidRequest,
                    ErrorInvalidSchemaVersionForMailboxVersion):
                # The guessed server version is wrong. Try the next version
                log.debug('API version %s was invalid', api_version)
                continue
            except ErrorExceededConnectionCount as e:
                # This indicates that the connecting user has too many open TCP connections to the server. Decrease
                # our session pool size.
                try:
                    self.protocol.decrease_poolsize()
                    continue
                except SessionPoolMinSizeReached:
                    # We're already as low as we can go. Let the user handle this.
                    raise e
            finally:
                if not self.streaming:
                    # In streaming mode, we may not have accessed the raw stream yet. Caller must handle this.
                    r.close()  # Release memory

        raise self.NO_VALID_SERVER_VERSIONS('Tried versions %s but all were invalid' % self._api_versions_to_try)

    def _handle_backoff(self, e):
        """Take a request from the server to back off and checks the retry policy for what to do. Re-raise the
        exception if conditions are not met.

        :param e: An ErrorServerBusy instance
        :return:
        """
        log.debug('Got ErrorServerBusy (back off %s seconds)', e.back_off)
        # ErrorServerBusy is very often a symptom of sending too many requests. Scale back connections if possible.
        try:
            self.protocol.decrease_poolsize()
        except SessionPoolMinSizeReached:
            pass
        if self.protocol.retry_policy.fail_fast:
            raise e
        self.protocol.retry_policy.back_off(e.back_off)
        # We'll warn about this later if we actually need to sleep

    def _update_api_version(self, api_version, header, **parse_opts):
        """Parse the server version contained in SOAP headers and update the version hint stored by the caller, if
        necessary.
        """
        try:
            head_version = Version.from_soap_header(requested_api_version=api_version, header=header)
        except TransportError as te:
            log.debug('Failed to update version info (%s)', te)
            return
        if self._version_hint == head_version:
            # Nothing to do
            return
        log.debug('Found new version (%s -> %s)', self._version_hint, head_version)
        # The api_version that worked was different than our hint, or we never got a build version. Store the working
        # version.
        self._version_hint = head_version

    @classmethod
    def _response_tag(cls):
        """Return the name of the element containing the service response."""
        return '{%s}%sResponse' % (MNS, cls.SERVICE_NAME)

    @staticmethod
    def _response_messages_tag():
        """Return the name of the element containing service response messages."""
        return '{%s}ResponseMessages' % MNS

    @classmethod
    def _response_message_tag(cls):
        """Return the name of the element of a single response message."""
        return '{%s}%sResponseMessage' % (MNS, cls.SERVICE_NAME)

    @classmethod
    def _get_soap_parts(cls, response, **parse_opts):
        """Split the SOAP response into its headers an body elements."""
        try:
            root = to_xml(response.iter_content())
        except ParseError as e:
            raise SOAPError('Bad SOAP response: %s' % e)
        header = root.find('{%s}Header' % SOAPNS)
        if header is None:
            # This is normal when the response contains SOAP-level errors
            log.debug('No header in XML response')
        body = root.find('{%s}Body' % SOAPNS)
        if body is None:
            raise MalformedResponseError('No Body element in SOAP response')
        return header, body

    def _get_soap_messages(self, body, **parse_opts):
        """Return the elements in the response containing the response messages. Raises any SOAP exceptions."""
        response = body.find(self._response_tag())
        if response is None:
            fault = body.find('{%s}Fault' % SOAPNS)
            if fault is None:
                raise SOAPError(
                    'Unknown SOAP response (expected %s or Fault): %s' % (self._response_tag(), xml_to_str(body))
                )
            self._raise_soap_errors(fault=fault)  # Will throw SOAPError or custom EWS error
        response_messages = response.find(self._response_messages_tag())
        if response_messages is None:
            # Result isn't delivered in a list of FooResponseMessages, but directly in the FooResponse. Consumers expect
            # a list, so return a list
            return [response]
        return response_messages.findall(self._response_message_tag())

    @classmethod
    def _raise_soap_errors(cls, fault):
        """Parse error messages contained in SOAP headers and raise as exceptions defined in this package."""
        # Fault: See http://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383507
        faultcode = get_xml_attr(fault, 'faultcode')
        faultstring = get_xml_attr(fault, 'faultstring')
        faultactor = get_xml_attr(fault, 'faultactor')
        detail = fault.find('detail')
        if detail is not None:
            code, msg = None, ''
            if detail.find('{%s}ResponseCode' % ENS) is not None:
                code = get_xml_attr(detail, '{%s}ResponseCode' % ENS).strip()
            if detail.find('{%s}Message' % ENS) is not None:
                msg = get_xml_attr(detail, '{%s}Message' % ENS).strip()
            msg_xml = detail.find('{%s}MessageXml' % TNS)  # Crazy. Here, it's in the TNS namespace
            if code == 'ErrorServerBusy':
                back_off = None
                try:
                    value = msg_xml.find('{%s}Value' % TNS)
                    if value.get('Name') == 'BackOffMilliseconds':
                        back_off = int(value.text) / 1000.0  # Convert to seconds
                except (TypeError, AttributeError):
                    pass
                raise ErrorServerBusy(msg, back_off=back_off)
            if code == 'ErrorSchemaValidation' and msg_xml is not None:
                violation = get_xml_attr(msg_xml, '{%s}Violation' % TNS)
                if violation is not None:
                    msg = '%s %s' % (msg, violation)
            try:
                raise vars(errors)[code](msg)
            except KeyError:
                detail = '%s: code: %s msg: %s (%s)' % (cls.SERVICE_NAME, code, msg, xml_to_str(detail))
        try:
            raise vars(errors)[faultcode](faultstring)
        except KeyError:
            pass
        raise SOAPError('SOAP error code: %s string: %s actor: %s detail: %s' % (
            faultcode, faultstring, faultactor, detail))

    def _get_element_container(self, message, name=None):
        """Return the XML element in a response element that contains the elements we want the service to return. For
        example, in a GetFolder response, 'message' is the GetFolderResponseMessage element, and we return the 'Folders'
        element:

        <m:GetFolderResponseMessage ResponseClass="Success">
          <m:ResponseCode>NoError</m:ResponseCode>
          <m:Folders>
            <t:Folder>
              <t:FolderId Id="AQApA=" ChangeKey="AQAAAB" />
              [...]
            </t:Folder>
          </m:Folders>
        </m:GetFolderResponseMessage>

        Some service responses don't have a containing element for the returned elements ('name' is None). In
        that case, we return the 'SomeServiceResponseMessage' element.

        If the response contains a warning or an error message, we raise the relevant exception, unless the error class
        is contained in WARNINGS_TO_CATCH_IN_RESPONSE or ERRORS_TO_CATCH_IN_RESPONSE, in which case we return the
        exception instance.
        """
        # ResponseClass is an XML attribute of various SomeServiceResponseMessage elements: Possible values are:
        # Success, Warning, Error. See e.g.
        # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/finditemresponsemessage
        response_class = message.get('ResponseClass')
        # ResponseCode, MessageText: See
        # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/responsecode
        response_code = get_xml_attr(message, '{%s}ResponseCode' % MNS)
        if response_class == 'Success' and response_code == 'NoError':
            if not name:
                return message
            container = message.find(name)
            if container is None:
                raise MalformedResponseError('No %s elements in ResponseMessage (%s)' % (name, xml_to_str(message)))
            return container
        if response_code == 'NoError':
            return True
        # Raise any non-acceptable errors in the container, or return the container or the acceptable exception instance
        msg_text = get_xml_attr(message, '{%s}MessageText' % MNS)
        msg_xml = message.find('{%s}MessageXml' % MNS)
        if response_class == 'Warning':
            try:
                raise self._get_exception(code=response_code, text=msg_text, msg_xml=msg_xml)
            except self.WARNINGS_TO_CATCH_IN_RESPONSE as e:
                return e
            except self.WARNINGS_TO_IGNORE_IN_RESPONSE as e:
                log.warning(str(e))
                container = message.find(name)
                if container is None:
                    raise MalformedResponseError('No %s elements in ResponseMessage (%s)' % (name, xml_to_str(message)))
                return container
        # rspclass == 'Error', or 'Success' and not 'NoError'
        try:
            raise self._get_exception(code=response_code, text=msg_text, msg_xml=msg_xml)
        except self.ERRORS_TO_CATCH_IN_RESPONSE as e:
            return e

    @staticmethod
    def _get_exception(code, text, msg_xml):
        """Parse error messages contained in EWS responses and raise as exceptions defined in this package."""
        if not code:
            return TransportError('Empty ResponseCode in ResponseMessage (MessageText: %s, MessageXml: %s)' % (
                text, msg_xml))
        if msg_xml is not None:
            # If this is an ErrorInvalidPropertyRequest error, the xml may contain a specific FieldURI
            for elem_cls in (FieldURI, IndexedFieldURI, ExtendedFieldURI, ExceptionFieldURI):
                elem = msg_xml.find(elem_cls.response_tag())
                if elem is not None:
                    field_uri = elem_cls.from_xml(elem, account=None)
                    text += ' (field: %s)' % field_uri
                    break

            # If this is an ErrorInvalidValueForProperty error, the xml may contain the name and value of the property
            if code == 'ErrorInvalidValueForProperty':
                msg_parts = {}
                for elem in msg_xml.findall('{%s}Value' % TNS):
                    key, val = elem.get('Name'), elem.text
                    if key:
                        msg_parts[key] = val
                if msg_parts:
                    text += ' (%s)' % ', '.join('%s: %s' % (k, v) for k, v in msg_parts.items())

            # If this is an ErrorInternalServerError error, the xml may contain a more specific error code
            inner_code, inner_text = None, None
            for value_elem in msg_xml.findall('{%s}Value' % TNS):
                name = value_elem.get('Name')
                if name == 'InnerErrorResponseCode':
                    inner_code = value_elem.text
                elif name == 'InnerErrorMessageText':
                    inner_text = value_elem.text
            if inner_code:
                try:
                    # Raise the error as the inner error code
                    return vars(errors)[inner_code]('%s (raised from: %s(%r))' % (inner_text, code, text))
                except KeyError:
                    # Inner code is unknown to us. Just append to the original text
                    text += ' (inner error: %s(%r))' % (inner_code, inner_text)
        try:
            # Raise the error corresponding to the ResponseCode
            return vars(errors)[code](text)
        except KeyError:
            # Should not happen
            return TransportError('Unknown ResponseCode in ResponseMessage: %s (MessageText: %s, MessageXml: %s)' % (
                    code, text, msg_xml))

    def _get_elements_in_response(self, response):
        """Take a list of 'SomeServiceResponseMessage' elements and return the elements in each response message that
        we want the service to return. With e.g. 'CreateItem', we get a list of 'CreateItemResponseMessage' elements
        and return the 'Message' elements.

        <m:CreateItemResponseMessage ResponseClass="Success">
          <m:ResponseCode>NoError</m:ResponseCode>
          <m:Items>
            <t:Message>
              <t:ItemId Id="AQApA=" ChangeKey="AQAAAB"/>
            </t:Message>
          </m:Items>
        </m:CreateItemResponseMessage>
        <m:CreateItemResponseMessage ResponseClass="Success">
          <m:ResponseCode>NoError</m:ResponseCode>
          <m:Items>
            <t:Message>
              <t:ItemId Id="AQApB=" ChangeKey="AQAAAC"/>
            </t:Message>
          </m:Items>
        </m:CreateItemResponseMessage>

        :param response: a list of 'SomeServiceResponseMessage' XML objects
        :return: a generator of items as returned by '_get_elements_in_container()
        """
        for msg in response:
            container_or_exc = self._get_element_container(message=msg, name=self.element_container_name)
            if isinstance(container_or_exc, (bool, Exception)):
                yield container_or_exc
            else:
                for c in self._get_elements_in_container(container=container_or_exc):
                    yield c

    @classmethod
    def _get_elements_in_container(cls, container):
        """Return a list of response elements from an XML response element container. With e.g.
        'CreateItem', 'Items' is the container element and we return the 'Message' child elements:

          <m:Items>
            <t:Message>
              <t:ItemId Id="AQApA=" ChangeKey="AQAAAB"/>
            </t:Message>
          </m:Items>

        If the service does not return response elements, return True to indicate the status. Errors have already been
        raised.
        """
        if cls.returns_elements:
            return list(container)
        return [True]

    def _get_elems_from_page(self, elem, max_items, total_item_count):
        container = elem.find(self.element_container_name)
        if container is None:
            raise MalformedResponseError('No %s elements in ResponseMessage (%s)' % (
                self.element_container_name, xml_to_str(elem)))
        for e in self._get_elements_in_container(container=container):
            if max_items and total_item_count >= max_items:
                # No need to continue. Break out of elements loop
                log.debug("'max_items' count reached (elements)")
                break
            yield e

    def _get_pages(self, payload_func, kwargs, expected_message_count):
        """Request a page, or a list of pages if multiple collections are pages in a single request. Return each
        page.
        """
        payload = payload_func(**kwargs)
        page_elems = list(self._get_elements(payload=payload))
        if len(page_elems) != expected_message_count:
            raise MalformedResponseError(
                "Expected %s items in 'response', got %s" % (expected_message_count, len(page_elems))
            )
        return page_elems

    @staticmethod
    def _get_next_offset(paging_infos):
        next_offsets = {p['next_offset'] for p in paging_infos if p['next_offset'] is not None}
        if not next_offsets:
            # Paging is done for all messages
            return None
        # We cannot guarantee that all messages that have a next_offset also have the *same* next_offset. This is
        # because the collections that we are iterating may change while iterating. We'll do our best but we cannot
        # guarantee 100% consistency when large collections are simultaneously being changed on the server.
        #
        # It's not possible to supply a per-folder offset when iterating multiple folders, so we'll just have to
        # choose something that is most likely to work. Select the lowest of all the values to at least make sure
        # we don't miss any items, although we may then get duplicates ¯\_(ツ)_/¯
        if len(next_offsets) > 1:
            log.warning('Inconsistent next_offset values: %r. Using lowest value', next_offsets)
        return min(next_offsets)

    def _paged_call(self, payload_func, max_items, folders, **kwargs):
        """Call a service that supports paging requests. Return a generator over all response items. Keeps track of
        all paging-related counters.
        """
        paging_infos = {f: dict(item_count=0, next_offset=None) for f in folders}
        common_next_offset = kwargs['offset']
        total_item_count = 0
        while True:
            if not paging_infos:
                # Paging is done for all folders
                break
            log.debug('Getting page at offset %s (max_items %s)', common_next_offset, max_items)
            kwargs['offset'] = common_next_offset
            kwargs['folders'] = paging_infos.keys()  # Only request the paging of the remaining folders.
            pages = self._get_pages(payload_func, kwargs, len(paging_infos))
            for (page, next_offset), (f, paging_info) in zip(pages, list(paging_infos.items())):
                paging_info['next_offset'] = next_offset
                if isinstance(page, Exception):
                    # Assume this folder no longer works. Don't attempt to page it again.
                    log.debug('Exception occurred for folder %s. Removing.', f)
                    del paging_infos[f]
                    yield page
                    continue
                if page is not None:
                    for elem in self._get_elems_from_page(page, max_items, total_item_count):
                        paging_info['item_count'] += 1
                        total_item_count += 1
                        yield elem
                    if max_items and total_item_count >= max_items:
                        # No need to continue. Break out of inner loop
                        log.debug("'max_items' count reached (inner)")
                        break
                if not paging_info['next_offset']:
                    # Paging is done for this folder. Don't attempt to page it again.
                    log.debug('Paging has completed for folder %s. Removing.', f)
                    del paging_infos[f]
                    continue
                log.debug('Folder %s still has items', f)
                # Check sanity of paging offsets, but don't fail. When we are iterating huge collections that take a
                # long time to complete, the collection may change while we are iterating. This can affect the
                # 'next_offset' value and make it inconsistent with the number of already collected items.
                # We may have a mismatch if we stopped early due to reaching 'max_items'.
                if paging_info['next_offset'] != paging_info['item_count'] and (
                    not max_items or total_item_count < max_items
                ):
                    log.warning('Unexpected next offset: %s -> %s. Maybe the server-side collection has changed?',
                                paging_info['item_count'], paging_info['next_offset'])
            # Also break out of outer loop
            if max_items and total_item_count >= max_items:
                log.debug("'max_items' count reached (outer)")
                break
            common_next_offset = self._get_next_offset(paging_infos.values())
            if common_next_offset is None:
                # Paging is done for all folders
                break

    @staticmethod
    def _get_paging_values(elem):
        """Read paging information from the paging container element."""
        offset_attr = elem.get('IndexedPagingOffset')
        next_offset = None if offset_attr is None else int(offset_attr)
        item_count = int(elem.get('TotalItemsInView'))
        is_last_page = elem.get('IncludesLastItemInRange').lower() in ('true', '0')
        log.debug('Got page with offset %s, item_count %s, last_page %s', next_offset, item_count, is_last_page)
        # Clean up contradictory paging values
        if next_offset is None and not is_last_page:
            log.debug("Not last page in range, but server didn't send a page offset. Assuming first page")
            next_offset = 1
        if next_offset is not None and is_last_page:
            if next_offset != item_count:
                log.debug("Last page in range, but we still got an offset. Assuming paging has completed")
            next_offset = None
        if not item_count and not is_last_page:
            log.debug("Not last page in range, but also no items left. Assuming paging has completed")
            next_offset = None
        if item_count and next_offset == 0:
            log.debug("Non-zero offset, but also no items left. Assuming paging has completed")
            next_offset = None
        return item_count, next_offset

    def _get_page(self, message):
        """Get a single page from a request message, and return the container and next offset."""
        paging_elem = self._get_element_container(message=message, name=self.paging_container_name)
        if isinstance(paging_elem, Exception):
            return paging_elem, None
        item_count, next_offset = self._get_paging_values(paging_elem)
        if not item_count:
            paging_elem = None
        return paging_elem, next_offset

Subclasses

Class variables

var ERRORS_TO_CATCH_IN_RESPONSE
var NO_VALID_SERVER_VERSIONS

Global error type within this module.

var SERVICE_NAME
var WARNINGS_TO_CATCH_IN_RESPONSE

Global error type within this module.

var WARNINGS_TO_IGNORE_IN_RESPONSE
var element_container_name
var paging_container_name
var prefer_affinity
var returns_elements
var supported_from
var supports_paging

Methods

def get(self, expect_result=True, **kwargs)

Like .call(), but expects exactly one result from the server, or zero when expect_result=False, or either zero or one when expect_result=None. Returns either one object or None.

:param expect_result: None, True, or False :param kwargs: Same as arguments for .call() :return: Same as .call(), but returns either None or exactly one item

Expand source code
def get(self, expect_result=True, **kwargs):
    """Like .call(), but expects exactly one result from the server, or zero when expect_result=False, or either
    zero or one when expect_result=None. Returns either one object or None.

    :param expect_result: None, True, or False
    :param kwargs: Same as arguments for .call()
    :return: Same as .call(), but returns either None or exactly one item
    """
    res = list(self.call(**kwargs))
    # Raise any errors
    for r in res:
        if isinstance(r, Exception):
            raise r
    if expect_result is None and not res:
        # Allow empty result
        return None
    if expect_result is False:
        if res:
            raise ValueError('Expected result length 0, but got %r' % res)
        return None
    if len(res) != 1:
        raise ValueError('Expected result length 1, but got %r' % res)
    return res[0]
def parse(self, xml)

Used mostly for testing, when we want to parse static XML data.

Expand source code
def parse(self, xml):
    """Used mostly for testing, when we want to parse static XML data."""
    resp = DummyResponse(url=None, headers=None, request_headers=None, content=xml)
    _, body = self._get_soap_parts(response=resp)
    return self._elems_to_objs(self._get_elements_in_response(response=self._get_soap_messages(body=body)))
def stop_streaming(self)
Expand source code
def stop_streaming(self):
    if self._streaming_response:
        self._streaming_response.close()  # Release memory
        self._streaming_response = None
    if self._streaming_session:
        self.protocol.release_session(self._streaming_session)
        self._streaming_session = None
exchangelib-4.6.1/docs/exchangelib/services/convert_id.html000066400000000000000000000374611414601472700240620ustar00rootroot00000000000000 exchangelib.services.convert_id API documentation

Module exchangelib.services.convert_id

Expand source code
from .common import EWSService
from ..properties import AlternateId, AlternatePublicFolderId, AlternatePublicFolderItemId, ID_FORMATS
from ..util import create_element, set_xml_value
from ..version import EXCHANGE_2007_SP1


class ConvertId(EWSService):
    """Take a list of IDs to convert. Returns a list of converted IDs or exception instances, in the same order as the
    input list.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/convertid-operation
    """

    SERVICE_NAME = 'ConvertId'
    supported_from = EXCHANGE_2007_SP1

    def call(self, items, destination_format):
        if destination_format not in ID_FORMATS:
            raise ValueError("'destination_format' %r must be one of %s" % (destination_format, ID_FORMATS))
        return self._elems_to_objs(
            self._chunked_get_elements(self.get_payload, items=items, destination_format=destination_format)
        )

    def _elems_to_objs(self, elems):
        cls_map = {cls.response_tag(): cls for cls in (
            AlternateId, AlternatePublicFolderId, AlternatePublicFolderItemId
        )}
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield cls_map[elem.tag].from_xml(elem, account=None)

    def get_payload(self, items, destination_format):
        supported_item_classes = AlternateId, AlternatePublicFolderId, AlternatePublicFolderItemId
        convertid = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(DestinationFormat=destination_format))
        item_ids = create_element('m:SourceIds')
        for item in items:
            if not isinstance(item, supported_item_classes):
                raise ValueError("'item' value %r must be an instance of %r" % (item, supported_item_classes))
            set_xml_value(item_ids, item, version=self.protocol.version)
        if not len(item_ids):
            raise ValueError('"items" must not be empty')
        convertid.append(item_ids)
        return convertid

    @classmethod
    def _get_elements_in_container(cls, container):
        # We may have other elements in here, e.g. 'ResponseCode'. Filter away those.
        return container.findall(AlternateId.response_tag()) \
            + container.findall(AlternatePublicFolderId.response_tag()) \
            + container.findall(AlternatePublicFolderItemId.response_tag())

Classes

class ConvertId (protocol, chunk_size=None, timeout=None)

Take a list of IDs to convert. Returns a list of converted IDs or exception instances, in the same order as the input list.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/convertid-operation

Expand source code
class ConvertId(EWSService):
    """Take a list of IDs to convert. Returns a list of converted IDs or exception instances, in the same order as the
    input list.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/convertid-operation
    """

    SERVICE_NAME = 'ConvertId'
    supported_from = EXCHANGE_2007_SP1

    def call(self, items, destination_format):
        if destination_format not in ID_FORMATS:
            raise ValueError("'destination_format' %r must be one of %s" % (destination_format, ID_FORMATS))
        return self._elems_to_objs(
            self._chunked_get_elements(self.get_payload, items=items, destination_format=destination_format)
        )

    def _elems_to_objs(self, elems):
        cls_map = {cls.response_tag(): cls for cls in (
            AlternateId, AlternatePublicFolderId, AlternatePublicFolderItemId
        )}
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield cls_map[elem.tag].from_xml(elem, account=None)

    def get_payload(self, items, destination_format):
        supported_item_classes = AlternateId, AlternatePublicFolderId, AlternatePublicFolderItemId
        convertid = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(DestinationFormat=destination_format))
        item_ids = create_element('m:SourceIds')
        for item in items:
            if not isinstance(item, supported_item_classes):
                raise ValueError("'item' value %r must be an instance of %r" % (item, supported_item_classes))
            set_xml_value(item_ids, item, version=self.protocol.version)
        if not len(item_ids):
            raise ValueError('"items" must not be empty')
        convertid.append(item_ids)
        return convertid

    @classmethod
    def _get_elements_in_container(cls, container):
        # We may have other elements in here, e.g. 'ResponseCode'. Filter away those.
        return container.findall(AlternateId.response_tag()) \
            + container.findall(AlternatePublicFolderId.response_tag()) \
            + container.findall(AlternatePublicFolderItemId.response_tag())

Ancestors

Class variables

var SERVICE_NAME
var supported_from

Methods

def call(self, items, destination_format)
Expand source code
def call(self, items, destination_format):
    if destination_format not in ID_FORMATS:
        raise ValueError("'destination_format' %r must be one of %s" % (destination_format, ID_FORMATS))
    return self._elems_to_objs(
        self._chunked_get_elements(self.get_payload, items=items, destination_format=destination_format)
    )
def get_payload(self, items, destination_format)
Expand source code
def get_payload(self, items, destination_format):
    supported_item_classes = AlternateId, AlternatePublicFolderId, AlternatePublicFolderItemId
    convertid = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(DestinationFormat=destination_format))
    item_ids = create_element('m:SourceIds')
    for item in items:
        if not isinstance(item, supported_item_classes):
            raise ValueError("'item' value %r must be an instance of %r" % (item, supported_item_classes))
        set_xml_value(item_ids, item, version=self.protocol.version)
    if not len(item_ids):
        raise ValueError('"items" must not be empty')
    convertid.append(item_ids)
    return convertid

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/copy_item.html000066400000000000000000000223411414601472700237050ustar00rootroot00000000000000 exchangelib.services.copy_item API documentation

Module exchangelib.services.copy_item

Expand source code
from . import move_item


class CopyItem(move_item.MoveItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/copyitem-operation"""

    SERVICE_NAME = 'CopyItem'

Classes

class CopyItem (*args, **kwargs)
Expand source code
class CopyItem(move_item.MoveItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/copyitem-operation"""

    SERVICE_NAME = 'CopyItem'

Ancestors

Class variables

var SERVICE_NAME

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/create_attachment.html000066400000000000000000000354121414601472700253730ustar00rootroot00000000000000 exchangelib.services.create_attachment API documentation

Module exchangelib.services.create_attachment

Expand source code
from .common import EWSAccountService, to_item_id
from ..properties import ParentItemId
from ..util import create_element, set_xml_value, MNS


class CreateAttachment(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createattachment-operation
    """

    SERVICE_NAME = 'CreateAttachment'
    element_container_name = '{%s}Attachments' % MNS

    def call(self, parent_item, items):
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, parent_item=parent_item))

    def _elems_to_objs(self, elems):
        from ..attachments import FileAttachment, ItemAttachment
        cls_map = {cls.response_tag(): cls for cls in (FileAttachment, ItemAttachment)}
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield cls_map[elem.tag].from_xml(elem=elem, account=self.account)

    def get_payload(self, items, parent_item):
        from ..items import BaseItem
        payload = create_element('m:%s' % self.SERVICE_NAME)
        version = self.account.version
        if isinstance(parent_item, BaseItem):
            # to_item_id() would convert this to a normal ItemId, but the service wants a ParentItemId
            parent_item = ParentItemId(parent_item.id, parent_item.changekey)
        set_xml_value(payload, to_item_id(parent_item, ParentItemId, version=version), version=version)
        attachments = create_element('m:Attachments')
        for item in items:
            set_xml_value(attachments, item, version=self.account.version)
        if not len(attachments):
            raise ValueError('"items" must not be empty')
        payload.append(attachments)
        return payload

Classes

class CreateAttachment (*args, **kwargs)
Expand source code
class CreateAttachment(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createattachment-operation
    """

    SERVICE_NAME = 'CreateAttachment'
    element_container_name = '{%s}Attachments' % MNS

    def call(self, parent_item, items):
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, parent_item=parent_item))

    def _elems_to_objs(self, elems):
        from ..attachments import FileAttachment, ItemAttachment
        cls_map = {cls.response_tag(): cls for cls in (FileAttachment, ItemAttachment)}
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield cls_map[elem.tag].from_xml(elem=elem, account=self.account)

    def get_payload(self, items, parent_item):
        from ..items import BaseItem
        payload = create_element('m:%s' % self.SERVICE_NAME)
        version = self.account.version
        if isinstance(parent_item, BaseItem):
            # to_item_id() would convert this to a normal ItemId, but the service wants a ParentItemId
            parent_item = ParentItemId(parent_item.id, parent_item.changekey)
        set_xml_value(payload, to_item_id(parent_item, ParentItemId, version=version), version=version)
        attachments = create_element('m:Attachments')
        for item in items:
            set_xml_value(attachments, item, version=self.account.version)
        if not len(attachments):
            raise ValueError('"items" must not be empty')
        payload.append(attachments)
        return payload

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, parent_item, items)
Expand source code
def call(self, parent_item, items):
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, parent_item=parent_item))
def get_payload(self, items, parent_item)
Expand source code
def get_payload(self, items, parent_item):
    from ..items import BaseItem
    payload = create_element('m:%s' % self.SERVICE_NAME)
    version = self.account.version
    if isinstance(parent_item, BaseItem):
        # to_item_id() would convert this to a normal ItemId, but the service wants a ParentItemId
        parent_item = ParentItemId(parent_item.id, parent_item.changekey)
    set_xml_value(payload, to_item_id(parent_item, ParentItemId, version=version), version=version)
    attachments = create_element('m:Attachments')
    for item in items:
        set_xml_value(attachments, item, version=self.account.version)
    if not len(attachments):
        raise ValueError('"items" must not be empty')
    payload.append(attachments)
    return payload

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/create_folder.html000066400000000000000000000355071414601472700245230ustar00rootroot00000000000000 exchangelib.services.create_folder API documentation

Module exchangelib.services.create_folder

Expand source code
from .common import EWSAccountService, parse_folder_elem, create_folder_ids_element
from ..util import create_element, set_xml_value, MNS


class CreateFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createfolder-operation"""

    SERVICE_NAME = 'CreateFolder'
    element_container_name = '{%s}Folders' % MNS

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.folders = []  # A hack to communicate parsing args to _elems_to_objs()

    def call(self, parent_folder, folders):
        # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same
        # class as the folder instance it was requested with.
        self.folders = list(folders)  # Convert to a list, in case 'folders' is a generator. We're iterating twice.
        return self._elems_to_objs(self._chunked_get_elements(
                self.get_payload, items=self.folders, parent_folder=parent_folder,
        ))

    def _elems_to_objs(self, elems):
        for folder, elem in zip(self.folders, elems):
            if isinstance(elem, Exception):
                yield elem
                continue
            yield parse_folder_elem(elem=elem, folder=folder, account=self.account)

    def get_payload(self, folders, parent_folder):
        create_folder = create_element('m:%s' % self.SERVICE_NAME)
        parentfolderid = create_element('m:ParentFolderId')
        set_xml_value(parentfolderid, parent_folder, version=self.account.version)
        set_xml_value(create_folder, parentfolderid, version=self.account.version)
        folder_ids = create_folder_ids_element(tag='m:Folders', folders=folders, version=self.account.version)
        create_folder.append(folder_ids)
        return create_folder

Classes

class CreateFolder (*args, **kwargs)
Expand source code
class CreateFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createfolder-operation"""

    SERVICE_NAME = 'CreateFolder'
    element_container_name = '{%s}Folders' % MNS

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.folders = []  # A hack to communicate parsing args to _elems_to_objs()

    def call(self, parent_folder, folders):
        # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same
        # class as the folder instance it was requested with.
        self.folders = list(folders)  # Convert to a list, in case 'folders' is a generator. We're iterating twice.
        return self._elems_to_objs(self._chunked_get_elements(
                self.get_payload, items=self.folders, parent_folder=parent_folder,
        ))

    def _elems_to_objs(self, elems):
        for folder, elem in zip(self.folders, elems):
            if isinstance(elem, Exception):
                yield elem
                continue
            yield parse_folder_elem(elem=elem, folder=folder, account=self.account)

    def get_payload(self, folders, parent_folder):
        create_folder = create_element('m:%s' % self.SERVICE_NAME)
        parentfolderid = create_element('m:ParentFolderId')
        set_xml_value(parentfolderid, parent_folder, version=self.account.version)
        set_xml_value(create_folder, parentfolderid, version=self.account.version)
        folder_ids = create_folder_ids_element(tag='m:Folders', folders=folders, version=self.account.version)
        create_folder.append(folder_ids)
        return create_folder

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, parent_folder, folders)
Expand source code
def call(self, parent_folder, folders):
    # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same
    # class as the folder instance it was requested with.
    self.folders = list(folders)  # Convert to a list, in case 'folders' is a generator. We're iterating twice.
    return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload, items=self.folders, parent_folder=parent_folder,
    ))
def get_payload(self, folders, parent_folder)
Expand source code
def get_payload(self, folders, parent_folder):
    create_folder = create_element('m:%s' % self.SERVICE_NAME)
    parentfolderid = create_element('m:ParentFolderId')
    set_xml_value(parentfolderid, parent_folder, version=self.account.version)
    set_xml_value(create_folder, parentfolderid, version=self.account.version)
    folder_ids = create_folder_ids_element(tag='m:Folders', folders=folders, version=self.account.version)
    create_folder.append(folder_ids)
    return create_folder

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/create_item.html000066400000000000000000000602611414601472700242010ustar00rootroot00000000000000 exchangelib.services.create_item API documentation

Module exchangelib.services.create_item

Expand source code
from collections import OrderedDict

from .common import EWSAccountService
from ..util import create_element, set_xml_value, MNS


class CreateItem(EWSAccountService):
    """Take a folder and a list of items. Return the result of creation as a list of tuples (success[True|False],
    errormessage), in the same order as the input list.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation
    """

    SERVICE_NAME = 'CreateItem'
    element_container_name = '{%s}Items' % MNS

    def call(self, items, folder, message_disposition, send_meeting_invitations):
        from ..folders import BaseFolder, FolderId
        from ..items import SAVE_ONLY, SEND_AND_SAVE_COPY, SEND_ONLY, \
            SEND_MEETING_INVITATIONS_CHOICES, MESSAGE_DISPOSITION_CHOICES
        if message_disposition not in MESSAGE_DISPOSITION_CHOICES:
            raise ValueError("'message_disposition' %s must be one of %s" % (
                message_disposition, MESSAGE_DISPOSITION_CHOICES
            ))
        if send_meeting_invitations not in SEND_MEETING_INVITATIONS_CHOICES:
            raise ValueError("'send_meeting_invitations' %s must be one of %s" % (
                send_meeting_invitations, SEND_MEETING_INVITATIONS_CHOICES
            ))
        if folder is not None:
            if not isinstance(folder, (BaseFolder, FolderId)):
                raise ValueError("'folder' %r must be a Folder or FolderId instance" % folder)
            if folder.account != self.account:
                raise ValueError('"Folder must belong to this account')
        if message_disposition == SAVE_ONLY and folder is None:
            raise AttributeError("Folder must be supplied when in save-only mode")
        if message_disposition == SEND_AND_SAVE_COPY and folder is None:
            folder = self.account.sent  # 'Sent' is default EWS behaviour
        if message_disposition == SEND_ONLY and folder is not None:
            raise AttributeError("Folder must be None in send-ony mode")
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=items,
            folder=folder,
            message_disposition=message_disposition,
            send_meeting_invitations=send_meeting_invitations,
        ))

    def _elems_to_objs(self, elems):
        from ..items import BulkCreateResult
        for elem in elems:
            if isinstance(elem, (Exception, type(None))):
                yield elem
                continue
            if isinstance(elem, bool):
                yield elem
                continue
            yield BulkCreateResult.from_xml(elem=elem, account=self.account)

    @classmethod
    def _get_elements_in_container(cls, container):
        res = super()._get_elements_in_container(container)
        return res or [True]

    def get_payload(self, items, folder, message_disposition, send_meeting_invitations):
        """Take a list of Item objects (CalendarItem, Message etc) and return the XML for a CreateItem request.
        convert items to XML Elements.

        MessageDisposition is only applicable to email messages, where it is required.

        SendMeetingInvitations is required for calendar items. It is also applicable to tasks, meeting request
        responses (see
        https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation-meeting-request
        ) and sharing
        invitation accepts (see
        https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-acceptsharinginvitation
        ). The last two are not supported yet.

        :param items:
        :param folder:
        :param message_disposition:
        :param send_meeting_invitations:
        """
        createitem = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=OrderedDict([
                ('MessageDisposition', message_disposition),
                ('SendMeetingInvitations', send_meeting_invitations),
            ])
        )
        if folder:
            saveditemfolderid = create_element('m:SavedItemFolderId')
            set_xml_value(saveditemfolderid, folder, version=self.account.version)
            createitem.append(saveditemfolderid)
        item_elems = create_element('m:Items')
        for item in items:
            if not item.account:
                item.account = self.account
            set_xml_value(item_elems, item, version=self.account.version)
        if not len(item_elems):
            raise ValueError('"items" must not be empty')
        createitem.append(item_elems)
        return createitem

Classes

class CreateItem (*args, **kwargs)

Take a folder and a list of items. Return the result of creation as a list of tuples (success[True|False], errormessage), in the same order as the input list.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation

Expand source code
class CreateItem(EWSAccountService):
    """Take a folder and a list of items. Return the result of creation as a list of tuples (success[True|False],
    errormessage), in the same order as the input list.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation
    """

    SERVICE_NAME = 'CreateItem'
    element_container_name = '{%s}Items' % MNS

    def call(self, items, folder, message_disposition, send_meeting_invitations):
        from ..folders import BaseFolder, FolderId
        from ..items import SAVE_ONLY, SEND_AND_SAVE_COPY, SEND_ONLY, \
            SEND_MEETING_INVITATIONS_CHOICES, MESSAGE_DISPOSITION_CHOICES
        if message_disposition not in MESSAGE_DISPOSITION_CHOICES:
            raise ValueError("'message_disposition' %s must be one of %s" % (
                message_disposition, MESSAGE_DISPOSITION_CHOICES
            ))
        if send_meeting_invitations not in SEND_MEETING_INVITATIONS_CHOICES:
            raise ValueError("'send_meeting_invitations' %s must be one of %s" % (
                send_meeting_invitations, SEND_MEETING_INVITATIONS_CHOICES
            ))
        if folder is not None:
            if not isinstance(folder, (BaseFolder, FolderId)):
                raise ValueError("'folder' %r must be a Folder or FolderId instance" % folder)
            if folder.account != self.account:
                raise ValueError('"Folder must belong to this account')
        if message_disposition == SAVE_ONLY and folder is None:
            raise AttributeError("Folder must be supplied when in save-only mode")
        if message_disposition == SEND_AND_SAVE_COPY and folder is None:
            folder = self.account.sent  # 'Sent' is default EWS behaviour
        if message_disposition == SEND_ONLY and folder is not None:
            raise AttributeError("Folder must be None in send-ony mode")
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=items,
            folder=folder,
            message_disposition=message_disposition,
            send_meeting_invitations=send_meeting_invitations,
        ))

    def _elems_to_objs(self, elems):
        from ..items import BulkCreateResult
        for elem in elems:
            if isinstance(elem, (Exception, type(None))):
                yield elem
                continue
            if isinstance(elem, bool):
                yield elem
                continue
            yield BulkCreateResult.from_xml(elem=elem, account=self.account)

    @classmethod
    def _get_elements_in_container(cls, container):
        res = super()._get_elements_in_container(container)
        return res or [True]

    def get_payload(self, items, folder, message_disposition, send_meeting_invitations):
        """Take a list of Item objects (CalendarItem, Message etc) and return the XML for a CreateItem request.
        convert items to XML Elements.

        MessageDisposition is only applicable to email messages, where it is required.

        SendMeetingInvitations is required for calendar items. It is also applicable to tasks, meeting request
        responses (see
        https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation-meeting-request
        ) and sharing
        invitation accepts (see
        https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-acceptsharinginvitation
        ). The last two are not supported yet.

        :param items:
        :param folder:
        :param message_disposition:
        :param send_meeting_invitations:
        """
        createitem = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=OrderedDict([
                ('MessageDisposition', message_disposition),
                ('SendMeetingInvitations', send_meeting_invitations),
            ])
        )
        if folder:
            saveditemfolderid = create_element('m:SavedItemFolderId')
            set_xml_value(saveditemfolderid, folder, version=self.account.version)
            createitem.append(saveditemfolderid)
        item_elems = create_element('m:Items')
        for item in items:
            if not item.account:
                item.account = self.account
            set_xml_value(item_elems, item, version=self.account.version)
        if not len(item_elems):
            raise ValueError('"items" must not be empty')
        createitem.append(item_elems)
        return createitem

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, items, folder, message_disposition, send_meeting_invitations)
Expand source code
def call(self, items, folder, message_disposition, send_meeting_invitations):
    from ..folders import BaseFolder, FolderId
    from ..items import SAVE_ONLY, SEND_AND_SAVE_COPY, SEND_ONLY, \
        SEND_MEETING_INVITATIONS_CHOICES, MESSAGE_DISPOSITION_CHOICES
    if message_disposition not in MESSAGE_DISPOSITION_CHOICES:
        raise ValueError("'message_disposition' %s must be one of %s" % (
            message_disposition, MESSAGE_DISPOSITION_CHOICES
        ))
    if send_meeting_invitations not in SEND_MEETING_INVITATIONS_CHOICES:
        raise ValueError("'send_meeting_invitations' %s must be one of %s" % (
            send_meeting_invitations, SEND_MEETING_INVITATIONS_CHOICES
        ))
    if folder is not None:
        if not isinstance(folder, (BaseFolder, FolderId)):
            raise ValueError("'folder' %r must be a Folder or FolderId instance" % folder)
        if folder.account != self.account:
            raise ValueError('"Folder must belong to this account')
    if message_disposition == SAVE_ONLY and folder is None:
        raise AttributeError("Folder must be supplied when in save-only mode")
    if message_disposition == SEND_AND_SAVE_COPY and folder is None:
        folder = self.account.sent  # 'Sent' is default EWS behaviour
    if message_disposition == SEND_ONLY and folder is not None:
        raise AttributeError("Folder must be None in send-ony mode")
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload,
        items=items,
        folder=folder,
        message_disposition=message_disposition,
        send_meeting_invitations=send_meeting_invitations,
    ))
def get_payload(self, items, folder, message_disposition, send_meeting_invitations)

Take a list of Item objects (CalendarItem, Message etc) and return the XML for a CreateItem request. convert items to XML Elements.

MessageDisposition is only applicable to email messages, where it is required.

SendMeetingInvitations is required for calendar items. It is also applicable to tasks, meeting request responses (see https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation-meeting-request ) and sharing invitation accepts (see https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-acceptsharinginvitation ). The last two are not supported yet.

:param items: :param folder: :param message_disposition: :param send_meeting_invitations:

Expand source code
def get_payload(self, items, folder, message_disposition, send_meeting_invitations):
    """Take a list of Item objects (CalendarItem, Message etc) and return the XML for a CreateItem request.
    convert items to XML Elements.

    MessageDisposition is only applicable to email messages, where it is required.

    SendMeetingInvitations is required for calendar items. It is also applicable to tasks, meeting request
    responses (see
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation-meeting-request
    ) and sharing
    invitation accepts (see
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-acceptsharinginvitation
    ). The last two are not supported yet.

    :param items:
    :param folder:
    :param message_disposition:
    :param send_meeting_invitations:
    """
    createitem = create_element(
        'm:%s' % self.SERVICE_NAME,
        attrs=OrderedDict([
            ('MessageDisposition', message_disposition),
            ('SendMeetingInvitations', send_meeting_invitations),
        ])
    )
    if folder:
        saveditemfolderid = create_element('m:SavedItemFolderId')
        set_xml_value(saveditemfolderid, folder, version=self.account.version)
        createitem.append(saveditemfolderid)
    item_elems = create_element('m:Items')
    for item in items:
        if not item.account:
            item.account = self.account
        set_xml_value(item_elems, item, version=self.account.version)
    if not len(item_elems):
        raise ValueError('"items" must not be empty')
    createitem.append(item_elems)
    return createitem

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/create_user_configuration.html000066400000000000000000000306641414601472700271540ustar00rootroot00000000000000 exchangelib.services.create_user_configuration API documentation

Module exchangelib.services.create_user_configuration

Expand source code
from .common import EWSAccountService
from ..util import create_element, set_xml_value


class CreateUserConfiguration(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createuserconfiguration-operation
    """

    SERVICE_NAME = 'CreateUserConfiguration'
    returns_elements = False

    def call(self, user_configuration):
        return self._get_elements(payload=self.get_payload(user_configuration=user_configuration))

    def get_payload(self, user_configuration):
        createuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(createuserconfiguration, user_configuration, version=self.protocol.version)
        return createuserconfiguration

Classes

class CreateUserConfiguration (*args, **kwargs)
Expand source code
class CreateUserConfiguration(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createuserconfiguration-operation
    """

    SERVICE_NAME = 'CreateUserConfiguration'
    returns_elements = False

    def call(self, user_configuration):
        return self._get_elements(payload=self.get_payload(user_configuration=user_configuration))

    def get_payload(self, user_configuration):
        createuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(createuserconfiguration, user_configuration, version=self.protocol.version)
        return createuserconfiguration

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, user_configuration)
Expand source code
def call(self, user_configuration):
    return self._get_elements(payload=self.get_payload(user_configuration=user_configuration))
def get_payload(self, user_configuration)
Expand source code
def get_payload(self, user_configuration):
    createuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(createuserconfiguration, user_configuration, version=self.protocol.version)
    return createuserconfiguration

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/delete_attachment.html000066400000000000000000000305201414601472700253650ustar00rootroot00000000000000 exchangelib.services.delete_attachment API documentation

Module exchangelib.services.delete_attachment

Expand source code
from .common import EWSAccountService, create_attachment_ids_element
from ..properties import RootItemId
from ..util import create_element


class DeleteAttachment(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteattachment-operation
    """

    SERVICE_NAME = 'DeleteAttachment'

    def call(self, items):
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield RootItemId.from_xml(elem=elem, account=self.account)

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(RootItemId.response_tag())

    def get_payload(self, items):
        payload = create_element('m:%s' % self.SERVICE_NAME)
        attachment_ids = create_attachment_ids_element(items=items, version=self.account.version)
        payload.append(attachment_ids)
        return payload

Classes

class DeleteAttachment (*args, **kwargs)
Expand source code
class DeleteAttachment(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteattachment-operation
    """

    SERVICE_NAME = 'DeleteAttachment'

    def call(self, items):
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield RootItemId.from_xml(elem=elem, account=self.account)

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(RootItemId.response_tag())

    def get_payload(self, items):
        payload = create_element('m:%s' % self.SERVICE_NAME)
        attachment_ids = create_attachment_ids_element(items=items, version=self.account.version)
        payload.append(attachment_ids)
        return payload

Ancestors

Class variables

var SERVICE_NAME

Methods

def call(self, items)
Expand source code
def call(self, items):
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items))
def get_payload(self, items)
Expand source code
def get_payload(self, items):
    payload = create_element('m:%s' % self.SERVICE_NAME)
    attachment_ids = create_attachment_ids_element(items=items, version=self.account.version)
    payload.append(attachment_ids)
    return payload

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/delete_folder.html000066400000000000000000000303051414601472700245110ustar00rootroot00000000000000 exchangelib.services.delete_folder API documentation

Module exchangelib.services.delete_folder

Expand source code
from .common import EWSAccountService, create_folder_ids_element
from ..util import create_element


class DeleteFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deletefolder-operation"""

    SERVICE_NAME = 'DeleteFolder'
    returns_elements = False

    def call(self, folders, delete_type):
        return self._chunked_get_elements(self.get_payload, items=folders, delete_type=delete_type)

    def get_payload(self, folders, delete_type):
        deletefolder = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(DeleteType=delete_type))
        folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
        deletefolder.append(folder_ids)
        return deletefolder

Classes

class DeleteFolder (*args, **kwargs)
Expand source code
class DeleteFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deletefolder-operation"""

    SERVICE_NAME = 'DeleteFolder'
    returns_elements = False

    def call(self, folders, delete_type):
        return self._chunked_get_elements(self.get_payload, items=folders, delete_type=delete_type)

    def get_payload(self, folders, delete_type):
        deletefolder = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(DeleteType=delete_type))
        folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
        deletefolder.append(folder_ids)
        return deletefolder

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, folders, delete_type)
Expand source code
def call(self, folders, delete_type):
    return self._chunked_get_elements(self.get_payload, items=folders, delete_type=delete_type)
def get_payload(self, folders, delete_type)
Expand source code
def get_payload(self, folders, delete_type):
    deletefolder = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(DeleteType=delete_type))
    folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
    deletefolder.append(folder_ids)
    return deletefolder

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/delete_item.html000066400000000000000000000473271414601472700242100ustar00rootroot00000000000000 exchangelib.services.delete_item API documentation

Module exchangelib.services.delete_item

Expand source code
from collections import OrderedDict

from .common import EWSAccountService, create_item_ids_element
from ..util import create_element
from ..version import EXCHANGE_2013_SP1


class DeleteItem(EWSAccountService):
    """Take a folder and a list of (id, changekey) tuples. Return result of deletion as a list of tuples
    (success[True|False], errormessage), in the same order as the input list.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteitem-operation
    """

    SERVICE_NAME = 'DeleteItem'
    returns_elements = False

    def call(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts):
        from ..items import DELETE_TYPE_CHOICES, SEND_MEETING_CANCELLATIONS_CHOICES, AFFECTED_TASK_OCCURRENCES_CHOICES
        if delete_type not in DELETE_TYPE_CHOICES:
            raise ValueError("'delete_type' %s must be one of %s" % (
                delete_type, DELETE_TYPE_CHOICES
            ))
        if send_meeting_cancellations not in SEND_MEETING_CANCELLATIONS_CHOICES:
            raise ValueError("'send_meeting_cancellations' %s must be one of %s" % (
                send_meeting_cancellations, SEND_MEETING_CANCELLATIONS_CHOICES
            ))
        if affected_task_occurrences not in AFFECTED_TASK_OCCURRENCES_CHOICES:
            raise ValueError("'affected_task_occurrences' %s must be one of %s" % (
                affected_task_occurrences, AFFECTED_TASK_OCCURRENCES_CHOICES
            ))
        if suppress_read_receipts not in (True, False):
            raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts)
        return self._chunked_get_elements(
            self.get_payload,
            items=items,
            delete_type=delete_type,
            send_meeting_cancellations=send_meeting_cancellations,
            affected_task_occurrences=affected_task_occurrences,
            suppress_read_receipts=suppress_read_receipts,
        )

    def get_payload(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences,
                    suppress_read_receipts):
        # Takes a list of (id, changekey) tuples or Item objects and returns the XML for a DeleteItem request.
        if self.account.version.build >= EXCHANGE_2013_SP1:
            deleteitem = create_element(
                'm:%s' % self.SERVICE_NAME,
                attrs=OrderedDict([
                    ('DeleteType', delete_type),
                    ('SendMeetingCancellations', send_meeting_cancellations),
                    ('AffectedTaskOccurrences', affected_task_occurrences),
                    ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'),
                ])
            )
        else:
            deleteitem = create_element(
                'm:%s' % self.SERVICE_NAME,
                attrs=OrderedDict([
                    ('DeleteType', delete_type),
                    ('SendMeetingCancellations', send_meeting_cancellations),
                    ('AffectedTaskOccurrences', affected_task_occurrences),
                 ])
            )

        item_ids = create_item_ids_element(items=items, version=self.account.version)
        deleteitem.append(item_ids)
        return deleteitem

Classes

class DeleteItem (*args, **kwargs)

Take a folder and a list of (id, changekey) tuples. Return result of deletion as a list of tuples (success[True|False], errormessage), in the same order as the input list.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteitem-operation

Expand source code
class DeleteItem(EWSAccountService):
    """Take a folder and a list of (id, changekey) tuples. Return result of deletion as a list of tuples
    (success[True|False], errormessage), in the same order as the input list.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteitem-operation
    """

    SERVICE_NAME = 'DeleteItem'
    returns_elements = False

    def call(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts):
        from ..items import DELETE_TYPE_CHOICES, SEND_MEETING_CANCELLATIONS_CHOICES, AFFECTED_TASK_OCCURRENCES_CHOICES
        if delete_type not in DELETE_TYPE_CHOICES:
            raise ValueError("'delete_type' %s must be one of %s" % (
                delete_type, DELETE_TYPE_CHOICES
            ))
        if send_meeting_cancellations not in SEND_MEETING_CANCELLATIONS_CHOICES:
            raise ValueError("'send_meeting_cancellations' %s must be one of %s" % (
                send_meeting_cancellations, SEND_MEETING_CANCELLATIONS_CHOICES
            ))
        if affected_task_occurrences not in AFFECTED_TASK_OCCURRENCES_CHOICES:
            raise ValueError("'affected_task_occurrences' %s must be one of %s" % (
                affected_task_occurrences, AFFECTED_TASK_OCCURRENCES_CHOICES
            ))
        if suppress_read_receipts not in (True, False):
            raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts)
        return self._chunked_get_elements(
            self.get_payload,
            items=items,
            delete_type=delete_type,
            send_meeting_cancellations=send_meeting_cancellations,
            affected_task_occurrences=affected_task_occurrences,
            suppress_read_receipts=suppress_read_receipts,
        )

    def get_payload(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences,
                    suppress_read_receipts):
        # Takes a list of (id, changekey) tuples or Item objects and returns the XML for a DeleteItem request.
        if self.account.version.build >= EXCHANGE_2013_SP1:
            deleteitem = create_element(
                'm:%s' % self.SERVICE_NAME,
                attrs=OrderedDict([
                    ('DeleteType', delete_type),
                    ('SendMeetingCancellations', send_meeting_cancellations),
                    ('AffectedTaskOccurrences', affected_task_occurrences),
                    ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'),
                ])
            )
        else:
            deleteitem = create_element(
                'm:%s' % self.SERVICE_NAME,
                attrs=OrderedDict([
                    ('DeleteType', delete_type),
                    ('SendMeetingCancellations', send_meeting_cancellations),
                    ('AffectedTaskOccurrences', affected_task_occurrences),
                 ])
            )

        item_ids = create_item_ids_element(items=items, version=self.account.version)
        deleteitem.append(item_ids)
        return deleteitem

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts)
Expand source code
def call(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts):
    from ..items import DELETE_TYPE_CHOICES, SEND_MEETING_CANCELLATIONS_CHOICES, AFFECTED_TASK_OCCURRENCES_CHOICES
    if delete_type not in DELETE_TYPE_CHOICES:
        raise ValueError("'delete_type' %s must be one of %s" % (
            delete_type, DELETE_TYPE_CHOICES
        ))
    if send_meeting_cancellations not in SEND_MEETING_CANCELLATIONS_CHOICES:
        raise ValueError("'send_meeting_cancellations' %s must be one of %s" % (
            send_meeting_cancellations, SEND_MEETING_CANCELLATIONS_CHOICES
        ))
    if affected_task_occurrences not in AFFECTED_TASK_OCCURRENCES_CHOICES:
        raise ValueError("'affected_task_occurrences' %s must be one of %s" % (
            affected_task_occurrences, AFFECTED_TASK_OCCURRENCES_CHOICES
        ))
    if suppress_read_receipts not in (True, False):
        raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts)
    return self._chunked_get_elements(
        self.get_payload,
        items=items,
        delete_type=delete_type,
        send_meeting_cancellations=send_meeting_cancellations,
        affected_task_occurrences=affected_task_occurrences,
        suppress_read_receipts=suppress_read_receipts,
    )
def get_payload(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts)
Expand source code
def get_payload(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences,
                suppress_read_receipts):
    # Takes a list of (id, changekey) tuples or Item objects and returns the XML for a DeleteItem request.
    if self.account.version.build >= EXCHANGE_2013_SP1:
        deleteitem = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=OrderedDict([
                ('DeleteType', delete_type),
                ('SendMeetingCancellations', send_meeting_cancellations),
                ('AffectedTaskOccurrences', affected_task_occurrences),
                ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'),
            ])
        )
    else:
        deleteitem = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=OrderedDict([
                ('DeleteType', delete_type),
                ('SendMeetingCancellations', send_meeting_cancellations),
                ('AffectedTaskOccurrences', affected_task_occurrences),
             ])
        )

    item_ids = create_item_ids_element(items=items, version=self.account.version)
    deleteitem.append(item_ids)
    return deleteitem

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/delete_user_configuration.html000066400000000000000000000310061414601472700271420ustar00rootroot00000000000000 exchangelib.services.delete_user_configuration API documentation

Module exchangelib.services.delete_user_configuration

Expand source code
from .common import EWSAccountService
from ..util import create_element, set_xml_value


class DeleteUserConfiguration(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteuserconfiguration-operation
    """

    SERVICE_NAME = 'DeleteUserConfiguration'
    returns_elements = False

    def call(self, user_configuration_name):
        return self._get_elements(payload=self.get_payload(user_configuration_name=user_configuration_name))

    def get_payload(self, user_configuration_name):
        deleteuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(deleteuserconfiguration, user_configuration_name, version=self.account.version)
        return deleteuserconfiguration

Classes

class DeleteUserConfiguration (*args, **kwargs)
Expand source code
class DeleteUserConfiguration(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteuserconfiguration-operation
    """

    SERVICE_NAME = 'DeleteUserConfiguration'
    returns_elements = False

    def call(self, user_configuration_name):
        return self._get_elements(payload=self.get_payload(user_configuration_name=user_configuration_name))

    def get_payload(self, user_configuration_name):
        deleteuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(deleteuserconfiguration, user_configuration_name, version=self.account.version)
        return deleteuserconfiguration

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, user_configuration_name)
Expand source code
def call(self, user_configuration_name):
    return self._get_elements(payload=self.get_payload(user_configuration_name=user_configuration_name))
def get_payload(self, user_configuration_name)
Expand source code
def get_payload(self, user_configuration_name):
    deleteuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(deleteuserconfiguration, user_configuration_name, version=self.account.version)
    return deleteuserconfiguration

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/empty_folder.html000066400000000000000000000320561414601472700244120ustar00rootroot00000000000000 exchangelib.services.empty_folder API documentation

Module exchangelib.services.empty_folder

Expand source code
from collections import OrderedDict

from .common import EWSAccountService, create_folder_ids_element
from ..util import create_element


class EmptyFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/emptyfolder-operation"""

    SERVICE_NAME = 'EmptyFolder'
    returns_elements = False

    def call(self, folders, delete_type, delete_sub_folders):
        return self._chunked_get_elements(
            self.get_payload, items=folders, delete_type=delete_type, delete_sub_folders=delete_sub_folders
        )

    def get_payload(self, folders, delete_type, delete_sub_folders):
        emptyfolder = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=OrderedDict([
                ('DeleteType', delete_type),
                ('DeleteSubFolders', 'true' if delete_sub_folders else 'false'),
            ])
        )
        folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
        emptyfolder.append(folder_ids)
        return emptyfolder

Classes

class EmptyFolder (*args, **kwargs)
Expand source code
class EmptyFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/emptyfolder-operation"""

    SERVICE_NAME = 'EmptyFolder'
    returns_elements = False

    def call(self, folders, delete_type, delete_sub_folders):
        return self._chunked_get_elements(
            self.get_payload, items=folders, delete_type=delete_type, delete_sub_folders=delete_sub_folders
        )

    def get_payload(self, folders, delete_type, delete_sub_folders):
        emptyfolder = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=OrderedDict([
                ('DeleteType', delete_type),
                ('DeleteSubFolders', 'true' if delete_sub_folders else 'false'),
            ])
        )
        folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
        emptyfolder.append(folder_ids)
        return emptyfolder

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, folders, delete_type, delete_sub_folders)
Expand source code
def call(self, folders, delete_type, delete_sub_folders):
    return self._chunked_get_elements(
        self.get_payload, items=folders, delete_type=delete_type, delete_sub_folders=delete_sub_folders
    )
def get_payload(self, folders, delete_type, delete_sub_folders)
Expand source code
def get_payload(self, folders, delete_type, delete_sub_folders):
    emptyfolder = create_element(
        'm:%s' % self.SERVICE_NAME,
        attrs=OrderedDict([
            ('DeleteType', delete_type),
            ('DeleteSubFolders', 'true' if delete_sub_folders else 'false'),
        ])
    )
    folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
    emptyfolder.append(folder_ids)
    return emptyfolder

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/expand_dl.html000066400000000000000000000313051414601472700236530ustar00rootroot00000000000000 exchangelib.services.expand_dl API documentation

Module exchangelib.services.expand_dl

Expand source code
from .common import EWSService
from ..errors import ErrorNameResolutionMultipleResults
from ..properties import Mailbox
from ..util import create_element, set_xml_value, MNS


class ExpandDL(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/expanddl-operation"""

    SERVICE_NAME = 'ExpandDL'
    element_container_name = '{%s}DLExpansion' % MNS
    WARNINGS_TO_IGNORE_IN_RESPONSE = ErrorNameResolutionMultipleResults

    def call(self, distribution_list):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(distribution_list=distribution_list)))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Mailbox.from_xml(elem, account=None)

    def get_payload(self, distribution_list):
        payload = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(payload, distribution_list, version=self.protocol.version)
        return payload

Classes

class ExpandDL (protocol, chunk_size=None, timeout=None)
Expand source code
class ExpandDL(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/expanddl-operation"""

    SERVICE_NAME = 'ExpandDL'
    element_container_name = '{%s}DLExpansion' % MNS
    WARNINGS_TO_IGNORE_IN_RESPONSE = ErrorNameResolutionMultipleResults

    def call(self, distribution_list):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(distribution_list=distribution_list)))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Mailbox.from_xml(elem, account=None)

    def get_payload(self, distribution_list):
        payload = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(payload, distribution_list, version=self.protocol.version)
        return payload

Ancestors

Class variables

var SERVICE_NAME
var WARNINGS_TO_IGNORE_IN_RESPONSE

Global error type within this module.

var element_container_name

Methods

def call(self, distribution_list)
Expand source code
def call(self, distribution_list):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(distribution_list=distribution_list)))
def get_payload(self, distribution_list)
Expand source code
def get_payload(self, distribution_list):
    payload = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(payload, distribution_list, version=self.protocol.version)
    return payload

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/export_items.html000066400000000000000000000326061414601472700244440ustar00rootroot00000000000000 exchangelib.services.export_items API documentation

Module exchangelib.services.export_items

Expand source code
from .common import EWSAccountService, create_item_ids_element
from ..errors import ResponseMessageError
from ..util import create_element, MNS


class ExportItems(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/exportitems-operation"""

    ERRORS_TO_CATCH_IN_RESPONSE = ResponseMessageError
    SERVICE_NAME = 'ExportItems'
    element_container_name = '{%s}Data' % MNS

    def call(self, items):
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield elem.text  # All we want is the 64bit string in the 'Data' tag

    def get_payload(self, items):
        exportitems = create_element('m:%s' % self.SERVICE_NAME)
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        exportitems.append(item_ids)
        return exportitems

    # We need to override this since ExportItemsResponseMessage is formatted a
    # little bit differently. .
    @classmethod
    def _get_elements_in_container(cls, container):
        return [container]

Classes

class ExportItems (*args, **kwargs)
Expand source code
class ExportItems(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/exportitems-operation"""

    ERRORS_TO_CATCH_IN_RESPONSE = ResponseMessageError
    SERVICE_NAME = 'ExportItems'
    element_container_name = '{%s}Data' % MNS

    def call(self, items):
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield elem.text  # All we want is the 64bit string in the 'Data' tag

    def get_payload(self, items):
        exportitems = create_element('m:%s' % self.SERVICE_NAME)
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        exportitems.append(item_ids)
        return exportitems

    # We need to override this since ExportItemsResponseMessage is formatted a
    # little bit differently. .
    @classmethod
    def _get_elements_in_container(cls, container):
        return [container]

Ancestors

Class variables

var ERRORS_TO_CATCH_IN_RESPONSE

Global error type within this module.

var SERVICE_NAME
var element_container_name

Methods

def call(self, items)
Expand source code
def call(self, items):
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items))
def get_payload(self, items)
Expand source code
def get_payload(self, items):
    exportitems = create_element('m:%s' % self.SERVICE_NAME)
    item_ids = create_item_ids_element(items=items, version=self.account.version)
    exportitems.append(item_ids)
    return exportitems

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/find_folder.html000066400000000000000000000524321414601472700241740ustar00rootroot00000000000000 exchangelib.services.find_folder API documentation

Module exchangelib.services.find_folder

Expand source code
from collections import OrderedDict

from .common import EWSAccountService, create_shape_element
from ..util import create_element, set_xml_value, TNS, MNS
from ..version import EXCHANGE_2010


class FindFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/findfolder-operation"""

    SERVICE_NAME = 'FindFolder'
    element_container_name = '{%s}Folders' % TNS
    paging_container_name = '{%s}RootFolder' % MNS
    supports_paging = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.root = None  # A hack to communicate parsing args to _elems_to_objs()

    def call(self, folders, additional_fields, restriction, shape, depth, max_items, offset):
        """Find subfolders of a folder.

        :param folders: the folders to act on
        :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects
        :param restriction: Restriction object that defines the filters for the query
        :param shape: The set of attributes to return
        :param depth: How deep in the folder structure to search for folders
        :param max_items: The maximum number of items to return
        :param offset: the offset relative to the first item in the item collection. Usually 0.

        :return: XML elements for the matching folders
        """
        roots = {f.root for f in folders}
        if len(roots) != 1:
            raise ValueError('FindFolder must be called with folders in the same root hierarchy (%r)' % roots)
        self.root = roots.pop()
        return self._elems_to_objs(self._paged_call(
                payload_func=self.get_payload,
                max_items=max_items,
                folders=folders,
                **dict(
                    additional_fields=additional_fields,
                    restriction=restriction,
                    shape=shape,
                    depth=depth,
                    page_size=self.chunk_size,
                    offset=offset,
                )
        ))

    def _elems_to_objs(self, elems):
        from ..folders import Folder
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Folder.from_xml_with_root(elem=elem, root=self.root)

    def get_payload(self, folders, additional_fields, restriction, shape, depth, page_size, offset=0):
        findfolder = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
        foldershape = create_shape_element(
            tag='m:FolderShape', shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        findfolder.append(foldershape)
        if self.account.version.build >= EXCHANGE_2010:
            indexedpageviewitem = create_element(
                'm:IndexedPageFolderView',
                attrs=OrderedDict([
                    ('MaxEntriesReturned', str(page_size)),
                    ('Offset', str(offset)),
                    ('BasePoint', 'Beginning'),
                ])
            )
            findfolder.append(indexedpageviewitem)
        else:
            if offset != 0:
                raise ValueError('Offsets are only supported from Exchange 2010')
        if restriction:
            findfolder.append(restriction.to_xml(version=self.account.version))
        parentfolderids = create_element('m:ParentFolderIds')
        set_xml_value(parentfolderids, folders, version=self.account.version)
        findfolder.append(parentfolderids)
        return findfolder

Classes

class FindFolder (*args, **kwargs)
Expand source code
class FindFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/findfolder-operation"""

    SERVICE_NAME = 'FindFolder'
    element_container_name = '{%s}Folders' % TNS
    paging_container_name = '{%s}RootFolder' % MNS
    supports_paging = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.root = None  # A hack to communicate parsing args to _elems_to_objs()

    def call(self, folders, additional_fields, restriction, shape, depth, max_items, offset):
        """Find subfolders of a folder.

        :param folders: the folders to act on
        :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects
        :param restriction: Restriction object that defines the filters for the query
        :param shape: The set of attributes to return
        :param depth: How deep in the folder structure to search for folders
        :param max_items: The maximum number of items to return
        :param offset: the offset relative to the first item in the item collection. Usually 0.

        :return: XML elements for the matching folders
        """
        roots = {f.root for f in folders}
        if len(roots) != 1:
            raise ValueError('FindFolder must be called with folders in the same root hierarchy (%r)' % roots)
        self.root = roots.pop()
        return self._elems_to_objs(self._paged_call(
                payload_func=self.get_payload,
                max_items=max_items,
                folders=folders,
                **dict(
                    additional_fields=additional_fields,
                    restriction=restriction,
                    shape=shape,
                    depth=depth,
                    page_size=self.chunk_size,
                    offset=offset,
                )
        ))

    def _elems_to_objs(self, elems):
        from ..folders import Folder
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Folder.from_xml_with_root(elem=elem, root=self.root)

    def get_payload(self, folders, additional_fields, restriction, shape, depth, page_size, offset=0):
        findfolder = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
        foldershape = create_shape_element(
            tag='m:FolderShape', shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        findfolder.append(foldershape)
        if self.account.version.build >= EXCHANGE_2010:
            indexedpageviewitem = create_element(
                'm:IndexedPageFolderView',
                attrs=OrderedDict([
                    ('MaxEntriesReturned', str(page_size)),
                    ('Offset', str(offset)),
                    ('BasePoint', 'Beginning'),
                ])
            )
            findfolder.append(indexedpageviewitem)
        else:
            if offset != 0:
                raise ValueError('Offsets are only supported from Exchange 2010')
        if restriction:
            findfolder.append(restriction.to_xml(version=self.account.version))
        parentfolderids = create_element('m:ParentFolderIds')
        set_xml_value(parentfolderids, folders, version=self.account.version)
        findfolder.append(parentfolderids)
        return findfolder

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var paging_container_name
var supports_paging

Methods

def call(self, folders, additional_fields, restriction, shape, depth, max_items, offset)

Find subfolders of a folder.

:param folders: the folders to act on :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects :param restriction: Restriction object that defines the filters for the query :param shape: The set of attributes to return :param depth: How deep in the folder structure to search for folders :param max_items: The maximum number of items to return :param offset: the offset relative to the first item in the item collection. Usually 0.

:return: XML elements for the matching folders

Expand source code
def call(self, folders, additional_fields, restriction, shape, depth, max_items, offset):
    """Find subfolders of a folder.

    :param folders: the folders to act on
    :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects
    :param restriction: Restriction object that defines the filters for the query
    :param shape: The set of attributes to return
    :param depth: How deep in the folder structure to search for folders
    :param max_items: The maximum number of items to return
    :param offset: the offset relative to the first item in the item collection. Usually 0.

    :return: XML elements for the matching folders
    """
    roots = {f.root for f in folders}
    if len(roots) != 1:
        raise ValueError('FindFolder must be called with folders in the same root hierarchy (%r)' % roots)
    self.root = roots.pop()
    return self._elems_to_objs(self._paged_call(
            payload_func=self.get_payload,
            max_items=max_items,
            folders=folders,
            **dict(
                additional_fields=additional_fields,
                restriction=restriction,
                shape=shape,
                depth=depth,
                page_size=self.chunk_size,
                offset=offset,
            )
    ))
def get_payload(self, folders, additional_fields, restriction, shape, depth, page_size, offset=0)
Expand source code
def get_payload(self, folders, additional_fields, restriction, shape, depth, page_size, offset=0):
    findfolder = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
    foldershape = create_shape_element(
        tag='m:FolderShape', shape=shape, additional_fields=additional_fields, version=self.account.version
    )
    findfolder.append(foldershape)
    if self.account.version.build >= EXCHANGE_2010:
        indexedpageviewitem = create_element(
            'm:IndexedPageFolderView',
            attrs=OrderedDict([
                ('MaxEntriesReturned', str(page_size)),
                ('Offset', str(offset)),
                ('BasePoint', 'Beginning'),
            ])
        )
        findfolder.append(indexedpageviewitem)
    else:
        if offset != 0:
            raise ValueError('Offsets are only supported from Exchange 2010')
    if restriction:
        findfolder.append(restriction.to_xml(version=self.account.version))
    parentfolderids = create_element('m:ParentFolderIds')
    set_xml_value(parentfolderids, folders, version=self.account.version)
    findfolder.append(parentfolderids)
    return findfolder

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/find_item.html000066400000000000000000000557301414601472700236630ustar00rootroot00000000000000 exchangelib.services.find_item API documentation

Module exchangelib.services.find_item

Expand source code
from collections import OrderedDict

from .common import EWSAccountService, create_shape_element
from ..util import create_element, set_xml_value, TNS, MNS


class FindItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/finditem-operation"""

    SERVICE_NAME = 'FindItem'
    element_container_name = '{%s}Items' % TNS
    paging_container_name = '{%s}RootFolder' % MNS
    supports_paging = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # A hack to communicate parsing args to _elems_to_objs()
        self.additional_fields = None
        self.shape = None

    def call(self, folders, additional_fields, restriction, order_fields, shape, query_string, depth, calendar_view,
             max_items, offset):
        """Find items in an account.

        :param folders: the folders to act on
        :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects
        :param restriction: a Restriction object for
        :param order_fields: the fields to sort the results by
        :param shape: The set of attributes to return
        :param query_string: a QueryString object
        :param depth: How deep in the folder structure to search for items
        :param calendar_view: If set, returns recurring calendar items unfolded
        :param max_items: the max number of items to return
        :param offset: the offset relative to the first item in the item collection. Usually 0.

        :return: XML elements for the matching items
        """
        self.additional_fields = additional_fields
        self.shape = shape
        return self._elems_to_objs(self._paged_call(
            payload_func=self.get_payload,
            max_items=max_items,
            folders=folders,
            **dict(
                additional_fields=additional_fields,
                restriction=restriction,
                order_fields=order_fields,
                query_string=query_string,
                shape=shape,
                depth=depth,
                calendar_view=calendar_view,
                page_size=self.chunk_size,
                offset=offset,
            )
        ))

    def _elems_to_objs(self, elems):
        from ..folders.base import BaseFolder
        from ..items import Item, ID_ONLY
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            if self.shape == ID_ONLY and self.additional_fields is None:
                yield Item.id_from_xml(elem)
                continue
            yield BaseFolder.item_model_from_tag(elem.tag).from_xml(elem=elem, account=self.account)

    def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth,
                    calendar_view, page_size, offset=0):
        finditem = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
        itemshape = create_shape_element(
            tag='m:ItemShape', shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        finditem.append(itemshape)
        if calendar_view is None:
            view_type = create_element(
                'm:IndexedPageItemView',
                attrs=OrderedDict([
                    ('MaxEntriesReturned', str(page_size)),
                    ('Offset', str(offset)),
                    ('BasePoint', 'Beginning'),
                ])
            )
        else:
            view_type = calendar_view.to_xml(version=self.account.version)
        finditem.append(view_type)
        if restriction:
            finditem.append(restriction.to_xml(version=self.account.version))
        if order_fields:
            finditem.append(set_xml_value(
                create_element('m:SortOrder'),
                order_fields,
                version=self.account.version
            ))
        finditem.append(set_xml_value(
            create_element('m:ParentFolderIds'),
            folders,
            version=self.account.version
        ))
        if query_string:
            finditem.append(query_string.to_xml(version=self.account.version))
        return finditem

Classes

class FindItem (*args, **kwargs)
Expand source code
class FindItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/finditem-operation"""

    SERVICE_NAME = 'FindItem'
    element_container_name = '{%s}Items' % TNS
    paging_container_name = '{%s}RootFolder' % MNS
    supports_paging = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # A hack to communicate parsing args to _elems_to_objs()
        self.additional_fields = None
        self.shape = None

    def call(self, folders, additional_fields, restriction, order_fields, shape, query_string, depth, calendar_view,
             max_items, offset):
        """Find items in an account.

        :param folders: the folders to act on
        :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects
        :param restriction: a Restriction object for
        :param order_fields: the fields to sort the results by
        :param shape: The set of attributes to return
        :param query_string: a QueryString object
        :param depth: How deep in the folder structure to search for items
        :param calendar_view: If set, returns recurring calendar items unfolded
        :param max_items: the max number of items to return
        :param offset: the offset relative to the first item in the item collection. Usually 0.

        :return: XML elements for the matching items
        """
        self.additional_fields = additional_fields
        self.shape = shape
        return self._elems_to_objs(self._paged_call(
            payload_func=self.get_payload,
            max_items=max_items,
            folders=folders,
            **dict(
                additional_fields=additional_fields,
                restriction=restriction,
                order_fields=order_fields,
                query_string=query_string,
                shape=shape,
                depth=depth,
                calendar_view=calendar_view,
                page_size=self.chunk_size,
                offset=offset,
            )
        ))

    def _elems_to_objs(self, elems):
        from ..folders.base import BaseFolder
        from ..items import Item, ID_ONLY
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            if self.shape == ID_ONLY and self.additional_fields is None:
                yield Item.id_from_xml(elem)
                continue
            yield BaseFolder.item_model_from_tag(elem.tag).from_xml(elem=elem, account=self.account)

    def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth,
                    calendar_view, page_size, offset=0):
        finditem = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
        itemshape = create_shape_element(
            tag='m:ItemShape', shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        finditem.append(itemshape)
        if calendar_view is None:
            view_type = create_element(
                'm:IndexedPageItemView',
                attrs=OrderedDict([
                    ('MaxEntriesReturned', str(page_size)),
                    ('Offset', str(offset)),
                    ('BasePoint', 'Beginning'),
                ])
            )
        else:
            view_type = calendar_view.to_xml(version=self.account.version)
        finditem.append(view_type)
        if restriction:
            finditem.append(restriction.to_xml(version=self.account.version))
        if order_fields:
            finditem.append(set_xml_value(
                create_element('m:SortOrder'),
                order_fields,
                version=self.account.version
            ))
        finditem.append(set_xml_value(
            create_element('m:ParentFolderIds'),
            folders,
            version=self.account.version
        ))
        if query_string:
            finditem.append(query_string.to_xml(version=self.account.version))
        return finditem

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var paging_container_name
var supports_paging

Methods

def call(self, folders, additional_fields, restriction, order_fields, shape, query_string, depth, calendar_view, max_items, offset)

Find items in an account.

:param folders: the folders to act on :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects :param restriction: a Restriction object for :param order_fields: the fields to sort the results by :param shape: The set of attributes to return :param query_string: a QueryString object :param depth: How deep in the folder structure to search for items :param calendar_view: If set, returns recurring calendar items unfolded :param max_items: the max number of items to return :param offset: the offset relative to the first item in the item collection. Usually 0.

:return: XML elements for the matching items

Expand source code
def call(self, folders, additional_fields, restriction, order_fields, shape, query_string, depth, calendar_view,
         max_items, offset):
    """Find items in an account.

    :param folders: the folders to act on
    :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects
    :param restriction: a Restriction object for
    :param order_fields: the fields to sort the results by
    :param shape: The set of attributes to return
    :param query_string: a QueryString object
    :param depth: How deep in the folder structure to search for items
    :param calendar_view: If set, returns recurring calendar items unfolded
    :param max_items: the max number of items to return
    :param offset: the offset relative to the first item in the item collection. Usually 0.

    :return: XML elements for the matching items
    """
    self.additional_fields = additional_fields
    self.shape = shape
    return self._elems_to_objs(self._paged_call(
        payload_func=self.get_payload,
        max_items=max_items,
        folders=folders,
        **dict(
            additional_fields=additional_fields,
            restriction=restriction,
            order_fields=order_fields,
            query_string=query_string,
            shape=shape,
            depth=depth,
            calendar_view=calendar_view,
            page_size=self.chunk_size,
            offset=offset,
        )
    ))
def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, calendar_view, page_size, offset=0)
Expand source code
def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth,
                calendar_view, page_size, offset=0):
    finditem = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
    itemshape = create_shape_element(
        tag='m:ItemShape', shape=shape, additional_fields=additional_fields, version=self.account.version
    )
    finditem.append(itemshape)
    if calendar_view is None:
        view_type = create_element(
            'm:IndexedPageItemView',
            attrs=OrderedDict([
                ('MaxEntriesReturned', str(page_size)),
                ('Offset', str(offset)),
                ('BasePoint', 'Beginning'),
            ])
        )
    else:
        view_type = calendar_view.to_xml(version=self.account.version)
    finditem.append(view_type)
    if restriction:
        finditem.append(restriction.to_xml(version=self.account.version))
    if order_fields:
        finditem.append(set_xml_value(
            create_element('m:SortOrder'),
            order_fields,
            version=self.account.version
        ))
    finditem.append(set_xml_value(
        create_element('m:ParentFolderIds'),
        folders,
        version=self.account.version
    ))
    if query_string:
        finditem.append(query_string.to_xml(version=self.account.version))
    return finditem

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/find_people.html000066400000000000000000000601751414601472700242100ustar00rootroot00000000000000 exchangelib.services.find_people API documentation

Module exchangelib.services.find_people

Expand source code
import logging
from collections import OrderedDict

from .common import EWSAccountService, create_shape_element
from ..util import create_element, set_xml_value, MNS
from ..version import EXCHANGE_2013

log = logging.getLogger(__name__)


class FindPeople(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/findpeople-operation"""

    SERVICE_NAME = 'FindPeople'
    element_container_name = '{%s}People' % MNS
    supported_from = EXCHANGE_2013
    supports_paging = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # A hack to communicate parsing args to _elems_to_objs()
        self.additional_fields = None
        self.shape = None

    def call(self, folder, additional_fields, restriction, order_fields, shape, query_string, depth, max_items, offset):
        """Find items in an account.

        :param folder: the Folder object to query
        :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects
        :param restriction: a Restriction object for
        :param order_fields: the fields to sort the results by
        :param shape: The set of attributes to return
        :param query_string: a QueryString object
        :param depth: How deep in the folder structure to search for items
        :param max_items: the max number of items to return
        :param offset: the offset relative to the first item in the item collection. Usually 0.

        :return: XML elements for the matching items
        """
        self.additional_fields = additional_fields
        self.shape = shape
        return self._elems_to_objs(self._paged_call(
            payload_func=self.get_payload,
            max_items=max_items,
            folders=[folder],  # We can only query one folder, so there will only be one element in response
            **dict(
                additional_fields=additional_fields,
                restriction=restriction,
                order_fields=order_fields,
                query_string=query_string,
                shape=shape,
                depth=depth,
                page_size=self.chunk_size,
                offset=offset,
            )
        ))

    def _elems_to_objs(self, elems):
        from ..items import Persona, ID_ONLY
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            if self.shape == ID_ONLY and self.additional_fields is None:
                yield Persona.id_from_xml(elem)
                continue
            yield Persona.from_xml(elem, account=self.account)

    def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, page_size,
                    offset=0):
        folders = list(folders)
        if len(folders) != 1:
            raise ValueError('%r can only query one folder' % self.SERVICE_NAME)
        folder = folders[0]
        findpeople = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
        personashape = create_shape_element(
            tag='m:PersonaShape', shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        findpeople.append(personashape)
        view_type = create_element(
            'm:IndexedPageItemView',
            attrs=OrderedDict([
                ('MaxEntriesReturned', str(page_size)),
                ('Offset', str(offset)),
                ('BasePoint', 'Beginning'),
            ])
        )
        findpeople.append(view_type)
        if restriction:
            findpeople.append(restriction.to_xml(version=self.account.version))
        if order_fields:
            findpeople.append(set_xml_value(
                create_element('m:SortOrder'),
                order_fields,
                version=self.account.version
            ))
        findpeople.append(set_xml_value(
            create_element('m:ParentFolderId'),
            folder,
            version=self.account.version
        ))
        if query_string:
            findpeople.append(query_string.to_xml(version=self.account.version))
        return findpeople

    @staticmethod
    def _get_paging_values(elem):
        """Find paging values. The paging element from FindPeople is different from other paging containers."""
        item_count = int(elem.find('{%s}TotalNumberOfPeopleInView' % MNS).text)
        first_matching = int(elem.find('{%s}FirstMatchingRowIndex' % MNS).text)
        first_loaded = int(elem.find('{%s}FirstLoadedRowIndex' % MNS).text)
        log.debug('Got page with total items %s, first matching %s, first loaded %s ', item_count, first_matching,
                  first_loaded)
        next_offset = None  # GetPersona does not support fetching more pages
        return item_count, next_offset

Classes

class FindPeople (*args, **kwargs)
Expand source code
class FindPeople(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/findpeople-operation"""

    SERVICE_NAME = 'FindPeople'
    element_container_name = '{%s}People' % MNS
    supported_from = EXCHANGE_2013
    supports_paging = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # A hack to communicate parsing args to _elems_to_objs()
        self.additional_fields = None
        self.shape = None

    def call(self, folder, additional_fields, restriction, order_fields, shape, query_string, depth, max_items, offset):
        """Find items in an account.

        :param folder: the Folder object to query
        :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects
        :param restriction: a Restriction object for
        :param order_fields: the fields to sort the results by
        :param shape: The set of attributes to return
        :param query_string: a QueryString object
        :param depth: How deep in the folder structure to search for items
        :param max_items: the max number of items to return
        :param offset: the offset relative to the first item in the item collection. Usually 0.

        :return: XML elements for the matching items
        """
        self.additional_fields = additional_fields
        self.shape = shape
        return self._elems_to_objs(self._paged_call(
            payload_func=self.get_payload,
            max_items=max_items,
            folders=[folder],  # We can only query one folder, so there will only be one element in response
            **dict(
                additional_fields=additional_fields,
                restriction=restriction,
                order_fields=order_fields,
                query_string=query_string,
                shape=shape,
                depth=depth,
                page_size=self.chunk_size,
                offset=offset,
            )
        ))

    def _elems_to_objs(self, elems):
        from ..items import Persona, ID_ONLY
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            if self.shape == ID_ONLY and self.additional_fields is None:
                yield Persona.id_from_xml(elem)
                continue
            yield Persona.from_xml(elem, account=self.account)

    def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, page_size,
                    offset=0):
        folders = list(folders)
        if len(folders) != 1:
            raise ValueError('%r can only query one folder' % self.SERVICE_NAME)
        folder = folders[0]
        findpeople = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
        personashape = create_shape_element(
            tag='m:PersonaShape', shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        findpeople.append(personashape)
        view_type = create_element(
            'm:IndexedPageItemView',
            attrs=OrderedDict([
                ('MaxEntriesReturned', str(page_size)),
                ('Offset', str(offset)),
                ('BasePoint', 'Beginning'),
            ])
        )
        findpeople.append(view_type)
        if restriction:
            findpeople.append(restriction.to_xml(version=self.account.version))
        if order_fields:
            findpeople.append(set_xml_value(
                create_element('m:SortOrder'),
                order_fields,
                version=self.account.version
            ))
        findpeople.append(set_xml_value(
            create_element('m:ParentFolderId'),
            folder,
            version=self.account.version
        ))
        if query_string:
            findpeople.append(query_string.to_xml(version=self.account.version))
        return findpeople

    @staticmethod
    def _get_paging_values(elem):
        """Find paging values. The paging element from FindPeople is different from other paging containers."""
        item_count = int(elem.find('{%s}TotalNumberOfPeopleInView' % MNS).text)
        first_matching = int(elem.find('{%s}FirstMatchingRowIndex' % MNS).text)
        first_loaded = int(elem.find('{%s}FirstLoadedRowIndex' % MNS).text)
        log.debug('Got page with total items %s, first matching %s, first loaded %s ', item_count, first_matching,
                  first_loaded)
        next_offset = None  # GetPersona does not support fetching more pages
        return item_count, next_offset

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var supported_from
var supports_paging

Methods

def call(self, folder, additional_fields, restriction, order_fields, shape, query_string, depth, max_items, offset)

Find items in an account.

:param folder: the Folder object to query :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects :param restriction: a Restriction object for :param order_fields: the fields to sort the results by :param shape: The set of attributes to return :param query_string: a QueryString object :param depth: How deep in the folder structure to search for items :param max_items: the max number of items to return :param offset: the offset relative to the first item in the item collection. Usually 0.

:return: XML elements for the matching items

Expand source code
def call(self, folder, additional_fields, restriction, order_fields, shape, query_string, depth, max_items, offset):
    """Find items in an account.

    :param folder: the Folder object to query
    :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects
    :param restriction: a Restriction object for
    :param order_fields: the fields to sort the results by
    :param shape: The set of attributes to return
    :param query_string: a QueryString object
    :param depth: How deep in the folder structure to search for items
    :param max_items: the max number of items to return
    :param offset: the offset relative to the first item in the item collection. Usually 0.

    :return: XML elements for the matching items
    """
    self.additional_fields = additional_fields
    self.shape = shape
    return self._elems_to_objs(self._paged_call(
        payload_func=self.get_payload,
        max_items=max_items,
        folders=[folder],  # We can only query one folder, so there will only be one element in response
        **dict(
            additional_fields=additional_fields,
            restriction=restriction,
            order_fields=order_fields,
            query_string=query_string,
            shape=shape,
            depth=depth,
            page_size=self.chunk_size,
            offset=offset,
        )
    ))
def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, page_size, offset=0)
Expand source code
def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, page_size,
                offset=0):
    folders = list(folders)
    if len(folders) != 1:
        raise ValueError('%r can only query one folder' % self.SERVICE_NAME)
    folder = folders[0]
    findpeople = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
    personashape = create_shape_element(
        tag='m:PersonaShape', shape=shape, additional_fields=additional_fields, version=self.account.version
    )
    findpeople.append(personashape)
    view_type = create_element(
        'm:IndexedPageItemView',
        attrs=OrderedDict([
            ('MaxEntriesReturned', str(page_size)),
            ('Offset', str(offset)),
            ('BasePoint', 'Beginning'),
        ])
    )
    findpeople.append(view_type)
    if restriction:
        findpeople.append(restriction.to_xml(version=self.account.version))
    if order_fields:
        findpeople.append(set_xml_value(
            create_element('m:SortOrder'),
            order_fields,
            version=self.account.version
        ))
    findpeople.append(set_xml_value(
        create_element('m:ParentFolderId'),
        folder,
        version=self.account.version
    ))
    if query_string:
        findpeople.append(query_string.to_xml(version=self.account.version))
    return findpeople

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/get_attachment.html000066400000000000000000000612501414601472700247060ustar00rootroot00000000000000 exchangelib.services.get_attachment API documentation

Module exchangelib.services.get_attachment

Expand source code
from itertools import chain

from .common import EWSAccountService, create_attachment_ids_element
from ..util import create_element, add_xml_child, set_xml_value, DummyResponse, StreamingBase64Parser,\
    StreamingContentHandler, ElementNotFound, MNS

# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/bodytype
BODY_TYPE_CHOICES = ('Best', 'HTML', 'Text')


class GetAttachment(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getattachment-operation"""

    SERVICE_NAME = 'GetAttachment'
    element_container_name = '{%s}Attachments' % MNS

    def call(self, items, include_mime_content, body_type, filter_html_content, additional_fields):
        if body_type and body_type not in BODY_TYPE_CHOICES:
            raise ValueError("'body_type' %s must be one of %s" % (body_type, BODY_TYPE_CHOICES))
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload, items=items, include_mime_content=include_mime_content,
            body_type=body_type, filter_html_content=filter_html_content, additional_fields=additional_fields,
        ))

    def _elems_to_objs(self, elems):
        from ..attachments import FileAttachment, ItemAttachment
        cls_map = {cls.response_tag(): cls for cls in (FileAttachment, ItemAttachment)}
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield cls_map[elem.tag].from_xml(elem=elem, account=self.account)

    def get_payload(self, items, include_mime_content, body_type, filter_html_content, additional_fields):
        payload = create_element('m:%s' % self.SERVICE_NAME)
        shape_elem = create_element('m:AttachmentShape')
        if include_mime_content:
            add_xml_child(shape_elem, 't:IncludeMimeContent', 'true')
        if body_type:
            add_xml_child(shape_elem, 't:BodyType', body_type)
        if filter_html_content is not None:
            add_xml_child(shape_elem, 't:FilterHtmlContent', 'true' if filter_html_content else 'false')
        if additional_fields:
            additional_properties = create_element('t:AdditionalProperties')
            expanded_fields = chain(*(f.expand(version=self.account.version) for f in additional_fields))
            set_xml_value(additional_properties, sorted(
                expanded_fields,
                key=lambda f: (getattr(f.field, 'field_uri', ''), f.path)
            ), version=self.account.version)
            shape_elem.append(additional_properties)
        if len(shape_elem):
            payload.append(shape_elem)
        attachment_ids = create_attachment_ids_element(items=items, version=self.account.version)
        payload.append(attachment_ids)
        return payload

    def _update_api_version(self, api_version, header, **parse_opts):
        if not parse_opts.get('stream_file_content', False):
            super()._update_api_version(api_version, header, **parse_opts)
        # TODO: We're skipping this part in streaming mode because StreamingBase64Parser cannot parse the SOAP header

    @classmethod
    def _get_soap_parts(cls, response, **parse_opts):
        if not parse_opts.get('stream_file_content', False):
            return super()._get_soap_parts(response, **parse_opts)

        # Pass the response unaltered. We want to use our custom streaming parser
        return None, response

    def _get_soap_messages(self, body, **parse_opts):
        if not parse_opts.get('stream_file_content', False):
            return super()._get_soap_messages(body, **parse_opts)

        from ..attachments import FileAttachment
        # 'body' is actually the raw response passed on by '_get_soap_parts'
        r = body
        parser = StreamingBase64Parser()
        field = FileAttachment.get_field_by_fieldname('_content')
        handler = StreamingContentHandler(parser=parser, ns=field.namespace, element_name=field.field_uri)
        parser.setContentHandler(handler)
        return parser.parse(r)

    def stream_file_content(self, attachment_id):
        # The streaming XML parser can only stream content of one attachment
        payload = self.get_payload(
            items=[attachment_id], include_mime_content=False, body_type=None, filter_html_content=None,
            additional_fields=None,
        )
        self.streaming = True
        try:
            yield from self._get_response_xml(payload=payload, stream_file_content=True)
        except ElementNotFound as enf:
            # When the returned XML does not contain a Content element, ElementNotFound is thrown by parser.parse().
            # Let the non-streaming SOAP parser parse the response and hook into the normal exception handling.
            # Wrap in DummyResponse because _get_soap_parts() expects an iter_content() method.
            response = DummyResponse(url=None, headers=None, request_headers=None, content=enf.data)
            _, body = super()._get_soap_parts(response=response)
            res = super()._get_soap_messages(body=body)
            for e in self._get_elements_in_response(response=res):
                if isinstance(e, Exception):
                    raise e
            # The returned content did not contain any EWS exceptions. Give up and re-raise the original exception.
            raise enf
        finally:
            self.streaming = False
            self.stop_streaming()

Classes

class GetAttachment (*args, **kwargs)
Expand source code
class GetAttachment(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getattachment-operation"""

    SERVICE_NAME = 'GetAttachment'
    element_container_name = '{%s}Attachments' % MNS

    def call(self, items, include_mime_content, body_type, filter_html_content, additional_fields):
        if body_type and body_type not in BODY_TYPE_CHOICES:
            raise ValueError("'body_type' %s must be one of %s" % (body_type, BODY_TYPE_CHOICES))
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload, items=items, include_mime_content=include_mime_content,
            body_type=body_type, filter_html_content=filter_html_content, additional_fields=additional_fields,
        ))

    def _elems_to_objs(self, elems):
        from ..attachments import FileAttachment, ItemAttachment
        cls_map = {cls.response_tag(): cls for cls in (FileAttachment, ItemAttachment)}
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield cls_map[elem.tag].from_xml(elem=elem, account=self.account)

    def get_payload(self, items, include_mime_content, body_type, filter_html_content, additional_fields):
        payload = create_element('m:%s' % self.SERVICE_NAME)
        shape_elem = create_element('m:AttachmentShape')
        if include_mime_content:
            add_xml_child(shape_elem, 't:IncludeMimeContent', 'true')
        if body_type:
            add_xml_child(shape_elem, 't:BodyType', body_type)
        if filter_html_content is not None:
            add_xml_child(shape_elem, 't:FilterHtmlContent', 'true' if filter_html_content else 'false')
        if additional_fields:
            additional_properties = create_element('t:AdditionalProperties')
            expanded_fields = chain(*(f.expand(version=self.account.version) for f in additional_fields))
            set_xml_value(additional_properties, sorted(
                expanded_fields,
                key=lambda f: (getattr(f.field, 'field_uri', ''), f.path)
            ), version=self.account.version)
            shape_elem.append(additional_properties)
        if len(shape_elem):
            payload.append(shape_elem)
        attachment_ids = create_attachment_ids_element(items=items, version=self.account.version)
        payload.append(attachment_ids)
        return payload

    def _update_api_version(self, api_version, header, **parse_opts):
        if not parse_opts.get('stream_file_content', False):
            super()._update_api_version(api_version, header, **parse_opts)
        # TODO: We're skipping this part in streaming mode because StreamingBase64Parser cannot parse the SOAP header

    @classmethod
    def _get_soap_parts(cls, response, **parse_opts):
        if not parse_opts.get('stream_file_content', False):
            return super()._get_soap_parts(response, **parse_opts)

        # Pass the response unaltered. We want to use our custom streaming parser
        return None, response

    def _get_soap_messages(self, body, **parse_opts):
        if not parse_opts.get('stream_file_content', False):
            return super()._get_soap_messages(body, **parse_opts)

        from ..attachments import FileAttachment
        # 'body' is actually the raw response passed on by '_get_soap_parts'
        r = body
        parser = StreamingBase64Parser()
        field = FileAttachment.get_field_by_fieldname('_content')
        handler = StreamingContentHandler(parser=parser, ns=field.namespace, element_name=field.field_uri)
        parser.setContentHandler(handler)
        return parser.parse(r)

    def stream_file_content(self, attachment_id):
        # The streaming XML parser can only stream content of one attachment
        payload = self.get_payload(
            items=[attachment_id], include_mime_content=False, body_type=None, filter_html_content=None,
            additional_fields=None,
        )
        self.streaming = True
        try:
            yield from self._get_response_xml(payload=payload, stream_file_content=True)
        except ElementNotFound as enf:
            # When the returned XML does not contain a Content element, ElementNotFound is thrown by parser.parse().
            # Let the non-streaming SOAP parser parse the response and hook into the normal exception handling.
            # Wrap in DummyResponse because _get_soap_parts() expects an iter_content() method.
            response = DummyResponse(url=None, headers=None, request_headers=None, content=enf.data)
            _, body = super()._get_soap_parts(response=response)
            res = super()._get_soap_messages(body=body)
            for e in self._get_elements_in_response(response=res):
                if isinstance(e, Exception):
                    raise e
            # The returned content did not contain any EWS exceptions. Give up and re-raise the original exception.
            raise enf
        finally:
            self.streaming = False
            self.stop_streaming()

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, items, include_mime_content, body_type, filter_html_content, additional_fields)
Expand source code
def call(self, items, include_mime_content, body_type, filter_html_content, additional_fields):
    if body_type and body_type not in BODY_TYPE_CHOICES:
        raise ValueError("'body_type' %s must be one of %s" % (body_type, BODY_TYPE_CHOICES))
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload, items=items, include_mime_content=include_mime_content,
        body_type=body_type, filter_html_content=filter_html_content, additional_fields=additional_fields,
    ))
def get_payload(self, items, include_mime_content, body_type, filter_html_content, additional_fields)
Expand source code
def get_payload(self, items, include_mime_content, body_type, filter_html_content, additional_fields):
    payload = create_element('m:%s' % self.SERVICE_NAME)
    shape_elem = create_element('m:AttachmentShape')
    if include_mime_content:
        add_xml_child(shape_elem, 't:IncludeMimeContent', 'true')
    if body_type:
        add_xml_child(shape_elem, 't:BodyType', body_type)
    if filter_html_content is not None:
        add_xml_child(shape_elem, 't:FilterHtmlContent', 'true' if filter_html_content else 'false')
    if additional_fields:
        additional_properties = create_element('t:AdditionalProperties')
        expanded_fields = chain(*(f.expand(version=self.account.version) for f in additional_fields))
        set_xml_value(additional_properties, sorted(
            expanded_fields,
            key=lambda f: (getattr(f.field, 'field_uri', ''), f.path)
        ), version=self.account.version)
        shape_elem.append(additional_properties)
    if len(shape_elem):
        payload.append(shape_elem)
    attachment_ids = create_attachment_ids_element(items=items, version=self.account.version)
    payload.append(attachment_ids)
    return payload
def stream_file_content(self, attachment_id)
Expand source code
def stream_file_content(self, attachment_id):
    # The streaming XML parser can only stream content of one attachment
    payload = self.get_payload(
        items=[attachment_id], include_mime_content=False, body_type=None, filter_html_content=None,
        additional_fields=None,
    )
    self.streaming = True
    try:
        yield from self._get_response_xml(payload=payload, stream_file_content=True)
    except ElementNotFound as enf:
        # When the returned XML does not contain a Content element, ElementNotFound is thrown by parser.parse().
        # Let the non-streaming SOAP parser parse the response and hook into the normal exception handling.
        # Wrap in DummyResponse because _get_soap_parts() expects an iter_content() method.
        response = DummyResponse(url=None, headers=None, request_headers=None, content=enf.data)
        _, body = super()._get_soap_parts(response=response)
        res = super()._get_soap_messages(body=body)
        for e in self._get_elements_in_response(response=res):
            if isinstance(e, Exception):
                raise e
        # The returned content did not contain any EWS exceptions. Give up and re-raise the original exception.
        raise enf
    finally:
        self.streaming = False
        self.stop_streaming()

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/get_delegate.html000066400000000000000000000341731414601472700243340ustar00rootroot00000000000000 exchangelib.services.get_delegate API documentation

Module exchangelib.services.get_delegate

Expand source code
from .common import EWSAccountService
from ..properties import DLMailbox, DelegateUser  # The service expects a Mailbox element in the MNS namespace
from ..util import create_element, set_xml_value, MNS
from ..version import EXCHANGE_2007_SP1


class GetDelegate(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getdelegate-operation"""

    SERVICE_NAME = 'GetDelegate'
    supported_from = EXCHANGE_2007_SP1

    def call(self, user_ids, include_permissions):
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=user_ids or [None],
            mailbox=DLMailbox(email_address=self.account.primary_smtp_address),
            include_permissions=include_permissions,
        ))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield DelegateUser.from_xml(elem=elem, account=self.account)

    def get_payload(self, user_ids, mailbox, include_permissions):
        payload = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=dict(IncludePermissions='true' if include_permissions else 'false'),
        )
        set_xml_value(payload, mailbox, version=self.protocol.version)
        if user_ids != [None]:
            set_xml_value(payload, user_ids, version=self.protocol.version)
        return payload

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(DelegateUser.response_tag())

    @classmethod
    def _response_message_tag(cls):
        return '{%s}DelegateUserResponseMessageType' % MNS

Classes

class GetDelegate (*args, **kwargs)
Expand source code
class GetDelegate(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getdelegate-operation"""

    SERVICE_NAME = 'GetDelegate'
    supported_from = EXCHANGE_2007_SP1

    def call(self, user_ids, include_permissions):
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=user_ids or [None],
            mailbox=DLMailbox(email_address=self.account.primary_smtp_address),
            include_permissions=include_permissions,
        ))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield DelegateUser.from_xml(elem=elem, account=self.account)

    def get_payload(self, user_ids, mailbox, include_permissions):
        payload = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=dict(IncludePermissions='true' if include_permissions else 'false'),
        )
        set_xml_value(payload, mailbox, version=self.protocol.version)
        if user_ids != [None]:
            set_xml_value(payload, user_ids, version=self.protocol.version)
        return payload

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(DelegateUser.response_tag())

    @classmethod
    def _response_message_tag(cls):
        return '{%s}DelegateUserResponseMessageType' % MNS

Ancestors

Class variables

var SERVICE_NAME
var supported_from

Methods

def call(self, user_ids, include_permissions)
Expand source code
def call(self, user_ids, include_permissions):
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload,
        items=user_ids or [None],
        mailbox=DLMailbox(email_address=self.account.primary_smtp_address),
        include_permissions=include_permissions,
    ))
def get_payload(self, user_ids, mailbox, include_permissions)
Expand source code
def get_payload(self, user_ids, mailbox, include_permissions):
    payload = create_element(
        'm:%s' % self.SERVICE_NAME,
        attrs=dict(IncludePermissions='true' if include_permissions else 'false'),
    )
    set_xml_value(payload, mailbox, version=self.protocol.version)
    if user_ids != [None]:
        set_xml_value(payload, user_ids, version=self.protocol.version)
    return payload

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/get_events.html000066400000000000000000000320111414601472700240530ustar00rootroot00000000000000 exchangelib.services.get_events API documentation

Module exchangelib.services.get_events

Expand source code
import logging

from .common import EWSAccountService, add_xml_child
from ..properties import Notification
from ..util import create_element

log = logging.getLogger(__name__)


class GetEvents(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getevents-operation
    """

    SERVICE_NAME = 'GetEvents'
    prefer_affinity = True

    def call(self, subscription_id, watermark):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
                subscription_id=subscription_id, watermark=watermark,
        )))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Notification.from_xml(elem=elem, account=None)

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(Notification.response_tag())

    def get_payload(self, subscription_id, watermark):
        getstreamingevents = create_element('m:%s' % self.SERVICE_NAME)
        add_xml_child(getstreamingevents, 'm:SubscriptionId', subscription_id)
        add_xml_child(getstreamingevents, 'm:Watermark', watermark)
        return getstreamingevents

Classes

class GetEvents (*args, **kwargs)
Expand source code
class GetEvents(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getevents-operation
    """

    SERVICE_NAME = 'GetEvents'
    prefer_affinity = True

    def call(self, subscription_id, watermark):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
                subscription_id=subscription_id, watermark=watermark,
        )))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Notification.from_xml(elem=elem, account=None)

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(Notification.response_tag())

    def get_payload(self, subscription_id, watermark):
        getstreamingevents = create_element('m:%s' % self.SERVICE_NAME)
        add_xml_child(getstreamingevents, 'm:SubscriptionId', subscription_id)
        add_xml_child(getstreamingevents, 'm:Watermark', watermark)
        return getstreamingevents

Ancestors

Class variables

var SERVICE_NAME
var prefer_affinity

Methods

def call(self, subscription_id, watermark)
Expand source code
def call(self, subscription_id, watermark):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            subscription_id=subscription_id, watermark=watermark,
    )))
def get_payload(self, subscription_id, watermark)
Expand source code
def get_payload(self, subscription_id, watermark):
    getstreamingevents = create_element('m:%s' % self.SERVICE_NAME)
    add_xml_child(getstreamingevents, 'm:SubscriptionId', subscription_id)
    add_xml_child(getstreamingevents, 'm:Watermark', watermark)
    return getstreamingevents

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/get_folder.html000066400000000000000000000422411414601472700240300ustar00rootroot00000000000000 exchangelib.services.get_folder API documentation

Module exchangelib.services.get_folder

Expand source code
from .common import EWSAccountService, parse_folder_elem, create_folder_ids_element, \
    create_shape_element
from ..errors import ErrorFolderNotFound, ErrorNoPublicFolderReplicaAvailable, ErrorInvalidOperation
from ..util import create_element, MNS


class GetFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getfolder-operation"""

    SERVICE_NAME = 'GetFolder'
    element_container_name = '{%s}Folders' % MNS
    ERRORS_TO_CATCH_IN_RESPONSE = EWSAccountService.ERRORS_TO_CATCH_IN_RESPONSE + (
        ErrorFolderNotFound, ErrorNoPublicFolderReplicaAvailable, ErrorInvalidOperation,
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.folders = []  # A hack to communicate parsing args to _elems_to_objs()

    def call(self, folders, additional_fields, shape):
        """Take a folder ID and returns the full information for that folder.

        :param folders: a list of Folder objects
        :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects
        :param shape: The set of attributes to return

        :return: XML elements for the folders, in stable order
        """
        # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same
        # class as the folder instance it was requested with.
        self.folders = list(folders)  # Convert to a list, in case 'folders' is a generator. We're iterating twice.
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=self.folders,
            additional_fields=additional_fields,
            shape=shape,
        ))

    def _elems_to_objs(self, elems):
        for folder, elem in zip(self.folders, elems):
            if isinstance(elem, Exception):
                yield elem
                continue
            yield parse_folder_elem(elem=elem, folder=folder, account=self.account)

    def get_payload(self, folders, additional_fields, shape):
        getfolder = create_element('m:%s' % self.SERVICE_NAME)
        foldershape = create_shape_element(
            tag='m:FolderShape', shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        getfolder.append(foldershape)
        folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
        getfolder.append(folder_ids)
        return getfolder

Classes

class GetFolder (*args, **kwargs)
Expand source code
class GetFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getfolder-operation"""

    SERVICE_NAME = 'GetFolder'
    element_container_name = '{%s}Folders' % MNS
    ERRORS_TO_CATCH_IN_RESPONSE = EWSAccountService.ERRORS_TO_CATCH_IN_RESPONSE + (
        ErrorFolderNotFound, ErrorNoPublicFolderReplicaAvailable, ErrorInvalidOperation,
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.folders = []  # A hack to communicate parsing args to _elems_to_objs()

    def call(self, folders, additional_fields, shape):
        """Take a folder ID and returns the full information for that folder.

        :param folders: a list of Folder objects
        :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects
        :param shape: The set of attributes to return

        :return: XML elements for the folders, in stable order
        """
        # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same
        # class as the folder instance it was requested with.
        self.folders = list(folders)  # Convert to a list, in case 'folders' is a generator. We're iterating twice.
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=self.folders,
            additional_fields=additional_fields,
            shape=shape,
        ))

    def _elems_to_objs(self, elems):
        for folder, elem in zip(self.folders, elems):
            if isinstance(elem, Exception):
                yield elem
                continue
            yield parse_folder_elem(elem=elem, folder=folder, account=self.account)

    def get_payload(self, folders, additional_fields, shape):
        getfolder = create_element('m:%s' % self.SERVICE_NAME)
        foldershape = create_shape_element(
            tag='m:FolderShape', shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        getfolder.append(foldershape)
        folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
        getfolder.append(folder_ids)
        return getfolder

Ancestors

Class variables

var ERRORS_TO_CATCH_IN_RESPONSE
var SERVICE_NAME
var element_container_name

Methods

def call(self, folders, additional_fields, shape)

Take a folder ID and returns the full information for that folder.

:param folders: a list of Folder objects :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects :param shape: The set of attributes to return

:return: XML elements for the folders, in stable order

Expand source code
def call(self, folders, additional_fields, shape):
    """Take a folder ID and returns the full information for that folder.

    :param folders: a list of Folder objects
    :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects
    :param shape: The set of attributes to return

    :return: XML elements for the folders, in stable order
    """
    # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same
    # class as the folder instance it was requested with.
    self.folders = list(folders)  # Convert to a list, in case 'folders' is a generator. We're iterating twice.
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload,
        items=self.folders,
        additional_fields=additional_fields,
        shape=shape,
    ))
def get_payload(self, folders, additional_fields, shape)
Expand source code
def get_payload(self, folders, additional_fields, shape):
    getfolder = create_element('m:%s' % self.SERVICE_NAME)
    foldershape = create_shape_element(
        tag='m:FolderShape', shape=shape, additional_fields=additional_fields, version=self.account.version
    )
    getfolder.append(foldershape)
    folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
    getfolder.append(folder_ids)
    return getfolder

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/get_item.html000066400000000000000000000356161414601472700235230ustar00rootroot00000000000000 exchangelib.services.get_item API documentation

Module exchangelib.services.get_item

Expand source code
from .common import EWSAccountService, create_item_ids_element, create_shape_element
from ..util import create_element, MNS


class GetItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getitem-operation"""

    SERVICE_NAME = 'GetItem'
    element_container_name = '{%s}Items' % MNS

    def call(self, items, additional_fields, shape):
        """Return all items in an account that correspond to a list of ID's, in stable order.

        :param items: a list of (id, changekey) tuples or Item objects
        :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects
        :param shape: The shape of returned objects

        :return: XML elements for the items, in stable order
        """
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload, items=items, additional_fields=additional_fields, shape=shape,
        ))

    def _elems_to_objs(self, elems):
        from ..folders.base import BaseFolder
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield BaseFolder.item_model_from_tag(elem.tag).from_xml(elem=elem, account=self.account)

    def get_payload(self, items, additional_fields, shape):
        getitem = create_element('m:%s' % self.SERVICE_NAME)
        itemshape = create_shape_element(
            tag='m:ItemShape', shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        getitem.append(itemshape)
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        getitem.append(item_ids)
        return getitem

Classes

class GetItem (*args, **kwargs)
Expand source code
class GetItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getitem-operation"""

    SERVICE_NAME = 'GetItem'
    element_container_name = '{%s}Items' % MNS

    def call(self, items, additional_fields, shape):
        """Return all items in an account that correspond to a list of ID's, in stable order.

        :param items: a list of (id, changekey) tuples or Item objects
        :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects
        :param shape: The shape of returned objects

        :return: XML elements for the items, in stable order
        """
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload, items=items, additional_fields=additional_fields, shape=shape,
        ))

    def _elems_to_objs(self, elems):
        from ..folders.base import BaseFolder
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield BaseFolder.item_model_from_tag(elem.tag).from_xml(elem=elem, account=self.account)

    def get_payload(self, items, additional_fields, shape):
        getitem = create_element('m:%s' % self.SERVICE_NAME)
        itemshape = create_shape_element(
            tag='m:ItemShape', shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        getitem.append(itemshape)
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        getitem.append(item_ids)
        return getitem

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, items, additional_fields, shape)

Return all items in an account that correspond to a list of ID's, in stable order.

:param items: a list of (id, changekey) tuples or Item objects :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects :param shape: The shape of returned objects

:return: XML elements for the items, in stable order

Expand source code
def call(self, items, additional_fields, shape):
    """Return all items in an account that correspond to a list of ID's, in stable order.

    :param items: a list of (id, changekey) tuples or Item objects
    :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects
    :param shape: The shape of returned objects

    :return: XML elements for the items, in stable order
    """
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload, items=items, additional_fields=additional_fields, shape=shape,
    ))
def get_payload(self, items, additional_fields, shape)
Expand source code
def get_payload(self, items, additional_fields, shape):
    getitem = create_element('m:%s' % self.SERVICE_NAME)
    itemshape = create_shape_element(
        tag='m:ItemShape', shape=shape, additional_fields=additional_fields, version=self.account.version
    )
    getitem.append(itemshape)
    item_ids = create_item_ids_element(items=items, version=self.account.version)
    getitem.append(item_ids)
    return getitem

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/get_mail_tips.html000066400000000000000000000340401414601472700245340ustar00rootroot00000000000000 exchangelib.services.get_mail_tips API documentation

Module exchangelib.services.get_mail_tips

Expand source code
from .common import EWSService
from ..properties import MailTips
from ..util import create_element, set_xml_value, MNS


class GetMailTips(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getmailtips-operation"""

    SERVICE_NAME = 'GetMailTips'

    def call(self, sending_as, recipients, mail_tips_requested):
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=recipients,
            sending_as=sending_as,
            mail_tips_requested=mail_tips_requested,
        ))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield MailTips.from_xml(elem=elem, account=None)

    def get_payload(self, recipients, sending_as,  mail_tips_requested):
        payload = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(payload, sending_as, version=self.protocol.version)

        recipients_elem = create_element('m:Recipients')
        for recipient in recipients:
            set_xml_value(recipients_elem, recipient, version=self.protocol.version)
        if not len(recipients_elem):
            raise ValueError('"recipients" must not be empty')
        payload.append(recipients_elem)

        if mail_tips_requested:
            set_xml_value(payload, mail_tips_requested, version=self.protocol.version)
        return payload

    def _get_elements_in_response(self, response):
        for msg in response:
            yield self._get_element_container(message=msg, name=MailTips.response_tag())

    @classmethod
    def _response_message_tag(cls):
        return '{%s}MailTipsResponseMessageType' % MNS

Classes

class GetMailTips (protocol, chunk_size=None, timeout=None)
Expand source code
class GetMailTips(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getmailtips-operation"""

    SERVICE_NAME = 'GetMailTips'

    def call(self, sending_as, recipients, mail_tips_requested):
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=recipients,
            sending_as=sending_as,
            mail_tips_requested=mail_tips_requested,
        ))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield MailTips.from_xml(elem=elem, account=None)

    def get_payload(self, recipients, sending_as,  mail_tips_requested):
        payload = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(payload, sending_as, version=self.protocol.version)

        recipients_elem = create_element('m:Recipients')
        for recipient in recipients:
            set_xml_value(recipients_elem, recipient, version=self.protocol.version)
        if not len(recipients_elem):
            raise ValueError('"recipients" must not be empty')
        payload.append(recipients_elem)

        if mail_tips_requested:
            set_xml_value(payload, mail_tips_requested, version=self.protocol.version)
        return payload

    def _get_elements_in_response(self, response):
        for msg in response:
            yield self._get_element_container(message=msg, name=MailTips.response_tag())

    @classmethod
    def _response_message_tag(cls):
        return '{%s}MailTipsResponseMessageType' % MNS

Ancestors

Class variables

var SERVICE_NAME

Methods

def call(self, sending_as, recipients, mail_tips_requested)
Expand source code
def call(self, sending_as, recipients, mail_tips_requested):
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload,
        items=recipients,
        sending_as=sending_as,
        mail_tips_requested=mail_tips_requested,
    ))
def get_payload(self, recipients, sending_as, mail_tips_requested)
Expand source code
def get_payload(self, recipients, sending_as,  mail_tips_requested):
    payload = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(payload, sending_as, version=self.protocol.version)

    recipients_elem = create_element('m:Recipients')
    for recipient in recipients:
        set_xml_value(recipients_elem, recipient, version=self.protocol.version)
    if not len(recipients_elem):
        raise ValueError('"recipients" must not be empty')
    payload.append(recipients_elem)

    if mail_tips_requested:
        set_xml_value(payload, mail_tips_requested, version=self.protocol.version)
    return payload

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/get_persona.html000066400000000000000000000313521414601472700242250ustar00rootroot00000000000000 exchangelib.services.get_persona API documentation

Module exchangelib.services.get_persona

Expand source code
from .common import EWSAccountService, to_item_id
from ..properties import PersonaId
from ..util import create_element, set_xml_value, MNS


class GetPersona(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getpersona-operation"""

    SERVICE_NAME = 'GetPersona'

    def call(self, persona):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(persona=persona)))

    def _elems_to_objs(self, elems):
        from ..items import Persona
        elements = list(elems)
        if len(elements) != 1:
            raise ValueError('Expected exactly one element in response')
        elem = elements[0]
        if isinstance(elem, Exception):
            raise elem
        return Persona.from_xml(elem=elem, account=None)

    def get_payload(self, persona):
        version = self.protocol.version
        payload = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(payload, to_item_id(persona, PersonaId, version=version), version=version)
        return payload

    @classmethod
    def _get_elements_in_container(cls, container):
        from ..items import Persona
        return container.findall('{%s}%s' % (MNS, Persona.ELEMENT_NAME))

    @classmethod
    def _response_tag(cls):
        return '{%s}%sResponseMessage' % (MNS, cls.SERVICE_NAME)

Classes

class GetPersona (*args, **kwargs)
Expand source code
class GetPersona(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getpersona-operation"""

    SERVICE_NAME = 'GetPersona'

    def call(self, persona):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(persona=persona)))

    def _elems_to_objs(self, elems):
        from ..items import Persona
        elements = list(elems)
        if len(elements) != 1:
            raise ValueError('Expected exactly one element in response')
        elem = elements[0]
        if isinstance(elem, Exception):
            raise elem
        return Persona.from_xml(elem=elem, account=None)

    def get_payload(self, persona):
        version = self.protocol.version
        payload = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(payload, to_item_id(persona, PersonaId, version=version), version=version)
        return payload

    @classmethod
    def _get_elements_in_container(cls, container):
        from ..items import Persona
        return container.findall('{%s}%s' % (MNS, Persona.ELEMENT_NAME))

    @classmethod
    def _response_tag(cls):
        return '{%s}%sResponseMessage' % (MNS, cls.SERVICE_NAME)

Ancestors

Class variables

var SERVICE_NAME

Methods

def call(self, persona)
Expand source code
def call(self, persona):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(persona=persona)))
def get_payload(self, persona)
Expand source code
def get_payload(self, persona):
    version = self.protocol.version
    payload = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(payload, to_item_id(persona, PersonaId, version=version), version=version)
    return payload

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/get_room_lists.html000066400000000000000000000301771414601472700247540ustar00rootroot00000000000000 exchangelib.services.get_room_lists API documentation

Module exchangelib.services.get_room_lists

Expand source code
from .common import EWSService
from ..properties import RoomList
from ..util import create_element, MNS
from ..version import EXCHANGE_2010


class GetRoomLists(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getroomlists-operation"""

    SERVICE_NAME = 'GetRoomLists'
    element_container_name = '{%s}RoomLists' % MNS
    supported_from = EXCHANGE_2010

    def call(self):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload()))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield RoomList.from_xml(elem=elem, account=None)

    def get_payload(self):
        return create_element('m:%s' % self.SERVICE_NAME)

Classes

class GetRoomLists (protocol, chunk_size=None, timeout=None)
Expand source code
class GetRoomLists(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getroomlists-operation"""

    SERVICE_NAME = 'GetRoomLists'
    element_container_name = '{%s}RoomLists' % MNS
    supported_from = EXCHANGE_2010

    def call(self):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload()))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield RoomList.from_xml(elem=elem, account=None)

    def get_payload(self):
        return create_element('m:%s' % self.SERVICE_NAME)

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var supported_from

Methods

def call(self)
Expand source code
def call(self):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload()))
def get_payload(self)
Expand source code
def get_payload(self):
    return create_element('m:%s' % self.SERVICE_NAME)

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/get_rooms.html000066400000000000000000000305201414601472700237110ustar00rootroot00000000000000 exchangelib.services.get_rooms API documentation

Module exchangelib.services.get_rooms

Expand source code
from .common import EWSService
from ..properties import Room
from ..util import create_element, set_xml_value, MNS
from ..version import EXCHANGE_2010


class GetRooms(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getrooms-operation"""

    SERVICE_NAME = 'GetRooms'
    element_container_name = '{%s}Rooms' % MNS
    supported_from = EXCHANGE_2010

    def call(self, roomlist):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(roomlist=roomlist)))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Room.from_xml(elem=elem, account=None)

    def get_payload(self, roomlist):
        getrooms = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(getrooms, roomlist, version=self.protocol.version)
        return getrooms

Classes

class GetRooms (protocol, chunk_size=None, timeout=None)
Expand source code
class GetRooms(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getrooms-operation"""

    SERVICE_NAME = 'GetRooms'
    element_container_name = '{%s}Rooms' % MNS
    supported_from = EXCHANGE_2010

    def call(self, roomlist):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(roomlist=roomlist)))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Room.from_xml(elem=elem, account=None)

    def get_payload(self, roomlist):
        getrooms = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(getrooms, roomlist, version=self.protocol.version)
        return getrooms

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var supported_from

Methods

def call(self, roomlist)
Expand source code
def call(self, roomlist):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(roomlist=roomlist)))
def get_payload(self, roomlist)
Expand source code
def get_payload(self, roomlist):
    getrooms = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(getrooms, roomlist, version=self.protocol.version)
    return getrooms

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/get_searchable_mailboxes.html000066400000000000000000000413341414601472700267130ustar00rootroot00000000000000 exchangelib.services.get_searchable_mailboxes API documentation

Module exchangelib.services.get_searchable_mailboxes

Expand source code
from .common import EWSService
from ..errors import MalformedResponseError
from ..properties import SearchableMailbox, FailedMailbox
from ..util import create_element, add_xml_child, MNS
from ..version import EXCHANGE_2013


class GetSearchableMailboxes(EWSService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getsearchablemailboxes-operation
    """

    SERVICE_NAME = 'GetSearchableMailboxes'
    element_container_name = '{%s}SearchableMailboxes' % MNS
    failed_mailboxes_container_name = '{%s}FailedMailboxes' % MNS
    supported_from = EXCHANGE_2013

    def call(self, search_filter, expand_group_membership):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
                search_filter=search_filter,
                expand_group_membership=expand_group_membership,
        )))

    def _elems_to_objs(self, elems):
        cls_map = {cls.response_tag(): cls for cls in (SearchableMailbox, FailedMailbox)}
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield cls_map[elem.tag].from_xml(elem=elem, account=None)

    def get_payload(self, search_filter, expand_group_membership):
        payload = create_element('m:%s' % self.SERVICE_NAME)
        if search_filter:
            add_xml_child(payload, 'm:SearchFilter', search_filter)
        if expand_group_membership is not None:
            add_xml_child(payload, 'm:ExpandGroupMembership', 'true' if expand_group_membership else 'false')
        return payload

    def _get_elements_in_response(self, response):
        for msg in response:
            for container_name in (self.element_container_name, self.failed_mailboxes_container_name):
                try:
                    container_or_exc = self._get_element_container(message=msg, name=container_name)
                except MalformedResponseError:
                    # Responses may contain no failed mailboxes. _get_element_container() does not accept this.
                    if container_name == self.failed_mailboxes_container_name:
                        continue
                    raise
                if isinstance(container_or_exc, (bool, Exception)):
                    yield container_or_exc
                    continue
                yield from self._get_elements_in_container(container=container_or_exc)

Classes

class GetSearchableMailboxes (protocol, chunk_size=None, timeout=None)
Expand source code
class GetSearchableMailboxes(EWSService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getsearchablemailboxes-operation
    """

    SERVICE_NAME = 'GetSearchableMailboxes'
    element_container_name = '{%s}SearchableMailboxes' % MNS
    failed_mailboxes_container_name = '{%s}FailedMailboxes' % MNS
    supported_from = EXCHANGE_2013

    def call(self, search_filter, expand_group_membership):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
                search_filter=search_filter,
                expand_group_membership=expand_group_membership,
        )))

    def _elems_to_objs(self, elems):
        cls_map = {cls.response_tag(): cls for cls in (SearchableMailbox, FailedMailbox)}
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield cls_map[elem.tag].from_xml(elem=elem, account=None)

    def get_payload(self, search_filter, expand_group_membership):
        payload = create_element('m:%s' % self.SERVICE_NAME)
        if search_filter:
            add_xml_child(payload, 'm:SearchFilter', search_filter)
        if expand_group_membership is not None:
            add_xml_child(payload, 'm:ExpandGroupMembership', 'true' if expand_group_membership else 'false')
        return payload

    def _get_elements_in_response(self, response):
        for msg in response:
            for container_name in (self.element_container_name, self.failed_mailboxes_container_name):
                try:
                    container_or_exc = self._get_element_container(message=msg, name=container_name)
                except MalformedResponseError:
                    # Responses may contain no failed mailboxes. _get_element_container() does not accept this.
                    if container_name == self.failed_mailboxes_container_name:
                        continue
                    raise
                if isinstance(container_or_exc, (bool, Exception)):
                    yield container_or_exc
                    continue
                yield from self._get_elements_in_container(container=container_or_exc)

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var failed_mailboxes_container_name
var supported_from

Methods

def call(self, search_filter, expand_group_membership)
Expand source code
def call(self, search_filter, expand_group_membership):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            search_filter=search_filter,
            expand_group_membership=expand_group_membership,
    )))
def get_payload(self, search_filter, expand_group_membership)
Expand source code
def get_payload(self, search_filter, expand_group_membership):
    payload = create_element('m:%s' % self.SERVICE_NAME)
    if search_filter:
        add_xml_child(payload, 'm:SearchFilter', search_filter)
    if expand_group_membership is not None:
        add_xml_child(payload, 'm:ExpandGroupMembership', 'true' if expand_group_membership else 'false')
    return payload

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/get_server_time_zones.html000066400000000000000000000565731414601472700263340ustar00rootroot00000000000000 exchangelib.services.get_server_time_zones API documentation

Module exchangelib.services.get_server_time_zones

Expand source code
import datetime

from .common import EWSService
from ..errors import NaiveDateTimeNotAllowed
from ..ewsdatetime import EWSDateTime
from ..fields import WEEKDAY_NAMES
from ..util import create_element, set_xml_value, xml_text_to_value, peek, TNS, MNS
from ..version import EXCHANGE_2010


class GetServerTimeZones(EWSService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getservertimezones-operation
    """

    SERVICE_NAME = 'GetServerTimeZones'
    element_container_name = '{%s}TimeZoneDefinitions' % MNS
    supported_from = EXCHANGE_2010

    def call(self, timezones=None, return_full_timezone_data=False):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            timezones=timezones,
            return_full_timezone_data=return_full_timezone_data
        )))

    def get_payload(self, timezones, return_full_timezone_data):
        payload = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=dict(ReturnFullTimeZoneData='true' if return_full_timezone_data else 'false'),
        )
        if timezones is not None:
            is_empty, timezones = peek(timezones)
            if not is_empty:
                tz_ids = create_element('m:Ids')
                for timezone in timezones:
                    tz_id = set_xml_value(create_element('t:Id'), timezone.ms_id, version=self.protocol.version)
                    tz_ids.append(tz_id)
                payload.append(tz_ids)
        return payload

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            tz_id = elem.get('Id')
            tz_name = elem.get('Name')
            tz_periods = self._get_periods(elem)
            tz_transitions_groups = self._get_transitions_groups(elem)
            tz_transitions = self._get_transitions(elem)
            yield tz_id, tz_name, tz_periods, tz_transitions, tz_transitions_groups

    @staticmethod
    def _get_periods(timezonedef):
        tz_periods = {}
        periods = timezonedef.find('{%s}Periods' % TNS)
        for period in periods.findall('{%s}Period' % TNS):
            # Convert e.g. "trule:Microsoft/Registry/W. Europe Standard Time/2006-Daylight" to (2006, 'Daylight')
            p_year, p_type = period.get('Id').rsplit('/', 1)[1].split('-')
            tz_periods[(int(p_year), p_type)] = dict(
                name=period.get('Name'),
                bias=xml_text_to_value(period.get('Bias'), datetime.timedelta)
            )
        return tz_periods

    @staticmethod
    def _get_transitions_groups(timezonedef):
        tz_transitions_groups = {}
        transitiongroups = timezonedef.find('{%s}TransitionsGroups' % TNS)
        if transitiongroups is not None:
            for transitiongroup in transitiongroups.findall('{%s}TransitionsGroup' % TNS):
                tg_id = int(transitiongroup.get('Id'))
                tz_transitions_groups[tg_id] = []
                for transition in transitiongroup.findall('{%s}Transition' % TNS):
                    # Apply same conversion to To as for period IDs
                    to_year, to_type = transition.find('{%s}To' % TNS).text.rsplit('/', 1)[1].split('-')
                    tz_transitions_groups[tg_id].append(dict(
                        to=(int(to_year), to_type),
                    ))
                for transition in transitiongroup.findall('{%s}RecurringDayTransition' % TNS):
                    # Apply same conversion to To as for period IDs
                    to_year, to_type = transition.find('{%s}To' % TNS).text.rsplit('/', 1)[1].split('-')
                    occurrence = xml_text_to_value(transition.find('{%s}Occurrence' % TNS).text, int)
                    if occurrence == -1:
                        # See TimeZoneTransition.from_xml()
                        occurrence = 5
                    tz_transitions_groups[tg_id].append(dict(
                        to=(int(to_year), to_type),
                        offset=xml_text_to_value(transition.find('{%s}TimeOffset' % TNS).text, datetime.timedelta),
                        iso_month=xml_text_to_value(transition.find('{%s}Month' % TNS).text, int),
                        iso_weekday=WEEKDAY_NAMES.index(transition.find('{%s}DayOfWeek' % TNS).text) + 1,
                        occurrence=occurrence,
                    ))
        return tz_transitions_groups

    @staticmethod
    def _get_transitions(timezonedef):
        tz_transitions = {}
        transitions = timezonedef.find('{%s}Transitions' % TNS)
        if transitions is not None:
            for transition in transitions.findall('{%s}Transition' % TNS):
                to = transition.find('{%s}To' % TNS)
                if to.get('Kind') != 'Group':
                    raise ValueError('Unexpected "Kind" XML attr: %s' % to.get('Kind'))
                tg_id = xml_text_to_value(to.text, int)
                tz_transitions[tg_id] = None
            for transition in transitions.findall('{%s}AbsoluteDateTransition' % TNS):
                to = transition.find('{%s}To' % TNS)
                if to.get('Kind') != 'Group':
                    raise ValueError('Unexpected "Kind" XML attr: %s' % to.get('Kind'))
                tg_id = xml_text_to_value(to.text, int)
                try:
                    t_date = xml_text_to_value(transition.find('{%s}DateTime' % TNS).text, EWSDateTime).date()
                except NaiveDateTimeNotAllowed as e:
                    # We encountered a naive datetime. Don't worry. we just need the date
                    t_date = e.local_dt.date()
                tz_transitions[tg_id] = t_date
        return tz_transitions

Classes

class GetServerTimeZones (protocol, chunk_size=None, timeout=None)
Expand source code
class GetServerTimeZones(EWSService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getservertimezones-operation
    """

    SERVICE_NAME = 'GetServerTimeZones'
    element_container_name = '{%s}TimeZoneDefinitions' % MNS
    supported_from = EXCHANGE_2010

    def call(self, timezones=None, return_full_timezone_data=False):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            timezones=timezones,
            return_full_timezone_data=return_full_timezone_data
        )))

    def get_payload(self, timezones, return_full_timezone_data):
        payload = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=dict(ReturnFullTimeZoneData='true' if return_full_timezone_data else 'false'),
        )
        if timezones is not None:
            is_empty, timezones = peek(timezones)
            if not is_empty:
                tz_ids = create_element('m:Ids')
                for timezone in timezones:
                    tz_id = set_xml_value(create_element('t:Id'), timezone.ms_id, version=self.protocol.version)
                    tz_ids.append(tz_id)
                payload.append(tz_ids)
        return payload

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            tz_id = elem.get('Id')
            tz_name = elem.get('Name')
            tz_periods = self._get_periods(elem)
            tz_transitions_groups = self._get_transitions_groups(elem)
            tz_transitions = self._get_transitions(elem)
            yield tz_id, tz_name, tz_periods, tz_transitions, tz_transitions_groups

    @staticmethod
    def _get_periods(timezonedef):
        tz_periods = {}
        periods = timezonedef.find('{%s}Periods' % TNS)
        for period in periods.findall('{%s}Period' % TNS):
            # Convert e.g. "trule:Microsoft/Registry/W. Europe Standard Time/2006-Daylight" to (2006, 'Daylight')
            p_year, p_type = period.get('Id').rsplit('/', 1)[1].split('-')
            tz_periods[(int(p_year), p_type)] = dict(
                name=period.get('Name'),
                bias=xml_text_to_value(period.get('Bias'), datetime.timedelta)
            )
        return tz_periods

    @staticmethod
    def _get_transitions_groups(timezonedef):
        tz_transitions_groups = {}
        transitiongroups = timezonedef.find('{%s}TransitionsGroups' % TNS)
        if transitiongroups is not None:
            for transitiongroup in transitiongroups.findall('{%s}TransitionsGroup' % TNS):
                tg_id = int(transitiongroup.get('Id'))
                tz_transitions_groups[tg_id] = []
                for transition in transitiongroup.findall('{%s}Transition' % TNS):
                    # Apply same conversion to To as for period IDs
                    to_year, to_type = transition.find('{%s}To' % TNS).text.rsplit('/', 1)[1].split('-')
                    tz_transitions_groups[tg_id].append(dict(
                        to=(int(to_year), to_type),
                    ))
                for transition in transitiongroup.findall('{%s}RecurringDayTransition' % TNS):
                    # Apply same conversion to To as for period IDs
                    to_year, to_type = transition.find('{%s}To' % TNS).text.rsplit('/', 1)[1].split('-')
                    occurrence = xml_text_to_value(transition.find('{%s}Occurrence' % TNS).text, int)
                    if occurrence == -1:
                        # See TimeZoneTransition.from_xml()
                        occurrence = 5
                    tz_transitions_groups[tg_id].append(dict(
                        to=(int(to_year), to_type),
                        offset=xml_text_to_value(transition.find('{%s}TimeOffset' % TNS).text, datetime.timedelta),
                        iso_month=xml_text_to_value(transition.find('{%s}Month' % TNS).text, int),
                        iso_weekday=WEEKDAY_NAMES.index(transition.find('{%s}DayOfWeek' % TNS).text) + 1,
                        occurrence=occurrence,
                    ))
        return tz_transitions_groups

    @staticmethod
    def _get_transitions(timezonedef):
        tz_transitions = {}
        transitions = timezonedef.find('{%s}Transitions' % TNS)
        if transitions is not None:
            for transition in transitions.findall('{%s}Transition' % TNS):
                to = transition.find('{%s}To' % TNS)
                if to.get('Kind') != 'Group':
                    raise ValueError('Unexpected "Kind" XML attr: %s' % to.get('Kind'))
                tg_id = xml_text_to_value(to.text, int)
                tz_transitions[tg_id] = None
            for transition in transitions.findall('{%s}AbsoluteDateTransition' % TNS):
                to = transition.find('{%s}To' % TNS)
                if to.get('Kind') != 'Group':
                    raise ValueError('Unexpected "Kind" XML attr: %s' % to.get('Kind'))
                tg_id = xml_text_to_value(to.text, int)
                try:
                    t_date = xml_text_to_value(transition.find('{%s}DateTime' % TNS).text, EWSDateTime).date()
                except NaiveDateTimeNotAllowed as e:
                    # We encountered a naive datetime. Don't worry. we just need the date
                    t_date = e.local_dt.date()
                tz_transitions[tg_id] = t_date
        return tz_transitions

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var supported_from

Methods

def call(self, timezones=None, return_full_timezone_data=False)
Expand source code
def call(self, timezones=None, return_full_timezone_data=False):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(
        timezones=timezones,
        return_full_timezone_data=return_full_timezone_data
    )))
def get_payload(self, timezones, return_full_timezone_data)
Expand source code
def get_payload(self, timezones, return_full_timezone_data):
    payload = create_element(
        'm:%s' % self.SERVICE_NAME,
        attrs=dict(ReturnFullTimeZoneData='true' if return_full_timezone_data else 'false'),
    )
    if timezones is not None:
        is_empty, timezones = peek(timezones)
        if not is_empty:
            tz_ids = create_element('m:Ids')
            for timezone in timezones:
                tz_id = set_xml_value(create_element('t:Id'), timezone.ms_id, version=self.protocol.version)
                tz_ids.append(tz_id)
            payload.append(tz_ids)
    return payload

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/get_streaming_events.html000066400000000000000000000531171414601472700261360ustar00rootroot00000000000000 exchangelib.services.get_streaming_events API documentation

Module exchangelib.services.get_streaming_events

Expand source code
import logging

from .common import EWSAccountService, add_xml_child
from ..properties import Notification
from ..util import create_element, get_xml_attr, get_xml_attrs, MNS, DocumentYielder, DummyResponse

log = logging.getLogger(__name__)
xml_log = logging.getLogger('%s.xml' % __name__)


class GetStreamingEvents(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getstreamingevents-operation
    """

    SERVICE_NAME = 'GetStreamingEvents'
    element_container_name = '{%s}Notifications' % MNS
    streaming = True
    prefer_affinity = True

    # Connection status values
    OK = 'OK'
    CLOSED = 'Closed'

    def __init__(self, *args, **kwargs):
        # These values are set each time call() is consumed
        self.connection_status = None
        self.error_subscription_ids = []
        super().__init__(*args, **kwargs)

    def call(self, subscription_ids, connection_timeout):
        if connection_timeout < 1:
            raise ValueError("'connection_timeout' must be a positive integer")
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
                subscription_ids=subscription_ids, connection_timeout=connection_timeout,
        )))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Notification.from_xml(elem=elem, account=None)

    @classmethod
    def _get_soap_parts(cls, response, **parse_opts):
        # Pass the response unaltered. We want to use our custom document yielder
        return None, response

    def _get_soap_messages(self, body, **parse_opts):
        # 'body' is actually the raw response passed on by '_get_soap_parts'. We want to continuously read the content,
        # looking for complete XML documents. When we have a full document, we want to parse it as if it was a normal
        # XML response.
        r = body
        for i, doc in enumerate(DocumentYielder(r.iter_content()), start=1):
            xml_log.debug('''Response XML (docs received: %(i)s): %(xml_response)s''', dict(i=i, xml_response=doc))
            response = DummyResponse(url=None, headers=None, request_headers=None, content=doc)
            try:
                _, body = super()._get_soap_parts(response=response, **parse_opts)
            except Exception:
                r.close()  # Release memory
                raise
            # TODO: We're skipping ._update_api_version() here because we don't have access to the 'api_version' used.
            # TODO: We should be doing a lot of error handling for ._get_soap_messages().
            yield from super()._get_soap_messages(body=body, **parse_opts)
            if self.connection_status == self.CLOSED:
                # Don't wait for the TCP connection to timeout
                break

    def _get_element_container(self, message, name=None):
        error_ids_elem = message.find('{%s}ErrorSubscriptionIds' % MNS)
        if error_ids_elem is not None:
            self.error_subscription_ids = get_xml_attrs(error_ids_elem, '{%s}ErrorSubscriptionId' % MNS)
            log.debug('These subscription IDs are invalid: %s', self.error_subscription_ids)
        self.connection_status = get_xml_attr(message, '{%s}ConnectionStatus' % MNS)  # Either 'OK' or 'Closed'
        log.debug('Connection status is: %s', self.connection_status)
        # Upstream expects to find a 'name' tag but our response does not always have it. Return an empty element.
        if message.find(name) is None:
            return []
        return super()._get_element_container(message=message, name=name)

    def get_payload(self, subscription_ids, connection_timeout):
        getstreamingevents = create_element('m:%s' % self.SERVICE_NAME)
        subscriptions_elem = create_element('m:SubscriptionIds')
        for subscription_id in subscription_ids:
            add_xml_child(subscriptions_elem, 't:SubscriptionId', subscription_id)
        if not len(subscriptions_elem):
            raise ValueError('"subscription_ids" must not be empty')

        getstreamingevents.append(subscriptions_elem)
        add_xml_child(getstreamingevents, 'm:ConnectionTimeout', connection_timeout)
        return getstreamingevents

Classes

class GetStreamingEvents (*args, **kwargs)
Expand source code
class GetStreamingEvents(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getstreamingevents-operation
    """

    SERVICE_NAME = 'GetStreamingEvents'
    element_container_name = '{%s}Notifications' % MNS
    streaming = True
    prefer_affinity = True

    # Connection status values
    OK = 'OK'
    CLOSED = 'Closed'

    def __init__(self, *args, **kwargs):
        # These values are set each time call() is consumed
        self.connection_status = None
        self.error_subscription_ids = []
        super().__init__(*args, **kwargs)

    def call(self, subscription_ids, connection_timeout):
        if connection_timeout < 1:
            raise ValueError("'connection_timeout' must be a positive integer")
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
                subscription_ids=subscription_ids, connection_timeout=connection_timeout,
        )))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Notification.from_xml(elem=elem, account=None)

    @classmethod
    def _get_soap_parts(cls, response, **parse_opts):
        # Pass the response unaltered. We want to use our custom document yielder
        return None, response

    def _get_soap_messages(self, body, **parse_opts):
        # 'body' is actually the raw response passed on by '_get_soap_parts'. We want to continuously read the content,
        # looking for complete XML documents. When we have a full document, we want to parse it as if it was a normal
        # XML response.
        r = body
        for i, doc in enumerate(DocumentYielder(r.iter_content()), start=1):
            xml_log.debug('''Response XML (docs received: %(i)s): %(xml_response)s''', dict(i=i, xml_response=doc))
            response = DummyResponse(url=None, headers=None, request_headers=None, content=doc)
            try:
                _, body = super()._get_soap_parts(response=response, **parse_opts)
            except Exception:
                r.close()  # Release memory
                raise
            # TODO: We're skipping ._update_api_version() here because we don't have access to the 'api_version' used.
            # TODO: We should be doing a lot of error handling for ._get_soap_messages().
            yield from super()._get_soap_messages(body=body, **parse_opts)
            if self.connection_status == self.CLOSED:
                # Don't wait for the TCP connection to timeout
                break

    def _get_element_container(self, message, name=None):
        error_ids_elem = message.find('{%s}ErrorSubscriptionIds' % MNS)
        if error_ids_elem is not None:
            self.error_subscription_ids = get_xml_attrs(error_ids_elem, '{%s}ErrorSubscriptionId' % MNS)
            log.debug('These subscription IDs are invalid: %s', self.error_subscription_ids)
        self.connection_status = get_xml_attr(message, '{%s}ConnectionStatus' % MNS)  # Either 'OK' or 'Closed'
        log.debug('Connection status is: %s', self.connection_status)
        # Upstream expects to find a 'name' tag but our response does not always have it. Return an empty element.
        if message.find(name) is None:
            return []
        return super()._get_element_container(message=message, name=name)

    def get_payload(self, subscription_ids, connection_timeout):
        getstreamingevents = create_element('m:%s' % self.SERVICE_NAME)
        subscriptions_elem = create_element('m:SubscriptionIds')
        for subscription_id in subscription_ids:
            add_xml_child(subscriptions_elem, 't:SubscriptionId', subscription_id)
        if not len(subscriptions_elem):
            raise ValueError('"subscription_ids" must not be empty')

        getstreamingevents.append(subscriptions_elem)
        add_xml_child(getstreamingevents, 'm:ConnectionTimeout', connection_timeout)
        return getstreamingevents

Ancestors

Class variables

var CLOSED
var OK
var SERVICE_NAME
var element_container_name
var prefer_affinity
var streaming

Methods

def call(self, subscription_ids, connection_timeout)
Expand source code
def call(self, subscription_ids, connection_timeout):
    if connection_timeout < 1:
        raise ValueError("'connection_timeout' must be a positive integer")
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            subscription_ids=subscription_ids, connection_timeout=connection_timeout,
    )))
def get_payload(self, subscription_ids, connection_timeout)
Expand source code
def get_payload(self, subscription_ids, connection_timeout):
    getstreamingevents = create_element('m:%s' % self.SERVICE_NAME)
    subscriptions_elem = create_element('m:SubscriptionIds')
    for subscription_id in subscription_ids:
        add_xml_child(subscriptions_elem, 't:SubscriptionId', subscription_id)
    if not len(subscriptions_elem):
        raise ValueError('"subscription_ids" must not be empty')

    getstreamingevents.append(subscriptions_elem)
    add_xml_child(getstreamingevents, 'm:ConnectionTimeout', connection_timeout)
    return getstreamingevents

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/get_user_availability.html000066400000000000000000000364441414601472700262750ustar00rootroot00000000000000 exchangelib.services.get_user_availability API documentation

Module exchangelib.services.get_user_availability

Expand source code
from .common import EWSService
from ..properties import FreeBusyView
from ..util import create_element, set_xml_value, MNS


class GetUserAvailability(EWSService):
    """Get detailed availability information for a list of users.
    MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getuseravailability-operation
    """

    SERVICE_NAME = 'GetUserAvailability'

    def call(self, timezone, mailbox_data, free_busy_view_options):
        # TODO: Also supports SuggestionsViewOptions, see
        #  https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/suggestionsviewoptions
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            timezone=timezone,
            mailbox_data=mailbox_data,
            free_busy_view_options=free_busy_view_options
        )))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield FreeBusyView.from_xml(elem=elem, account=None)

    def get_payload(self, timezone, mailbox_data, free_busy_view_options):
        payload = create_element('m:%sRequest' % self.SERVICE_NAME)
        set_xml_value(payload, timezone, version=self.protocol.version)
        mailbox_data_array = create_element('m:MailboxDataArray')
        set_xml_value(mailbox_data_array, mailbox_data, version=self.protocol.version)
        payload.append(mailbox_data_array)
        set_xml_value(payload, free_busy_view_options, version=self.protocol.version)
        return payload

    @staticmethod
    def _response_messages_tag():
        return '{%s}FreeBusyResponseArray' % MNS

    @classmethod
    def _response_message_tag(cls):
        return '{%s}FreeBusyResponse' % MNS

    def _get_elements_in_response(self, response):
        for msg in response:
            # Just check the response code and raise errors
            self._get_element_container(message=msg.find('{%s}ResponseMessage' % MNS))
            yield from self._get_elements_in_container(container=msg)

    @classmethod
    def _get_elements_in_container(cls, container):
        return [container.find('{%s}FreeBusyView' % MNS)]

Classes

class GetUserAvailability (protocol, chunk_size=None, timeout=None)
Expand source code
class GetUserAvailability(EWSService):
    """Get detailed availability information for a list of users.
    MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getuseravailability-operation
    """

    SERVICE_NAME = 'GetUserAvailability'

    def call(self, timezone, mailbox_data, free_busy_view_options):
        # TODO: Also supports SuggestionsViewOptions, see
        #  https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/suggestionsviewoptions
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            timezone=timezone,
            mailbox_data=mailbox_data,
            free_busy_view_options=free_busy_view_options
        )))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield FreeBusyView.from_xml(elem=elem, account=None)

    def get_payload(self, timezone, mailbox_data, free_busy_view_options):
        payload = create_element('m:%sRequest' % self.SERVICE_NAME)
        set_xml_value(payload, timezone, version=self.protocol.version)
        mailbox_data_array = create_element('m:MailboxDataArray')
        set_xml_value(mailbox_data_array, mailbox_data, version=self.protocol.version)
        payload.append(mailbox_data_array)
        set_xml_value(payload, free_busy_view_options, version=self.protocol.version)
        return payload

    @staticmethod
    def _response_messages_tag():
        return '{%s}FreeBusyResponseArray' % MNS

    @classmethod
    def _response_message_tag(cls):
        return '{%s}FreeBusyResponse' % MNS

    def _get_elements_in_response(self, response):
        for msg in response:
            # Just check the response code and raise errors
            self._get_element_container(message=msg.find('{%s}ResponseMessage' % MNS))
            yield from self._get_elements_in_container(container=msg)

    @classmethod
    def _get_elements_in_container(cls, container):
        return [container.find('{%s}FreeBusyView' % MNS)]

Ancestors

Class variables

var SERVICE_NAME

Methods

def call(self, timezone, mailbox_data, free_busy_view_options)
Expand source code
def call(self, timezone, mailbox_data, free_busy_view_options):
    # TODO: Also supports SuggestionsViewOptions, see
    #  https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/suggestionsviewoptions
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(
        timezone=timezone,
        mailbox_data=mailbox_data,
        free_busy_view_options=free_busy_view_options
    )))
def get_payload(self, timezone, mailbox_data, free_busy_view_options)
Expand source code
def get_payload(self, timezone, mailbox_data, free_busy_view_options):
    payload = create_element('m:%sRequest' % self.SERVICE_NAME)
    set_xml_value(payload, timezone, version=self.protocol.version)
    mailbox_data_array = create_element('m:MailboxDataArray')
    set_xml_value(mailbox_data_array, mailbox_data, version=self.protocol.version)
    payload.append(mailbox_data_array)
    set_xml_value(payload, free_busy_view_options, version=self.protocol.version)
    return payload

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/get_user_configuration.html000066400000000000000000000345671414601472700264760ustar00rootroot00000000000000 exchangelib.services.get_user_configuration API documentation

Module exchangelib.services.get_user_configuration

Expand source code
from .common import EWSAccountService
from ..properties import UserConfiguration
from ..util import create_element, set_xml_value

ID = 'Id'
DICTIONARY = 'Dictionary'
XML_DATA = 'XmlData'
BINARY_DATA = 'BinaryData'
ALL = 'All'
PROPERTIES_CHOICES = {ID, DICTIONARY, XML_DATA, BINARY_DATA, ALL}


class GetUserConfiguration(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getuserconfiguration-operation
    """

    SERVICE_NAME = 'GetUserConfiguration'

    def call(self, user_configuration_name, properties):
        if properties not in PROPERTIES_CHOICES:
            raise ValueError("'properties' %r must be one of %s" % (properties, PROPERTIES_CHOICES))
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
                user_configuration_name=user_configuration_name, properties=properties
        )))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield UserConfiguration.from_xml(elem=elem, account=self.account)

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(UserConfiguration.response_tag())

    def get_payload(self, user_configuration_name, properties):
        getuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(getuserconfiguration, user_configuration_name, version=self.account.version)
        user_configuration_properties = create_element('m:UserConfigurationProperties')
        set_xml_value(user_configuration_properties, properties, version=self.account.version)
        getuserconfiguration.append(user_configuration_properties)
        return getuserconfiguration

Classes

class GetUserConfiguration (*args, **kwargs)
Expand source code
class GetUserConfiguration(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getuserconfiguration-operation
    """

    SERVICE_NAME = 'GetUserConfiguration'

    def call(self, user_configuration_name, properties):
        if properties not in PROPERTIES_CHOICES:
            raise ValueError("'properties' %r must be one of %s" % (properties, PROPERTIES_CHOICES))
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
                user_configuration_name=user_configuration_name, properties=properties
        )))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield UserConfiguration.from_xml(elem=elem, account=self.account)

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(UserConfiguration.response_tag())

    def get_payload(self, user_configuration_name, properties):
        getuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(getuserconfiguration, user_configuration_name, version=self.account.version)
        user_configuration_properties = create_element('m:UserConfigurationProperties')
        set_xml_value(user_configuration_properties, properties, version=self.account.version)
        getuserconfiguration.append(user_configuration_properties)
        return getuserconfiguration

Ancestors

Class variables

var SERVICE_NAME

Methods

def call(self, user_configuration_name, properties)
Expand source code
def call(self, user_configuration_name, properties):
    if properties not in PROPERTIES_CHOICES:
        raise ValueError("'properties' %r must be one of %s" % (properties, PROPERTIES_CHOICES))
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            user_configuration_name=user_configuration_name, properties=properties
    )))
def get_payload(self, user_configuration_name, properties)
Expand source code
def get_payload(self, user_configuration_name, properties):
    getuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(getuserconfiguration, user_configuration_name, version=self.account.version)
    user_configuration_properties = create_element('m:UserConfigurationProperties')
    set_xml_value(user_configuration_properties, properties, version=self.account.version)
    getuserconfiguration.append(user_configuration_properties)
    return getuserconfiguration

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/get_user_oof_settings.html000066400000000000000000000337011414601472700263170ustar00rootroot00000000000000 exchangelib.services.get_user_oof_settings API documentation

Module exchangelib.services.get_user_oof_settings

Expand source code
from .common import EWSAccountService
from ..properties import AvailabilityMailbox
from ..settings import OofSettings
from ..util import create_element, set_xml_value, MNS, TNS


class GetUserOofSettings(EWSAccountService):
    """Get automatic reply settings for the specified mailbox.
    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getuseroofsettings-operation
    """

    SERVICE_NAME = 'GetUserOofSettings'
    element_container_name = '{%s}OofSettings' % TNS

    def call(self, mailbox):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(mailbox=mailbox)))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield OofSettings.from_xml(elem=elem, account=self.account)

    def get_payload(self, mailbox):
        payload = create_element('m:%sRequest' % self.SERVICE_NAME)
        return set_xml_value(payload, AvailabilityMailbox.from_mailbox(mailbox), version=self.account.version)

    @classmethod
    def _get_elements_in_container(cls, container):
        # This service only returns one result, directly in 'container'
        return [container]

    def _get_element_container(self, message, name=None):
        # This service returns the result container outside the response message
        super()._get_element_container(message=message.find(self._response_message_tag()), name=None)
        return message.find(name)

    @classmethod
    def _response_message_tag(cls):
        return '{%s}ResponseMessage' % MNS

Classes

class GetUserOofSettings (*args, **kwargs)
Expand source code
class GetUserOofSettings(EWSAccountService):
    """Get automatic reply settings for the specified mailbox.
    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getuseroofsettings-operation
    """

    SERVICE_NAME = 'GetUserOofSettings'
    element_container_name = '{%s}OofSettings' % TNS

    def call(self, mailbox):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(mailbox=mailbox)))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield OofSettings.from_xml(elem=elem, account=self.account)

    def get_payload(self, mailbox):
        payload = create_element('m:%sRequest' % self.SERVICE_NAME)
        return set_xml_value(payload, AvailabilityMailbox.from_mailbox(mailbox), version=self.account.version)

    @classmethod
    def _get_elements_in_container(cls, container):
        # This service only returns one result, directly in 'container'
        return [container]

    def _get_element_container(self, message, name=None):
        # This service returns the result container outside the response message
        super()._get_element_container(message=message.find(self._response_message_tag()), name=None)
        return message.find(name)

    @classmethod
    def _response_message_tag(cls):
        return '{%s}ResponseMessage' % MNS

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, mailbox)
Expand source code
def call(self, mailbox):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(mailbox=mailbox)))
def get_payload(self, mailbox)
Expand source code
def get_payload(self, mailbox):
    payload = create_element('m:%sRequest' % self.SERVICE_NAME)
    return set_xml_value(payload, AvailabilityMailbox.from_mailbox(mailbox), version=self.account.version)

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/index.html000066400000000000000000013607311414601472700230350ustar00rootroot00000000000000 exchangelib.services API documentation

Module exchangelib.services

Implement a selection of EWS services (operations).

Exchange is very picky about things like the order of XML elements in SOAP requests, so we need to generate XML automatically instead of taking advantage of Python SOAP libraries and the WSDL file.

Exchange EWS operations overview: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ews-operations-in-exchange

Expand source code
"""Implement a selection of EWS services (operations).

Exchange is very picky about things like the order of XML elements in SOAP requests, so we need to generate XML
automatically instead of taking advantage of Python SOAP libraries and the WSDL file.

Exchange EWS operations overview:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ews-operations-in-exchange
"""

from .archive_item import ArchiveItem
from .common import CHUNK_SIZE
from .convert_id import ConvertId
from .copy_item import CopyItem
from .create_attachment import CreateAttachment
from .create_folder import CreateFolder
from .create_item import CreateItem
from .create_user_configuration import CreateUserConfiguration
from .delete_attachment import DeleteAttachment
from .delete_folder import DeleteFolder
from .delete_item import DeleteItem
from .delete_user_configuration import DeleteUserConfiguration
from .empty_folder import EmptyFolder
from .expand_dl import ExpandDL
from .export_items import ExportItems
from .find_folder import FindFolder
from .find_item import FindItem
from .find_people import FindPeople
from .get_attachment import GetAttachment
from .get_delegate import GetDelegate
from .get_events import GetEvents
from .get_folder import GetFolder
from .get_item import GetItem
from .get_mail_tips import GetMailTips
from .get_persona import GetPersona
from .get_room_lists import GetRoomLists
from .get_rooms import GetRooms
from .get_searchable_mailboxes import GetSearchableMailboxes
from .get_server_time_zones import GetServerTimeZones
from .get_streaming_events import GetStreamingEvents
from .get_user_availability import GetUserAvailability
from .get_user_configuration import GetUserConfiguration
from .get_user_oof_settings import GetUserOofSettings
from .mark_as_junk import MarkAsJunk
from .move_folder import MoveFolder
from .move_item import MoveItem
from .resolve_names import ResolveNames
from .send_item import SendItem
from .send_notification import SendNotification
from .set_user_oof_settings import SetUserOofSettings
from .subscribe import SubscribeToStreaming, SubscribeToPull, SubscribeToPush
from .sync_folder_hierarchy import SyncFolderHierarchy
from .sync_folder_items import SyncFolderItems
from .unsubscribe import Unsubscribe
from .update_folder import UpdateFolder
from .update_item import UpdateItem
from .update_user_configuration import UpdateUserConfiguration
from .upload_items import UploadItems

__all__ = [
    'CHUNK_SIZE',
    'ArchiveItem',
    'ConvertId',
    'CopyItem',
    'CreateAttachment',
    'CreateFolder',
    'CreateItem',
    'CreateUserConfiguration',
    'DeleteAttachment',
    'DeleteFolder',
    'DeleteUserConfiguration',
    'DeleteItem',
    'EmptyFolder',
    'ExpandDL',
    'ExportItems',
    'FindFolder',
    'FindItem',
    'FindPeople',
    'GetAttachment',
    'GetDelegate',
    'GetEvents',
    'GetFolder',
    'GetItem',
    'GetMailTips',
    'GetPersona',
    'GetRoomLists',
    'GetRooms',
    'GetSearchableMailboxes',
    'GetServerTimeZones',
    'GetStreamingEvents',
    'GetUserAvailability',
    'GetUserConfiguration',
    'GetUserOofSettings',
    'MarkAsJunk',
    'MoveFolder',
    'MoveItem',
    'ResolveNames',
    'SendItem',
    'SendNotification',
    'SetUserOofSettings',
    'SubscribeToPull',
    'SubscribeToPush',
    'SubscribeToStreaming',
    'SyncFolderHierarchy',
    'SyncFolderItems',
    'Unsubscribe',
    'UpdateFolder',
    'UpdateItem',
    'UpdateUserConfiguration',
    'UploadItems',
]

Sub-modules

exchangelib.services.archive_item
exchangelib.services.common
exchangelib.services.convert_id
exchangelib.services.copy_item
exchangelib.services.create_attachment
exchangelib.services.create_folder
exchangelib.services.create_item
exchangelib.services.create_user_configuration
exchangelib.services.delete_attachment
exchangelib.services.delete_folder
exchangelib.services.delete_item
exchangelib.services.delete_user_configuration
exchangelib.services.empty_folder
exchangelib.services.expand_dl
exchangelib.services.export_items
exchangelib.services.find_folder
exchangelib.services.find_item
exchangelib.services.find_people
exchangelib.services.get_attachment
exchangelib.services.get_delegate
exchangelib.services.get_events
exchangelib.services.get_folder
exchangelib.services.get_item
exchangelib.services.get_mail_tips
exchangelib.services.get_persona
exchangelib.services.get_room_lists
exchangelib.services.get_rooms
exchangelib.services.get_searchable_mailboxes
exchangelib.services.get_server_time_zones
exchangelib.services.get_streaming_events
exchangelib.services.get_user_availability
exchangelib.services.get_user_configuration
exchangelib.services.get_user_oof_settings
exchangelib.services.mark_as_junk
exchangelib.services.move_folder
exchangelib.services.move_item
exchangelib.services.resolve_names
exchangelib.services.send_item
exchangelib.services.send_notification
exchangelib.services.set_user_oof_settings
exchangelib.services.subscribe

The 'Subscribe' service has two different modes, pull and push, with different signatures. Implement as two distinct classes.

exchangelib.services.sync_folder_hierarchy
exchangelib.services.sync_folder_items
exchangelib.services.unsubscribe
exchangelib.services.update_folder
exchangelib.services.update_item
exchangelib.services.update_user_configuration
exchangelib.services.upload_items

Classes

class ArchiveItem (*args, **kwargs)
Expand source code
class ArchiveItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/archiveitem-operation"""

    SERVICE_NAME = 'ArchiveItem'
    element_container_name = '{%s}Items' % MNS
    supported_from = EXCHANGE_2013

    def call(self, items, to_folder):
        """Move a list of items to a specific folder in the archive mailbox.

        :param items: a list of (id, changekey) tuples or Item objects
        :param to_folder:

        :return: None
        """
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, to_folder=to_folder))

    def _elems_to_objs(self, elems):
        from ..items import Item
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Item.id_from_xml(elem)

    def get_payload(self, items, to_folder):
        archiveitem = create_element('m:%s' % self.SERVICE_NAME)
        folder_id = create_folder_ids_element(tag='m:ArchiveSourceFolderId', folders=[to_folder],
                                              version=self.account.version)
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        archiveitem.append(folder_id)
        archiveitem.append(item_ids)
        return archiveitem

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var supported_from

Methods

def call(self, items, to_folder)

Move a list of items to a specific folder in the archive mailbox.

:param items: a list of (id, changekey) tuples or Item objects :param to_folder:

:return: None

Expand source code
def call(self, items, to_folder):
    """Move a list of items to a specific folder in the archive mailbox.

    :param items: a list of (id, changekey) tuples or Item objects
    :param to_folder:

    :return: None
    """
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, to_folder=to_folder))
def get_payload(self, items, to_folder)
Expand source code
def get_payload(self, items, to_folder):
    archiveitem = create_element('m:%s' % self.SERVICE_NAME)
    folder_id = create_folder_ids_element(tag='m:ArchiveSourceFolderId', folders=[to_folder],
                                          version=self.account.version)
    item_ids = create_item_ids_element(items=items, version=self.account.version)
    archiveitem.append(folder_id)
    archiveitem.append(item_ids)
    return archiveitem

Inherited members

class ConvertId (protocol, chunk_size=None, timeout=None)

Take a list of IDs to convert. Returns a list of converted IDs or exception instances, in the same order as the input list.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/convertid-operation

Expand source code
class ConvertId(EWSService):
    """Take a list of IDs to convert. Returns a list of converted IDs or exception instances, in the same order as the
    input list.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/convertid-operation
    """

    SERVICE_NAME = 'ConvertId'
    supported_from = EXCHANGE_2007_SP1

    def call(self, items, destination_format):
        if destination_format not in ID_FORMATS:
            raise ValueError("'destination_format' %r must be one of %s" % (destination_format, ID_FORMATS))
        return self._elems_to_objs(
            self._chunked_get_elements(self.get_payload, items=items, destination_format=destination_format)
        )

    def _elems_to_objs(self, elems):
        cls_map = {cls.response_tag(): cls for cls in (
            AlternateId, AlternatePublicFolderId, AlternatePublicFolderItemId
        )}
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield cls_map[elem.tag].from_xml(elem, account=None)

    def get_payload(self, items, destination_format):
        supported_item_classes = AlternateId, AlternatePublicFolderId, AlternatePublicFolderItemId
        convertid = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(DestinationFormat=destination_format))
        item_ids = create_element('m:SourceIds')
        for item in items:
            if not isinstance(item, supported_item_classes):
                raise ValueError("'item' value %r must be an instance of %r" % (item, supported_item_classes))
            set_xml_value(item_ids, item, version=self.protocol.version)
        if not len(item_ids):
            raise ValueError('"items" must not be empty')
        convertid.append(item_ids)
        return convertid

    @classmethod
    def _get_elements_in_container(cls, container):
        # We may have other elements in here, e.g. 'ResponseCode'. Filter away those.
        return container.findall(AlternateId.response_tag()) \
            + container.findall(AlternatePublicFolderId.response_tag()) \
            + container.findall(AlternatePublicFolderItemId.response_tag())

Ancestors

Class variables

var SERVICE_NAME
var supported_from

Methods

def call(self, items, destination_format)
Expand source code
def call(self, items, destination_format):
    if destination_format not in ID_FORMATS:
        raise ValueError("'destination_format' %r must be one of %s" % (destination_format, ID_FORMATS))
    return self._elems_to_objs(
        self._chunked_get_elements(self.get_payload, items=items, destination_format=destination_format)
    )
def get_payload(self, items, destination_format)
Expand source code
def get_payload(self, items, destination_format):
    supported_item_classes = AlternateId, AlternatePublicFolderId, AlternatePublicFolderItemId
    convertid = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(DestinationFormat=destination_format))
    item_ids = create_element('m:SourceIds')
    for item in items:
        if not isinstance(item, supported_item_classes):
            raise ValueError("'item' value %r must be an instance of %r" % (item, supported_item_classes))
        set_xml_value(item_ids, item, version=self.protocol.version)
    if not len(item_ids):
        raise ValueError('"items" must not be empty')
    convertid.append(item_ids)
    return convertid

Inherited members

class CopyItem (*args, **kwargs)
Expand source code
class CopyItem(move_item.MoveItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/copyitem-operation"""

    SERVICE_NAME = 'CopyItem'

Ancestors

Class variables

var SERVICE_NAME

Inherited members

class CreateAttachment (*args, **kwargs)
Expand source code
class CreateAttachment(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createattachment-operation
    """

    SERVICE_NAME = 'CreateAttachment'
    element_container_name = '{%s}Attachments' % MNS

    def call(self, parent_item, items):
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, parent_item=parent_item))

    def _elems_to_objs(self, elems):
        from ..attachments import FileAttachment, ItemAttachment
        cls_map = {cls.response_tag(): cls for cls in (FileAttachment, ItemAttachment)}
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield cls_map[elem.tag].from_xml(elem=elem, account=self.account)

    def get_payload(self, items, parent_item):
        from ..items import BaseItem
        payload = create_element('m:%s' % self.SERVICE_NAME)
        version = self.account.version
        if isinstance(parent_item, BaseItem):
            # to_item_id() would convert this to a normal ItemId, but the service wants a ParentItemId
            parent_item = ParentItemId(parent_item.id, parent_item.changekey)
        set_xml_value(payload, to_item_id(parent_item, ParentItemId, version=version), version=version)
        attachments = create_element('m:Attachments')
        for item in items:
            set_xml_value(attachments, item, version=self.account.version)
        if not len(attachments):
            raise ValueError('"items" must not be empty')
        payload.append(attachments)
        return payload

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, parent_item, items)
Expand source code
def call(self, parent_item, items):
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, parent_item=parent_item))
def get_payload(self, items, parent_item)
Expand source code
def get_payload(self, items, parent_item):
    from ..items import BaseItem
    payload = create_element('m:%s' % self.SERVICE_NAME)
    version = self.account.version
    if isinstance(parent_item, BaseItem):
        # to_item_id() would convert this to a normal ItemId, but the service wants a ParentItemId
        parent_item = ParentItemId(parent_item.id, parent_item.changekey)
    set_xml_value(payload, to_item_id(parent_item, ParentItemId, version=version), version=version)
    attachments = create_element('m:Attachments')
    for item in items:
        set_xml_value(attachments, item, version=self.account.version)
    if not len(attachments):
        raise ValueError('"items" must not be empty')
    payload.append(attachments)
    return payload

Inherited members

class CreateFolder (*args, **kwargs)
Expand source code
class CreateFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createfolder-operation"""

    SERVICE_NAME = 'CreateFolder'
    element_container_name = '{%s}Folders' % MNS

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.folders = []  # A hack to communicate parsing args to _elems_to_objs()

    def call(self, parent_folder, folders):
        # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same
        # class as the folder instance it was requested with.
        self.folders = list(folders)  # Convert to a list, in case 'folders' is a generator. We're iterating twice.
        return self._elems_to_objs(self._chunked_get_elements(
                self.get_payload, items=self.folders, parent_folder=parent_folder,
        ))

    def _elems_to_objs(self, elems):
        for folder, elem in zip(self.folders, elems):
            if isinstance(elem, Exception):
                yield elem
                continue
            yield parse_folder_elem(elem=elem, folder=folder, account=self.account)

    def get_payload(self, folders, parent_folder):
        create_folder = create_element('m:%s' % self.SERVICE_NAME)
        parentfolderid = create_element('m:ParentFolderId')
        set_xml_value(parentfolderid, parent_folder, version=self.account.version)
        set_xml_value(create_folder, parentfolderid, version=self.account.version)
        folder_ids = create_folder_ids_element(tag='m:Folders', folders=folders, version=self.account.version)
        create_folder.append(folder_ids)
        return create_folder

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, parent_folder, folders)
Expand source code
def call(self, parent_folder, folders):
    # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same
    # class as the folder instance it was requested with.
    self.folders = list(folders)  # Convert to a list, in case 'folders' is a generator. We're iterating twice.
    return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload, items=self.folders, parent_folder=parent_folder,
    ))
def get_payload(self, folders, parent_folder)
Expand source code
def get_payload(self, folders, parent_folder):
    create_folder = create_element('m:%s' % self.SERVICE_NAME)
    parentfolderid = create_element('m:ParentFolderId')
    set_xml_value(parentfolderid, parent_folder, version=self.account.version)
    set_xml_value(create_folder, parentfolderid, version=self.account.version)
    folder_ids = create_folder_ids_element(tag='m:Folders', folders=folders, version=self.account.version)
    create_folder.append(folder_ids)
    return create_folder

Inherited members

class CreateItem (*args, **kwargs)

Take a folder and a list of items. Return the result of creation as a list of tuples (success[True|False], errormessage), in the same order as the input list.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation

Expand source code
class CreateItem(EWSAccountService):
    """Take a folder and a list of items. Return the result of creation as a list of tuples (success[True|False],
    errormessage), in the same order as the input list.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation
    """

    SERVICE_NAME = 'CreateItem'
    element_container_name = '{%s}Items' % MNS

    def call(self, items, folder, message_disposition, send_meeting_invitations):
        from ..folders import BaseFolder, FolderId
        from ..items import SAVE_ONLY, SEND_AND_SAVE_COPY, SEND_ONLY, \
            SEND_MEETING_INVITATIONS_CHOICES, MESSAGE_DISPOSITION_CHOICES
        if message_disposition not in MESSAGE_DISPOSITION_CHOICES:
            raise ValueError("'message_disposition' %s must be one of %s" % (
                message_disposition, MESSAGE_DISPOSITION_CHOICES
            ))
        if send_meeting_invitations not in SEND_MEETING_INVITATIONS_CHOICES:
            raise ValueError("'send_meeting_invitations' %s must be one of %s" % (
                send_meeting_invitations, SEND_MEETING_INVITATIONS_CHOICES
            ))
        if folder is not None:
            if not isinstance(folder, (BaseFolder, FolderId)):
                raise ValueError("'folder' %r must be a Folder or FolderId instance" % folder)
            if folder.account != self.account:
                raise ValueError('"Folder must belong to this account')
        if message_disposition == SAVE_ONLY and folder is None:
            raise AttributeError("Folder must be supplied when in save-only mode")
        if message_disposition == SEND_AND_SAVE_COPY and folder is None:
            folder = self.account.sent  # 'Sent' is default EWS behaviour
        if message_disposition == SEND_ONLY and folder is not None:
            raise AttributeError("Folder must be None in send-ony mode")
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=items,
            folder=folder,
            message_disposition=message_disposition,
            send_meeting_invitations=send_meeting_invitations,
        ))

    def _elems_to_objs(self, elems):
        from ..items import BulkCreateResult
        for elem in elems:
            if isinstance(elem, (Exception, type(None))):
                yield elem
                continue
            if isinstance(elem, bool):
                yield elem
                continue
            yield BulkCreateResult.from_xml(elem=elem, account=self.account)

    @classmethod
    def _get_elements_in_container(cls, container):
        res = super()._get_elements_in_container(container)
        return res or [True]

    def get_payload(self, items, folder, message_disposition, send_meeting_invitations):
        """Take a list of Item objects (CalendarItem, Message etc) and return the XML for a CreateItem request.
        convert items to XML Elements.

        MessageDisposition is only applicable to email messages, where it is required.

        SendMeetingInvitations is required for calendar items. It is also applicable to tasks, meeting request
        responses (see
        https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation-meeting-request
        ) and sharing
        invitation accepts (see
        https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-acceptsharinginvitation
        ). The last two are not supported yet.

        :param items:
        :param folder:
        :param message_disposition:
        :param send_meeting_invitations:
        """
        createitem = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=OrderedDict([
                ('MessageDisposition', message_disposition),
                ('SendMeetingInvitations', send_meeting_invitations),
            ])
        )
        if folder:
            saveditemfolderid = create_element('m:SavedItemFolderId')
            set_xml_value(saveditemfolderid, folder, version=self.account.version)
            createitem.append(saveditemfolderid)
        item_elems = create_element('m:Items')
        for item in items:
            if not item.account:
                item.account = self.account
            set_xml_value(item_elems, item, version=self.account.version)
        if not len(item_elems):
            raise ValueError('"items" must not be empty')
        createitem.append(item_elems)
        return createitem

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, items, folder, message_disposition, send_meeting_invitations)
Expand source code
def call(self, items, folder, message_disposition, send_meeting_invitations):
    from ..folders import BaseFolder, FolderId
    from ..items import SAVE_ONLY, SEND_AND_SAVE_COPY, SEND_ONLY, \
        SEND_MEETING_INVITATIONS_CHOICES, MESSAGE_DISPOSITION_CHOICES
    if message_disposition not in MESSAGE_DISPOSITION_CHOICES:
        raise ValueError("'message_disposition' %s must be one of %s" % (
            message_disposition, MESSAGE_DISPOSITION_CHOICES
        ))
    if send_meeting_invitations not in SEND_MEETING_INVITATIONS_CHOICES:
        raise ValueError("'send_meeting_invitations' %s must be one of %s" % (
            send_meeting_invitations, SEND_MEETING_INVITATIONS_CHOICES
        ))
    if folder is not None:
        if not isinstance(folder, (BaseFolder, FolderId)):
            raise ValueError("'folder' %r must be a Folder or FolderId instance" % folder)
        if folder.account != self.account:
            raise ValueError('"Folder must belong to this account')
    if message_disposition == SAVE_ONLY and folder is None:
        raise AttributeError("Folder must be supplied when in save-only mode")
    if message_disposition == SEND_AND_SAVE_COPY and folder is None:
        folder = self.account.sent  # 'Sent' is default EWS behaviour
    if message_disposition == SEND_ONLY and folder is not None:
        raise AttributeError("Folder must be None in send-ony mode")
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload,
        items=items,
        folder=folder,
        message_disposition=message_disposition,
        send_meeting_invitations=send_meeting_invitations,
    ))
def get_payload(self, items, folder, message_disposition, send_meeting_invitations)

Take a list of Item objects (CalendarItem, Message etc) and return the XML for a CreateItem request. convert items to XML Elements.

MessageDisposition is only applicable to email messages, where it is required.

SendMeetingInvitations is required for calendar items. It is also applicable to tasks, meeting request responses (see https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation-meeting-request ) and sharing invitation accepts (see https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-acceptsharinginvitation ). The last two are not supported yet.

:param items: :param folder: :param message_disposition: :param send_meeting_invitations:

Expand source code
def get_payload(self, items, folder, message_disposition, send_meeting_invitations):
    """Take a list of Item objects (CalendarItem, Message etc) and return the XML for a CreateItem request.
    convert items to XML Elements.

    MessageDisposition is only applicable to email messages, where it is required.

    SendMeetingInvitations is required for calendar items. It is also applicable to tasks, meeting request
    responses (see
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation-meeting-request
    ) and sharing
    invitation accepts (see
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-acceptsharinginvitation
    ). The last two are not supported yet.

    :param items:
    :param folder:
    :param message_disposition:
    :param send_meeting_invitations:
    """
    createitem = create_element(
        'm:%s' % self.SERVICE_NAME,
        attrs=OrderedDict([
            ('MessageDisposition', message_disposition),
            ('SendMeetingInvitations', send_meeting_invitations),
        ])
    )
    if folder:
        saveditemfolderid = create_element('m:SavedItemFolderId')
        set_xml_value(saveditemfolderid, folder, version=self.account.version)
        createitem.append(saveditemfolderid)
    item_elems = create_element('m:Items')
    for item in items:
        if not item.account:
            item.account = self.account
        set_xml_value(item_elems, item, version=self.account.version)
    if not len(item_elems):
        raise ValueError('"items" must not be empty')
    createitem.append(item_elems)
    return createitem

Inherited members

class CreateUserConfiguration (*args, **kwargs)
Expand source code
class CreateUserConfiguration(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createuserconfiguration-operation
    """

    SERVICE_NAME = 'CreateUserConfiguration'
    returns_elements = False

    def call(self, user_configuration):
        return self._get_elements(payload=self.get_payload(user_configuration=user_configuration))

    def get_payload(self, user_configuration):
        createuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(createuserconfiguration, user_configuration, version=self.protocol.version)
        return createuserconfiguration

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, user_configuration)
Expand source code
def call(self, user_configuration):
    return self._get_elements(payload=self.get_payload(user_configuration=user_configuration))
def get_payload(self, user_configuration)
Expand source code
def get_payload(self, user_configuration):
    createuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(createuserconfiguration, user_configuration, version=self.protocol.version)
    return createuserconfiguration

Inherited members

class DeleteAttachment (*args, **kwargs)
Expand source code
class DeleteAttachment(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteattachment-operation
    """

    SERVICE_NAME = 'DeleteAttachment'

    def call(self, items):
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield RootItemId.from_xml(elem=elem, account=self.account)

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(RootItemId.response_tag())

    def get_payload(self, items):
        payload = create_element('m:%s' % self.SERVICE_NAME)
        attachment_ids = create_attachment_ids_element(items=items, version=self.account.version)
        payload.append(attachment_ids)
        return payload

Ancestors

Class variables

var SERVICE_NAME

Methods

def call(self, items)
Expand source code
def call(self, items):
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items))
def get_payload(self, items)
Expand source code
def get_payload(self, items):
    payload = create_element('m:%s' % self.SERVICE_NAME)
    attachment_ids = create_attachment_ids_element(items=items, version=self.account.version)
    payload.append(attachment_ids)
    return payload

Inherited members

class DeleteFolder (*args, **kwargs)
Expand source code
class DeleteFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deletefolder-operation"""

    SERVICE_NAME = 'DeleteFolder'
    returns_elements = False

    def call(self, folders, delete_type):
        return self._chunked_get_elements(self.get_payload, items=folders, delete_type=delete_type)

    def get_payload(self, folders, delete_type):
        deletefolder = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(DeleteType=delete_type))
        folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
        deletefolder.append(folder_ids)
        return deletefolder

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, folders, delete_type)
Expand source code
def call(self, folders, delete_type):
    return self._chunked_get_elements(self.get_payload, items=folders, delete_type=delete_type)
def get_payload(self, folders, delete_type)
Expand source code
def get_payload(self, folders, delete_type):
    deletefolder = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(DeleteType=delete_type))
    folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
    deletefolder.append(folder_ids)
    return deletefolder

Inherited members

class DeleteItem (*args, **kwargs)

Take a folder and a list of (id, changekey) tuples. Return result of deletion as a list of tuples (success[True|False], errormessage), in the same order as the input list.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteitem-operation

Expand source code
class DeleteItem(EWSAccountService):
    """Take a folder and a list of (id, changekey) tuples. Return result of deletion as a list of tuples
    (success[True|False], errormessage), in the same order as the input list.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteitem-operation
    """

    SERVICE_NAME = 'DeleteItem'
    returns_elements = False

    def call(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts):
        from ..items import DELETE_TYPE_CHOICES, SEND_MEETING_CANCELLATIONS_CHOICES, AFFECTED_TASK_OCCURRENCES_CHOICES
        if delete_type not in DELETE_TYPE_CHOICES:
            raise ValueError("'delete_type' %s must be one of %s" % (
                delete_type, DELETE_TYPE_CHOICES
            ))
        if send_meeting_cancellations not in SEND_MEETING_CANCELLATIONS_CHOICES:
            raise ValueError("'send_meeting_cancellations' %s must be one of %s" % (
                send_meeting_cancellations, SEND_MEETING_CANCELLATIONS_CHOICES
            ))
        if affected_task_occurrences not in AFFECTED_TASK_OCCURRENCES_CHOICES:
            raise ValueError("'affected_task_occurrences' %s must be one of %s" % (
                affected_task_occurrences, AFFECTED_TASK_OCCURRENCES_CHOICES
            ))
        if suppress_read_receipts not in (True, False):
            raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts)
        return self._chunked_get_elements(
            self.get_payload,
            items=items,
            delete_type=delete_type,
            send_meeting_cancellations=send_meeting_cancellations,
            affected_task_occurrences=affected_task_occurrences,
            suppress_read_receipts=suppress_read_receipts,
        )

    def get_payload(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences,
                    suppress_read_receipts):
        # Takes a list of (id, changekey) tuples or Item objects and returns the XML for a DeleteItem request.
        if self.account.version.build >= EXCHANGE_2013_SP1:
            deleteitem = create_element(
                'm:%s' % self.SERVICE_NAME,
                attrs=OrderedDict([
                    ('DeleteType', delete_type),
                    ('SendMeetingCancellations', send_meeting_cancellations),
                    ('AffectedTaskOccurrences', affected_task_occurrences),
                    ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'),
                ])
            )
        else:
            deleteitem = create_element(
                'm:%s' % self.SERVICE_NAME,
                attrs=OrderedDict([
                    ('DeleteType', delete_type),
                    ('SendMeetingCancellations', send_meeting_cancellations),
                    ('AffectedTaskOccurrences', affected_task_occurrences),
                 ])
            )

        item_ids = create_item_ids_element(items=items, version=self.account.version)
        deleteitem.append(item_ids)
        return deleteitem

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts)
Expand source code
def call(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts):
    from ..items import DELETE_TYPE_CHOICES, SEND_MEETING_CANCELLATIONS_CHOICES, AFFECTED_TASK_OCCURRENCES_CHOICES
    if delete_type not in DELETE_TYPE_CHOICES:
        raise ValueError("'delete_type' %s must be one of %s" % (
            delete_type, DELETE_TYPE_CHOICES
        ))
    if send_meeting_cancellations not in SEND_MEETING_CANCELLATIONS_CHOICES:
        raise ValueError("'send_meeting_cancellations' %s must be one of %s" % (
            send_meeting_cancellations, SEND_MEETING_CANCELLATIONS_CHOICES
        ))
    if affected_task_occurrences not in AFFECTED_TASK_OCCURRENCES_CHOICES:
        raise ValueError("'affected_task_occurrences' %s must be one of %s" % (
            affected_task_occurrences, AFFECTED_TASK_OCCURRENCES_CHOICES
        ))
    if suppress_read_receipts not in (True, False):
        raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts)
    return self._chunked_get_elements(
        self.get_payload,
        items=items,
        delete_type=delete_type,
        send_meeting_cancellations=send_meeting_cancellations,
        affected_task_occurrences=affected_task_occurrences,
        suppress_read_receipts=suppress_read_receipts,
    )
def get_payload(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts)
Expand source code
def get_payload(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences,
                suppress_read_receipts):
    # Takes a list of (id, changekey) tuples or Item objects and returns the XML for a DeleteItem request.
    if self.account.version.build >= EXCHANGE_2013_SP1:
        deleteitem = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=OrderedDict([
                ('DeleteType', delete_type),
                ('SendMeetingCancellations', send_meeting_cancellations),
                ('AffectedTaskOccurrences', affected_task_occurrences),
                ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'),
            ])
        )
    else:
        deleteitem = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=OrderedDict([
                ('DeleteType', delete_type),
                ('SendMeetingCancellations', send_meeting_cancellations),
                ('AffectedTaskOccurrences', affected_task_occurrences),
             ])
        )

    item_ids = create_item_ids_element(items=items, version=self.account.version)
    deleteitem.append(item_ids)
    return deleteitem

Inherited members

class DeleteUserConfiguration (*args, **kwargs)
Expand source code
class DeleteUserConfiguration(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteuserconfiguration-operation
    """

    SERVICE_NAME = 'DeleteUserConfiguration'
    returns_elements = False

    def call(self, user_configuration_name):
        return self._get_elements(payload=self.get_payload(user_configuration_name=user_configuration_name))

    def get_payload(self, user_configuration_name):
        deleteuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(deleteuserconfiguration, user_configuration_name, version=self.account.version)
        return deleteuserconfiguration

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, user_configuration_name)
Expand source code
def call(self, user_configuration_name):
    return self._get_elements(payload=self.get_payload(user_configuration_name=user_configuration_name))
def get_payload(self, user_configuration_name)
Expand source code
def get_payload(self, user_configuration_name):
    deleteuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(deleteuserconfiguration, user_configuration_name, version=self.account.version)
    return deleteuserconfiguration

Inherited members

class EmptyFolder (*args, **kwargs)
Expand source code
class EmptyFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/emptyfolder-operation"""

    SERVICE_NAME = 'EmptyFolder'
    returns_elements = False

    def call(self, folders, delete_type, delete_sub_folders):
        return self._chunked_get_elements(
            self.get_payload, items=folders, delete_type=delete_type, delete_sub_folders=delete_sub_folders
        )

    def get_payload(self, folders, delete_type, delete_sub_folders):
        emptyfolder = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=OrderedDict([
                ('DeleteType', delete_type),
                ('DeleteSubFolders', 'true' if delete_sub_folders else 'false'),
            ])
        )
        folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
        emptyfolder.append(folder_ids)
        return emptyfolder

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, folders, delete_type, delete_sub_folders)
Expand source code
def call(self, folders, delete_type, delete_sub_folders):
    return self._chunked_get_elements(
        self.get_payload, items=folders, delete_type=delete_type, delete_sub_folders=delete_sub_folders
    )
def get_payload(self, folders, delete_type, delete_sub_folders)
Expand source code
def get_payload(self, folders, delete_type, delete_sub_folders):
    emptyfolder = create_element(
        'm:%s' % self.SERVICE_NAME,
        attrs=OrderedDict([
            ('DeleteType', delete_type),
            ('DeleteSubFolders', 'true' if delete_sub_folders else 'false'),
        ])
    )
    folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
    emptyfolder.append(folder_ids)
    return emptyfolder

Inherited members

class ExpandDL (protocol, chunk_size=None, timeout=None)
Expand source code
class ExpandDL(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/expanddl-operation"""

    SERVICE_NAME = 'ExpandDL'
    element_container_name = '{%s}DLExpansion' % MNS
    WARNINGS_TO_IGNORE_IN_RESPONSE = ErrorNameResolutionMultipleResults

    def call(self, distribution_list):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(distribution_list=distribution_list)))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Mailbox.from_xml(elem, account=None)

    def get_payload(self, distribution_list):
        payload = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(payload, distribution_list, version=self.protocol.version)
        return payload

Ancestors

Class variables

var SERVICE_NAME
var WARNINGS_TO_IGNORE_IN_RESPONSE

Global error type within this module.

var element_container_name

Methods

def call(self, distribution_list)
Expand source code
def call(self, distribution_list):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(distribution_list=distribution_list)))
def get_payload(self, distribution_list)
Expand source code
def get_payload(self, distribution_list):
    payload = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(payload, distribution_list, version=self.protocol.version)
    return payload

Inherited members

class ExportItems (*args, **kwargs)
Expand source code
class ExportItems(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/exportitems-operation"""

    ERRORS_TO_CATCH_IN_RESPONSE = ResponseMessageError
    SERVICE_NAME = 'ExportItems'
    element_container_name = '{%s}Data' % MNS

    def call(self, items):
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield elem.text  # All we want is the 64bit string in the 'Data' tag

    def get_payload(self, items):
        exportitems = create_element('m:%s' % self.SERVICE_NAME)
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        exportitems.append(item_ids)
        return exportitems

    # We need to override this since ExportItemsResponseMessage is formatted a
    # little bit differently. .
    @classmethod
    def _get_elements_in_container(cls, container):
        return [container]

Ancestors

Class variables

var ERRORS_TO_CATCH_IN_RESPONSE

Global error type within this module.

var SERVICE_NAME
var element_container_name

Methods

def call(self, items)
Expand source code
def call(self, items):
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items))
def get_payload(self, items)
Expand source code
def get_payload(self, items):
    exportitems = create_element('m:%s' % self.SERVICE_NAME)
    item_ids = create_item_ids_element(items=items, version=self.account.version)
    exportitems.append(item_ids)
    return exportitems

Inherited members

class FindFolder (*args, **kwargs)
Expand source code
class FindFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/findfolder-operation"""

    SERVICE_NAME = 'FindFolder'
    element_container_name = '{%s}Folders' % TNS
    paging_container_name = '{%s}RootFolder' % MNS
    supports_paging = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.root = None  # A hack to communicate parsing args to _elems_to_objs()

    def call(self, folders, additional_fields, restriction, shape, depth, max_items, offset):
        """Find subfolders of a folder.

        :param folders: the folders to act on
        :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects
        :param restriction: Restriction object that defines the filters for the query
        :param shape: The set of attributes to return
        :param depth: How deep in the folder structure to search for folders
        :param max_items: The maximum number of items to return
        :param offset: the offset relative to the first item in the item collection. Usually 0.

        :return: XML elements for the matching folders
        """
        roots = {f.root for f in folders}
        if len(roots) != 1:
            raise ValueError('FindFolder must be called with folders in the same root hierarchy (%r)' % roots)
        self.root = roots.pop()
        return self._elems_to_objs(self._paged_call(
                payload_func=self.get_payload,
                max_items=max_items,
                folders=folders,
                **dict(
                    additional_fields=additional_fields,
                    restriction=restriction,
                    shape=shape,
                    depth=depth,
                    page_size=self.chunk_size,
                    offset=offset,
                )
        ))

    def _elems_to_objs(self, elems):
        from ..folders import Folder
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Folder.from_xml_with_root(elem=elem, root=self.root)

    def get_payload(self, folders, additional_fields, restriction, shape, depth, page_size, offset=0):
        findfolder = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
        foldershape = create_shape_element(
            tag='m:FolderShape', shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        findfolder.append(foldershape)
        if self.account.version.build >= EXCHANGE_2010:
            indexedpageviewitem = create_element(
                'm:IndexedPageFolderView',
                attrs=OrderedDict([
                    ('MaxEntriesReturned', str(page_size)),
                    ('Offset', str(offset)),
                    ('BasePoint', 'Beginning'),
                ])
            )
            findfolder.append(indexedpageviewitem)
        else:
            if offset != 0:
                raise ValueError('Offsets are only supported from Exchange 2010')
        if restriction:
            findfolder.append(restriction.to_xml(version=self.account.version))
        parentfolderids = create_element('m:ParentFolderIds')
        set_xml_value(parentfolderids, folders, version=self.account.version)
        findfolder.append(parentfolderids)
        return findfolder

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var paging_container_name
var supports_paging

Methods

def call(self, folders, additional_fields, restriction, shape, depth, max_items, offset)

Find subfolders of a folder.

:param folders: the folders to act on :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects :param restriction: Restriction object that defines the filters for the query :param shape: The set of attributes to return :param depth: How deep in the folder structure to search for folders :param max_items: The maximum number of items to return :param offset: the offset relative to the first item in the item collection. Usually 0.

:return: XML elements for the matching folders

Expand source code
def call(self, folders, additional_fields, restriction, shape, depth, max_items, offset):
    """Find subfolders of a folder.

    :param folders: the folders to act on
    :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects
    :param restriction: Restriction object that defines the filters for the query
    :param shape: The set of attributes to return
    :param depth: How deep in the folder structure to search for folders
    :param max_items: The maximum number of items to return
    :param offset: the offset relative to the first item in the item collection. Usually 0.

    :return: XML elements for the matching folders
    """
    roots = {f.root for f in folders}
    if len(roots) != 1:
        raise ValueError('FindFolder must be called with folders in the same root hierarchy (%r)' % roots)
    self.root = roots.pop()
    return self._elems_to_objs(self._paged_call(
            payload_func=self.get_payload,
            max_items=max_items,
            folders=folders,
            **dict(
                additional_fields=additional_fields,
                restriction=restriction,
                shape=shape,
                depth=depth,
                page_size=self.chunk_size,
                offset=offset,
            )
    ))
def get_payload(self, folders, additional_fields, restriction, shape, depth, page_size, offset=0)
Expand source code
def get_payload(self, folders, additional_fields, restriction, shape, depth, page_size, offset=0):
    findfolder = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
    foldershape = create_shape_element(
        tag='m:FolderShape', shape=shape, additional_fields=additional_fields, version=self.account.version
    )
    findfolder.append(foldershape)
    if self.account.version.build >= EXCHANGE_2010:
        indexedpageviewitem = create_element(
            'm:IndexedPageFolderView',
            attrs=OrderedDict([
                ('MaxEntriesReturned', str(page_size)),
                ('Offset', str(offset)),
                ('BasePoint', 'Beginning'),
            ])
        )
        findfolder.append(indexedpageviewitem)
    else:
        if offset != 0:
            raise ValueError('Offsets are only supported from Exchange 2010')
    if restriction:
        findfolder.append(restriction.to_xml(version=self.account.version))
    parentfolderids = create_element('m:ParentFolderIds')
    set_xml_value(parentfolderids, folders, version=self.account.version)
    findfolder.append(parentfolderids)
    return findfolder

Inherited members

class FindItem (*args, **kwargs)
Expand source code
class FindItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/finditem-operation"""

    SERVICE_NAME = 'FindItem'
    element_container_name = '{%s}Items' % TNS
    paging_container_name = '{%s}RootFolder' % MNS
    supports_paging = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # A hack to communicate parsing args to _elems_to_objs()
        self.additional_fields = None
        self.shape = None

    def call(self, folders, additional_fields, restriction, order_fields, shape, query_string, depth, calendar_view,
             max_items, offset):
        """Find items in an account.

        :param folders: the folders to act on
        :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects
        :param restriction: a Restriction object for
        :param order_fields: the fields to sort the results by
        :param shape: The set of attributes to return
        :param query_string: a QueryString object
        :param depth: How deep in the folder structure to search for items
        :param calendar_view: If set, returns recurring calendar items unfolded
        :param max_items: the max number of items to return
        :param offset: the offset relative to the first item in the item collection. Usually 0.

        :return: XML elements for the matching items
        """
        self.additional_fields = additional_fields
        self.shape = shape
        return self._elems_to_objs(self._paged_call(
            payload_func=self.get_payload,
            max_items=max_items,
            folders=folders,
            **dict(
                additional_fields=additional_fields,
                restriction=restriction,
                order_fields=order_fields,
                query_string=query_string,
                shape=shape,
                depth=depth,
                calendar_view=calendar_view,
                page_size=self.chunk_size,
                offset=offset,
            )
        ))

    def _elems_to_objs(self, elems):
        from ..folders.base import BaseFolder
        from ..items import Item, ID_ONLY
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            if self.shape == ID_ONLY and self.additional_fields is None:
                yield Item.id_from_xml(elem)
                continue
            yield BaseFolder.item_model_from_tag(elem.tag).from_xml(elem=elem, account=self.account)

    def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth,
                    calendar_view, page_size, offset=0):
        finditem = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
        itemshape = create_shape_element(
            tag='m:ItemShape', shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        finditem.append(itemshape)
        if calendar_view is None:
            view_type = create_element(
                'm:IndexedPageItemView',
                attrs=OrderedDict([
                    ('MaxEntriesReturned', str(page_size)),
                    ('Offset', str(offset)),
                    ('BasePoint', 'Beginning'),
                ])
            )
        else:
            view_type = calendar_view.to_xml(version=self.account.version)
        finditem.append(view_type)
        if restriction:
            finditem.append(restriction.to_xml(version=self.account.version))
        if order_fields:
            finditem.append(set_xml_value(
                create_element('m:SortOrder'),
                order_fields,
                version=self.account.version
            ))
        finditem.append(set_xml_value(
            create_element('m:ParentFolderIds'),
            folders,
            version=self.account.version
        ))
        if query_string:
            finditem.append(query_string.to_xml(version=self.account.version))
        return finditem

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var paging_container_name
var supports_paging

Methods

def call(self, folders, additional_fields, restriction, order_fields, shape, query_string, depth, calendar_view, max_items, offset)

Find items in an account.

:param folders: the folders to act on :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects :param restriction: a Restriction object for :param order_fields: the fields to sort the results by :param shape: The set of attributes to return :param query_string: a QueryString object :param depth: How deep in the folder structure to search for items :param calendar_view: If set, returns recurring calendar items unfolded :param max_items: the max number of items to return :param offset: the offset relative to the first item in the item collection. Usually 0.

:return: XML elements for the matching items

Expand source code
def call(self, folders, additional_fields, restriction, order_fields, shape, query_string, depth, calendar_view,
         max_items, offset):
    """Find items in an account.

    :param folders: the folders to act on
    :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects
    :param restriction: a Restriction object for
    :param order_fields: the fields to sort the results by
    :param shape: The set of attributes to return
    :param query_string: a QueryString object
    :param depth: How deep in the folder structure to search for items
    :param calendar_view: If set, returns recurring calendar items unfolded
    :param max_items: the max number of items to return
    :param offset: the offset relative to the first item in the item collection. Usually 0.

    :return: XML elements for the matching items
    """
    self.additional_fields = additional_fields
    self.shape = shape
    return self._elems_to_objs(self._paged_call(
        payload_func=self.get_payload,
        max_items=max_items,
        folders=folders,
        **dict(
            additional_fields=additional_fields,
            restriction=restriction,
            order_fields=order_fields,
            query_string=query_string,
            shape=shape,
            depth=depth,
            calendar_view=calendar_view,
            page_size=self.chunk_size,
            offset=offset,
        )
    ))
def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, calendar_view, page_size, offset=0)
Expand source code
def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth,
                calendar_view, page_size, offset=0):
    finditem = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
    itemshape = create_shape_element(
        tag='m:ItemShape', shape=shape, additional_fields=additional_fields, version=self.account.version
    )
    finditem.append(itemshape)
    if calendar_view is None:
        view_type = create_element(
            'm:IndexedPageItemView',
            attrs=OrderedDict([
                ('MaxEntriesReturned', str(page_size)),
                ('Offset', str(offset)),
                ('BasePoint', 'Beginning'),
            ])
        )
    else:
        view_type = calendar_view.to_xml(version=self.account.version)
    finditem.append(view_type)
    if restriction:
        finditem.append(restriction.to_xml(version=self.account.version))
    if order_fields:
        finditem.append(set_xml_value(
            create_element('m:SortOrder'),
            order_fields,
            version=self.account.version
        ))
    finditem.append(set_xml_value(
        create_element('m:ParentFolderIds'),
        folders,
        version=self.account.version
    ))
    if query_string:
        finditem.append(query_string.to_xml(version=self.account.version))
    return finditem

Inherited members

class FindPeople (*args, **kwargs)
Expand source code
class FindPeople(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/findpeople-operation"""

    SERVICE_NAME = 'FindPeople'
    element_container_name = '{%s}People' % MNS
    supported_from = EXCHANGE_2013
    supports_paging = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # A hack to communicate parsing args to _elems_to_objs()
        self.additional_fields = None
        self.shape = None

    def call(self, folder, additional_fields, restriction, order_fields, shape, query_string, depth, max_items, offset):
        """Find items in an account.

        :param folder: the Folder object to query
        :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects
        :param restriction: a Restriction object for
        :param order_fields: the fields to sort the results by
        :param shape: The set of attributes to return
        :param query_string: a QueryString object
        :param depth: How deep in the folder structure to search for items
        :param max_items: the max number of items to return
        :param offset: the offset relative to the first item in the item collection. Usually 0.

        :return: XML elements for the matching items
        """
        self.additional_fields = additional_fields
        self.shape = shape
        return self._elems_to_objs(self._paged_call(
            payload_func=self.get_payload,
            max_items=max_items,
            folders=[folder],  # We can only query one folder, so there will only be one element in response
            **dict(
                additional_fields=additional_fields,
                restriction=restriction,
                order_fields=order_fields,
                query_string=query_string,
                shape=shape,
                depth=depth,
                page_size=self.chunk_size,
                offset=offset,
            )
        ))

    def _elems_to_objs(self, elems):
        from ..items import Persona, ID_ONLY
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            if self.shape == ID_ONLY and self.additional_fields is None:
                yield Persona.id_from_xml(elem)
                continue
            yield Persona.from_xml(elem, account=self.account)

    def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, page_size,
                    offset=0):
        folders = list(folders)
        if len(folders) != 1:
            raise ValueError('%r can only query one folder' % self.SERVICE_NAME)
        folder = folders[0]
        findpeople = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
        personashape = create_shape_element(
            tag='m:PersonaShape', shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        findpeople.append(personashape)
        view_type = create_element(
            'm:IndexedPageItemView',
            attrs=OrderedDict([
                ('MaxEntriesReturned', str(page_size)),
                ('Offset', str(offset)),
                ('BasePoint', 'Beginning'),
            ])
        )
        findpeople.append(view_type)
        if restriction:
            findpeople.append(restriction.to_xml(version=self.account.version))
        if order_fields:
            findpeople.append(set_xml_value(
                create_element('m:SortOrder'),
                order_fields,
                version=self.account.version
            ))
        findpeople.append(set_xml_value(
            create_element('m:ParentFolderId'),
            folder,
            version=self.account.version
        ))
        if query_string:
            findpeople.append(query_string.to_xml(version=self.account.version))
        return findpeople

    @staticmethod
    def _get_paging_values(elem):
        """Find paging values. The paging element from FindPeople is different from other paging containers."""
        item_count = int(elem.find('{%s}TotalNumberOfPeopleInView' % MNS).text)
        first_matching = int(elem.find('{%s}FirstMatchingRowIndex' % MNS).text)
        first_loaded = int(elem.find('{%s}FirstLoadedRowIndex' % MNS).text)
        log.debug('Got page with total items %s, first matching %s, first loaded %s ', item_count, first_matching,
                  first_loaded)
        next_offset = None  # GetPersona does not support fetching more pages
        return item_count, next_offset

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var supported_from
var supports_paging

Methods

def call(self, folder, additional_fields, restriction, order_fields, shape, query_string, depth, max_items, offset)

Find items in an account.

:param folder: the Folder object to query :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects :param restriction: a Restriction object for :param order_fields: the fields to sort the results by :param shape: The set of attributes to return :param query_string: a QueryString object :param depth: How deep in the folder structure to search for items :param max_items: the max number of items to return :param offset: the offset relative to the first item in the item collection. Usually 0.

:return: XML elements for the matching items

Expand source code
def call(self, folder, additional_fields, restriction, order_fields, shape, query_string, depth, max_items, offset):
    """Find items in an account.

    :param folder: the Folder object to query
    :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects
    :param restriction: a Restriction object for
    :param order_fields: the fields to sort the results by
    :param shape: The set of attributes to return
    :param query_string: a QueryString object
    :param depth: How deep in the folder structure to search for items
    :param max_items: the max number of items to return
    :param offset: the offset relative to the first item in the item collection. Usually 0.

    :return: XML elements for the matching items
    """
    self.additional_fields = additional_fields
    self.shape = shape
    return self._elems_to_objs(self._paged_call(
        payload_func=self.get_payload,
        max_items=max_items,
        folders=[folder],  # We can only query one folder, so there will only be one element in response
        **dict(
            additional_fields=additional_fields,
            restriction=restriction,
            order_fields=order_fields,
            query_string=query_string,
            shape=shape,
            depth=depth,
            page_size=self.chunk_size,
            offset=offset,
        )
    ))
def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, page_size, offset=0)
Expand source code
def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, page_size,
                offset=0):
    folders = list(folders)
    if len(folders) != 1:
        raise ValueError('%r can only query one folder' % self.SERVICE_NAME)
    folder = folders[0]
    findpeople = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth))
    personashape = create_shape_element(
        tag='m:PersonaShape', shape=shape, additional_fields=additional_fields, version=self.account.version
    )
    findpeople.append(personashape)
    view_type = create_element(
        'm:IndexedPageItemView',
        attrs=OrderedDict([
            ('MaxEntriesReturned', str(page_size)),
            ('Offset', str(offset)),
            ('BasePoint', 'Beginning'),
        ])
    )
    findpeople.append(view_type)
    if restriction:
        findpeople.append(restriction.to_xml(version=self.account.version))
    if order_fields:
        findpeople.append(set_xml_value(
            create_element('m:SortOrder'),
            order_fields,
            version=self.account.version
        ))
    findpeople.append(set_xml_value(
        create_element('m:ParentFolderId'),
        folder,
        version=self.account.version
    ))
    if query_string:
        findpeople.append(query_string.to_xml(version=self.account.version))
    return findpeople

Inherited members

class GetAttachment (*args, **kwargs)
Expand source code
class GetAttachment(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getattachment-operation"""

    SERVICE_NAME = 'GetAttachment'
    element_container_name = '{%s}Attachments' % MNS

    def call(self, items, include_mime_content, body_type, filter_html_content, additional_fields):
        if body_type and body_type not in BODY_TYPE_CHOICES:
            raise ValueError("'body_type' %s must be one of %s" % (body_type, BODY_TYPE_CHOICES))
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload, items=items, include_mime_content=include_mime_content,
            body_type=body_type, filter_html_content=filter_html_content, additional_fields=additional_fields,
        ))

    def _elems_to_objs(self, elems):
        from ..attachments import FileAttachment, ItemAttachment
        cls_map = {cls.response_tag(): cls for cls in (FileAttachment, ItemAttachment)}
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield cls_map[elem.tag].from_xml(elem=elem, account=self.account)

    def get_payload(self, items, include_mime_content, body_type, filter_html_content, additional_fields):
        payload = create_element('m:%s' % self.SERVICE_NAME)
        shape_elem = create_element('m:AttachmentShape')
        if include_mime_content:
            add_xml_child(shape_elem, 't:IncludeMimeContent', 'true')
        if body_type:
            add_xml_child(shape_elem, 't:BodyType', body_type)
        if filter_html_content is not None:
            add_xml_child(shape_elem, 't:FilterHtmlContent', 'true' if filter_html_content else 'false')
        if additional_fields:
            additional_properties = create_element('t:AdditionalProperties')
            expanded_fields = chain(*(f.expand(version=self.account.version) for f in additional_fields))
            set_xml_value(additional_properties, sorted(
                expanded_fields,
                key=lambda f: (getattr(f.field, 'field_uri', ''), f.path)
            ), version=self.account.version)
            shape_elem.append(additional_properties)
        if len(shape_elem):
            payload.append(shape_elem)
        attachment_ids = create_attachment_ids_element(items=items, version=self.account.version)
        payload.append(attachment_ids)
        return payload

    def _update_api_version(self, api_version, header, **parse_opts):
        if not parse_opts.get('stream_file_content', False):
            super()._update_api_version(api_version, header, **parse_opts)
        # TODO: We're skipping this part in streaming mode because StreamingBase64Parser cannot parse the SOAP header

    @classmethod
    def _get_soap_parts(cls, response, **parse_opts):
        if not parse_opts.get('stream_file_content', False):
            return super()._get_soap_parts(response, **parse_opts)

        # Pass the response unaltered. We want to use our custom streaming parser
        return None, response

    def _get_soap_messages(self, body, **parse_opts):
        if not parse_opts.get('stream_file_content', False):
            return super()._get_soap_messages(body, **parse_opts)

        from ..attachments import FileAttachment
        # 'body' is actually the raw response passed on by '_get_soap_parts'
        r = body
        parser = StreamingBase64Parser()
        field = FileAttachment.get_field_by_fieldname('_content')
        handler = StreamingContentHandler(parser=parser, ns=field.namespace, element_name=field.field_uri)
        parser.setContentHandler(handler)
        return parser.parse(r)

    def stream_file_content(self, attachment_id):
        # The streaming XML parser can only stream content of one attachment
        payload = self.get_payload(
            items=[attachment_id], include_mime_content=False, body_type=None, filter_html_content=None,
            additional_fields=None,
        )
        self.streaming = True
        try:
            yield from self._get_response_xml(payload=payload, stream_file_content=True)
        except ElementNotFound as enf:
            # When the returned XML does not contain a Content element, ElementNotFound is thrown by parser.parse().
            # Let the non-streaming SOAP parser parse the response and hook into the normal exception handling.
            # Wrap in DummyResponse because _get_soap_parts() expects an iter_content() method.
            response = DummyResponse(url=None, headers=None, request_headers=None, content=enf.data)
            _, body = super()._get_soap_parts(response=response)
            res = super()._get_soap_messages(body=body)
            for e in self._get_elements_in_response(response=res):
                if isinstance(e, Exception):
                    raise e
            # The returned content did not contain any EWS exceptions. Give up and re-raise the original exception.
            raise enf
        finally:
            self.streaming = False
            self.stop_streaming()

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, items, include_mime_content, body_type, filter_html_content, additional_fields)
Expand source code
def call(self, items, include_mime_content, body_type, filter_html_content, additional_fields):
    if body_type and body_type not in BODY_TYPE_CHOICES:
        raise ValueError("'body_type' %s must be one of %s" % (body_type, BODY_TYPE_CHOICES))
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload, items=items, include_mime_content=include_mime_content,
        body_type=body_type, filter_html_content=filter_html_content, additional_fields=additional_fields,
    ))
def get_payload(self, items, include_mime_content, body_type, filter_html_content, additional_fields)
Expand source code
def get_payload(self, items, include_mime_content, body_type, filter_html_content, additional_fields):
    payload = create_element('m:%s' % self.SERVICE_NAME)
    shape_elem = create_element('m:AttachmentShape')
    if include_mime_content:
        add_xml_child(shape_elem, 't:IncludeMimeContent', 'true')
    if body_type:
        add_xml_child(shape_elem, 't:BodyType', body_type)
    if filter_html_content is not None:
        add_xml_child(shape_elem, 't:FilterHtmlContent', 'true' if filter_html_content else 'false')
    if additional_fields:
        additional_properties = create_element('t:AdditionalProperties')
        expanded_fields = chain(*(f.expand(version=self.account.version) for f in additional_fields))
        set_xml_value(additional_properties, sorted(
            expanded_fields,
            key=lambda f: (getattr(f.field, 'field_uri', ''), f.path)
        ), version=self.account.version)
        shape_elem.append(additional_properties)
    if len(shape_elem):
        payload.append(shape_elem)
    attachment_ids = create_attachment_ids_element(items=items, version=self.account.version)
    payload.append(attachment_ids)
    return payload
def stream_file_content(self, attachment_id)
Expand source code
def stream_file_content(self, attachment_id):
    # The streaming XML parser can only stream content of one attachment
    payload = self.get_payload(
        items=[attachment_id], include_mime_content=False, body_type=None, filter_html_content=None,
        additional_fields=None,
    )
    self.streaming = True
    try:
        yield from self._get_response_xml(payload=payload, stream_file_content=True)
    except ElementNotFound as enf:
        # When the returned XML does not contain a Content element, ElementNotFound is thrown by parser.parse().
        # Let the non-streaming SOAP parser parse the response and hook into the normal exception handling.
        # Wrap in DummyResponse because _get_soap_parts() expects an iter_content() method.
        response = DummyResponse(url=None, headers=None, request_headers=None, content=enf.data)
        _, body = super()._get_soap_parts(response=response)
        res = super()._get_soap_messages(body=body)
        for e in self._get_elements_in_response(response=res):
            if isinstance(e, Exception):
                raise e
        # The returned content did not contain any EWS exceptions. Give up and re-raise the original exception.
        raise enf
    finally:
        self.streaming = False
        self.stop_streaming()

Inherited members

class GetDelegate (*args, **kwargs)
Expand source code
class GetDelegate(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getdelegate-operation"""

    SERVICE_NAME = 'GetDelegate'
    supported_from = EXCHANGE_2007_SP1

    def call(self, user_ids, include_permissions):
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=user_ids or [None],
            mailbox=DLMailbox(email_address=self.account.primary_smtp_address),
            include_permissions=include_permissions,
        ))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield DelegateUser.from_xml(elem=elem, account=self.account)

    def get_payload(self, user_ids, mailbox, include_permissions):
        payload = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=dict(IncludePermissions='true' if include_permissions else 'false'),
        )
        set_xml_value(payload, mailbox, version=self.protocol.version)
        if user_ids != [None]:
            set_xml_value(payload, user_ids, version=self.protocol.version)
        return payload

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(DelegateUser.response_tag())

    @classmethod
    def _response_message_tag(cls):
        return '{%s}DelegateUserResponseMessageType' % MNS

Ancestors

Class variables

var SERVICE_NAME
var supported_from

Methods

def call(self, user_ids, include_permissions)
Expand source code
def call(self, user_ids, include_permissions):
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload,
        items=user_ids or [None],
        mailbox=DLMailbox(email_address=self.account.primary_smtp_address),
        include_permissions=include_permissions,
    ))
def get_payload(self, user_ids, mailbox, include_permissions)
Expand source code
def get_payload(self, user_ids, mailbox, include_permissions):
    payload = create_element(
        'm:%s' % self.SERVICE_NAME,
        attrs=dict(IncludePermissions='true' if include_permissions else 'false'),
    )
    set_xml_value(payload, mailbox, version=self.protocol.version)
    if user_ids != [None]:
        set_xml_value(payload, user_ids, version=self.protocol.version)
    return payload

Inherited members

class GetEvents (*args, **kwargs)
Expand source code
class GetEvents(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getevents-operation
    """

    SERVICE_NAME = 'GetEvents'
    prefer_affinity = True

    def call(self, subscription_id, watermark):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
                subscription_id=subscription_id, watermark=watermark,
        )))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Notification.from_xml(elem=elem, account=None)

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(Notification.response_tag())

    def get_payload(self, subscription_id, watermark):
        getstreamingevents = create_element('m:%s' % self.SERVICE_NAME)
        add_xml_child(getstreamingevents, 'm:SubscriptionId', subscription_id)
        add_xml_child(getstreamingevents, 'm:Watermark', watermark)
        return getstreamingevents

Ancestors

Class variables

var SERVICE_NAME
var prefer_affinity

Methods

def call(self, subscription_id, watermark)
Expand source code
def call(self, subscription_id, watermark):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            subscription_id=subscription_id, watermark=watermark,
    )))
def get_payload(self, subscription_id, watermark)
Expand source code
def get_payload(self, subscription_id, watermark):
    getstreamingevents = create_element('m:%s' % self.SERVICE_NAME)
    add_xml_child(getstreamingevents, 'm:SubscriptionId', subscription_id)
    add_xml_child(getstreamingevents, 'm:Watermark', watermark)
    return getstreamingevents

Inherited members

class GetFolder (*args, **kwargs)
Expand source code
class GetFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getfolder-operation"""

    SERVICE_NAME = 'GetFolder'
    element_container_name = '{%s}Folders' % MNS
    ERRORS_TO_CATCH_IN_RESPONSE = EWSAccountService.ERRORS_TO_CATCH_IN_RESPONSE + (
        ErrorFolderNotFound, ErrorNoPublicFolderReplicaAvailable, ErrorInvalidOperation,
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.folders = []  # A hack to communicate parsing args to _elems_to_objs()

    def call(self, folders, additional_fields, shape):
        """Take a folder ID and returns the full information for that folder.

        :param folders: a list of Folder objects
        :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects
        :param shape: The set of attributes to return

        :return: XML elements for the folders, in stable order
        """
        # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same
        # class as the folder instance it was requested with.
        self.folders = list(folders)  # Convert to a list, in case 'folders' is a generator. We're iterating twice.
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=self.folders,
            additional_fields=additional_fields,
            shape=shape,
        ))

    def _elems_to_objs(self, elems):
        for folder, elem in zip(self.folders, elems):
            if isinstance(elem, Exception):
                yield elem
                continue
            yield parse_folder_elem(elem=elem, folder=folder, account=self.account)

    def get_payload(self, folders, additional_fields, shape):
        getfolder = create_element('m:%s' % self.SERVICE_NAME)
        foldershape = create_shape_element(
            tag='m:FolderShape', shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        getfolder.append(foldershape)
        folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
        getfolder.append(folder_ids)
        return getfolder

Ancestors

Class variables

var ERRORS_TO_CATCH_IN_RESPONSE
var SERVICE_NAME
var element_container_name

Methods

def call(self, folders, additional_fields, shape)

Take a folder ID and returns the full information for that folder.

:param folders: a list of Folder objects :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects :param shape: The set of attributes to return

:return: XML elements for the folders, in stable order

Expand source code
def call(self, folders, additional_fields, shape):
    """Take a folder ID and returns the full information for that folder.

    :param folders: a list of Folder objects
    :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects
    :param shape: The set of attributes to return

    :return: XML elements for the folders, in stable order
    """
    # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same
    # class as the folder instance it was requested with.
    self.folders = list(folders)  # Convert to a list, in case 'folders' is a generator. We're iterating twice.
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload,
        items=self.folders,
        additional_fields=additional_fields,
        shape=shape,
    ))
def get_payload(self, folders, additional_fields, shape)
Expand source code
def get_payload(self, folders, additional_fields, shape):
    getfolder = create_element('m:%s' % self.SERVICE_NAME)
    foldershape = create_shape_element(
        tag='m:FolderShape', shape=shape, additional_fields=additional_fields, version=self.account.version
    )
    getfolder.append(foldershape)
    folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
    getfolder.append(folder_ids)
    return getfolder

Inherited members

class GetItem (*args, **kwargs)
Expand source code
class GetItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getitem-operation"""

    SERVICE_NAME = 'GetItem'
    element_container_name = '{%s}Items' % MNS

    def call(self, items, additional_fields, shape):
        """Return all items in an account that correspond to a list of ID's, in stable order.

        :param items: a list of (id, changekey) tuples or Item objects
        :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects
        :param shape: The shape of returned objects

        :return: XML elements for the items, in stable order
        """
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload, items=items, additional_fields=additional_fields, shape=shape,
        ))

    def _elems_to_objs(self, elems):
        from ..folders.base import BaseFolder
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield BaseFolder.item_model_from_tag(elem.tag).from_xml(elem=elem, account=self.account)

    def get_payload(self, items, additional_fields, shape):
        getitem = create_element('m:%s' % self.SERVICE_NAME)
        itemshape = create_shape_element(
            tag='m:ItemShape', shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        getitem.append(itemshape)
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        getitem.append(item_ids)
        return getitem

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, items, additional_fields, shape)

Return all items in an account that correspond to a list of ID's, in stable order.

:param items: a list of (id, changekey) tuples or Item objects :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects :param shape: The shape of returned objects

:return: XML elements for the items, in stable order

Expand source code
def call(self, items, additional_fields, shape):
    """Return all items in an account that correspond to a list of ID's, in stable order.

    :param items: a list of (id, changekey) tuples or Item objects
    :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects
    :param shape: The shape of returned objects

    :return: XML elements for the items, in stable order
    """
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload, items=items, additional_fields=additional_fields, shape=shape,
    ))
def get_payload(self, items, additional_fields, shape)
Expand source code
def get_payload(self, items, additional_fields, shape):
    getitem = create_element('m:%s' % self.SERVICE_NAME)
    itemshape = create_shape_element(
        tag='m:ItemShape', shape=shape, additional_fields=additional_fields, version=self.account.version
    )
    getitem.append(itemshape)
    item_ids = create_item_ids_element(items=items, version=self.account.version)
    getitem.append(item_ids)
    return getitem

Inherited members

class GetMailTips (protocol, chunk_size=None, timeout=None)
Expand source code
class GetMailTips(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getmailtips-operation"""

    SERVICE_NAME = 'GetMailTips'

    def call(self, sending_as, recipients, mail_tips_requested):
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=recipients,
            sending_as=sending_as,
            mail_tips_requested=mail_tips_requested,
        ))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield MailTips.from_xml(elem=elem, account=None)

    def get_payload(self, recipients, sending_as,  mail_tips_requested):
        payload = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(payload, sending_as, version=self.protocol.version)

        recipients_elem = create_element('m:Recipients')
        for recipient in recipients:
            set_xml_value(recipients_elem, recipient, version=self.protocol.version)
        if not len(recipients_elem):
            raise ValueError('"recipients" must not be empty')
        payload.append(recipients_elem)

        if mail_tips_requested:
            set_xml_value(payload, mail_tips_requested, version=self.protocol.version)
        return payload

    def _get_elements_in_response(self, response):
        for msg in response:
            yield self._get_element_container(message=msg, name=MailTips.response_tag())

    @classmethod
    def _response_message_tag(cls):
        return '{%s}MailTipsResponseMessageType' % MNS

Ancestors

Class variables

var SERVICE_NAME

Methods

def call(self, sending_as, recipients, mail_tips_requested)
Expand source code
def call(self, sending_as, recipients, mail_tips_requested):
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload,
        items=recipients,
        sending_as=sending_as,
        mail_tips_requested=mail_tips_requested,
    ))
def get_payload(self, recipients, sending_as, mail_tips_requested)
Expand source code
def get_payload(self, recipients, sending_as,  mail_tips_requested):
    payload = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(payload, sending_as, version=self.protocol.version)

    recipients_elem = create_element('m:Recipients')
    for recipient in recipients:
        set_xml_value(recipients_elem, recipient, version=self.protocol.version)
    if not len(recipients_elem):
        raise ValueError('"recipients" must not be empty')
    payload.append(recipients_elem)

    if mail_tips_requested:
        set_xml_value(payload, mail_tips_requested, version=self.protocol.version)
    return payload

Inherited members

class GetPersona (*args, **kwargs)
Expand source code
class GetPersona(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getpersona-operation"""

    SERVICE_NAME = 'GetPersona'

    def call(self, persona):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(persona=persona)))

    def _elems_to_objs(self, elems):
        from ..items import Persona
        elements = list(elems)
        if len(elements) != 1:
            raise ValueError('Expected exactly one element in response')
        elem = elements[0]
        if isinstance(elem, Exception):
            raise elem
        return Persona.from_xml(elem=elem, account=None)

    def get_payload(self, persona):
        version = self.protocol.version
        payload = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(payload, to_item_id(persona, PersonaId, version=version), version=version)
        return payload

    @classmethod
    def _get_elements_in_container(cls, container):
        from ..items import Persona
        return container.findall('{%s}%s' % (MNS, Persona.ELEMENT_NAME))

    @classmethod
    def _response_tag(cls):
        return '{%s}%sResponseMessage' % (MNS, cls.SERVICE_NAME)

Ancestors

Class variables

var SERVICE_NAME

Methods

def call(self, persona)
Expand source code
def call(self, persona):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(persona=persona)))
def get_payload(self, persona)
Expand source code
def get_payload(self, persona):
    version = self.protocol.version
    payload = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(payload, to_item_id(persona, PersonaId, version=version), version=version)
    return payload

Inherited members

class GetRoomLists (protocol, chunk_size=None, timeout=None)
Expand source code
class GetRoomLists(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getroomlists-operation"""

    SERVICE_NAME = 'GetRoomLists'
    element_container_name = '{%s}RoomLists' % MNS
    supported_from = EXCHANGE_2010

    def call(self):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload()))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield RoomList.from_xml(elem=elem, account=None)

    def get_payload(self):
        return create_element('m:%s' % self.SERVICE_NAME)

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var supported_from

Methods

def call(self)
Expand source code
def call(self):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload()))
def get_payload(self)
Expand source code
def get_payload(self):
    return create_element('m:%s' % self.SERVICE_NAME)

Inherited members

class GetRooms (protocol, chunk_size=None, timeout=None)
Expand source code
class GetRooms(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getrooms-operation"""

    SERVICE_NAME = 'GetRooms'
    element_container_name = '{%s}Rooms' % MNS
    supported_from = EXCHANGE_2010

    def call(self, roomlist):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(roomlist=roomlist)))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Room.from_xml(elem=elem, account=None)

    def get_payload(self, roomlist):
        getrooms = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(getrooms, roomlist, version=self.protocol.version)
        return getrooms

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var supported_from

Methods

def call(self, roomlist)
Expand source code
def call(self, roomlist):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(roomlist=roomlist)))
def get_payload(self, roomlist)
Expand source code
def get_payload(self, roomlist):
    getrooms = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(getrooms, roomlist, version=self.protocol.version)
    return getrooms

Inherited members

class GetSearchableMailboxes (protocol, chunk_size=None, timeout=None)
Expand source code
class GetSearchableMailboxes(EWSService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getsearchablemailboxes-operation
    """

    SERVICE_NAME = 'GetSearchableMailboxes'
    element_container_name = '{%s}SearchableMailboxes' % MNS
    failed_mailboxes_container_name = '{%s}FailedMailboxes' % MNS
    supported_from = EXCHANGE_2013

    def call(self, search_filter, expand_group_membership):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
                search_filter=search_filter,
                expand_group_membership=expand_group_membership,
        )))

    def _elems_to_objs(self, elems):
        cls_map = {cls.response_tag(): cls for cls in (SearchableMailbox, FailedMailbox)}
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield cls_map[elem.tag].from_xml(elem=elem, account=None)

    def get_payload(self, search_filter, expand_group_membership):
        payload = create_element('m:%s' % self.SERVICE_NAME)
        if search_filter:
            add_xml_child(payload, 'm:SearchFilter', search_filter)
        if expand_group_membership is not None:
            add_xml_child(payload, 'm:ExpandGroupMembership', 'true' if expand_group_membership else 'false')
        return payload

    def _get_elements_in_response(self, response):
        for msg in response:
            for container_name in (self.element_container_name, self.failed_mailboxes_container_name):
                try:
                    container_or_exc = self._get_element_container(message=msg, name=container_name)
                except MalformedResponseError:
                    # Responses may contain no failed mailboxes. _get_element_container() does not accept this.
                    if container_name == self.failed_mailboxes_container_name:
                        continue
                    raise
                if isinstance(container_or_exc, (bool, Exception)):
                    yield container_or_exc
                    continue
                yield from self._get_elements_in_container(container=container_or_exc)

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var failed_mailboxes_container_name
var supported_from

Methods

def call(self, search_filter, expand_group_membership)
Expand source code
def call(self, search_filter, expand_group_membership):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            search_filter=search_filter,
            expand_group_membership=expand_group_membership,
    )))
def get_payload(self, search_filter, expand_group_membership)
Expand source code
def get_payload(self, search_filter, expand_group_membership):
    payload = create_element('m:%s' % self.SERVICE_NAME)
    if search_filter:
        add_xml_child(payload, 'm:SearchFilter', search_filter)
    if expand_group_membership is not None:
        add_xml_child(payload, 'm:ExpandGroupMembership', 'true' if expand_group_membership else 'false')
    return payload

Inherited members

class GetServerTimeZones (protocol, chunk_size=None, timeout=None)
Expand source code
class GetServerTimeZones(EWSService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getservertimezones-operation
    """

    SERVICE_NAME = 'GetServerTimeZones'
    element_container_name = '{%s}TimeZoneDefinitions' % MNS
    supported_from = EXCHANGE_2010

    def call(self, timezones=None, return_full_timezone_data=False):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            timezones=timezones,
            return_full_timezone_data=return_full_timezone_data
        )))

    def get_payload(self, timezones, return_full_timezone_data):
        payload = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=dict(ReturnFullTimeZoneData='true' if return_full_timezone_data else 'false'),
        )
        if timezones is not None:
            is_empty, timezones = peek(timezones)
            if not is_empty:
                tz_ids = create_element('m:Ids')
                for timezone in timezones:
                    tz_id = set_xml_value(create_element('t:Id'), timezone.ms_id, version=self.protocol.version)
                    tz_ids.append(tz_id)
                payload.append(tz_ids)
        return payload

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            tz_id = elem.get('Id')
            tz_name = elem.get('Name')
            tz_periods = self._get_periods(elem)
            tz_transitions_groups = self._get_transitions_groups(elem)
            tz_transitions = self._get_transitions(elem)
            yield tz_id, tz_name, tz_periods, tz_transitions, tz_transitions_groups

    @staticmethod
    def _get_periods(timezonedef):
        tz_periods = {}
        periods = timezonedef.find('{%s}Periods' % TNS)
        for period in periods.findall('{%s}Period' % TNS):
            # Convert e.g. "trule:Microsoft/Registry/W. Europe Standard Time/2006-Daylight" to (2006, 'Daylight')
            p_year, p_type = period.get('Id').rsplit('/', 1)[1].split('-')
            tz_periods[(int(p_year), p_type)] = dict(
                name=period.get('Name'),
                bias=xml_text_to_value(period.get('Bias'), datetime.timedelta)
            )
        return tz_periods

    @staticmethod
    def _get_transitions_groups(timezonedef):
        tz_transitions_groups = {}
        transitiongroups = timezonedef.find('{%s}TransitionsGroups' % TNS)
        if transitiongroups is not None:
            for transitiongroup in transitiongroups.findall('{%s}TransitionsGroup' % TNS):
                tg_id = int(transitiongroup.get('Id'))
                tz_transitions_groups[tg_id] = []
                for transition in transitiongroup.findall('{%s}Transition' % TNS):
                    # Apply same conversion to To as for period IDs
                    to_year, to_type = transition.find('{%s}To' % TNS).text.rsplit('/', 1)[1].split('-')
                    tz_transitions_groups[tg_id].append(dict(
                        to=(int(to_year), to_type),
                    ))
                for transition in transitiongroup.findall('{%s}RecurringDayTransition' % TNS):
                    # Apply same conversion to To as for period IDs
                    to_year, to_type = transition.find('{%s}To' % TNS).text.rsplit('/', 1)[1].split('-')
                    occurrence = xml_text_to_value(transition.find('{%s}Occurrence' % TNS).text, int)
                    if occurrence == -1:
                        # See TimeZoneTransition.from_xml()
                        occurrence = 5
                    tz_transitions_groups[tg_id].append(dict(
                        to=(int(to_year), to_type),
                        offset=xml_text_to_value(transition.find('{%s}TimeOffset' % TNS).text, datetime.timedelta),
                        iso_month=xml_text_to_value(transition.find('{%s}Month' % TNS).text, int),
                        iso_weekday=WEEKDAY_NAMES.index(transition.find('{%s}DayOfWeek' % TNS).text) + 1,
                        occurrence=occurrence,
                    ))
        return tz_transitions_groups

    @staticmethod
    def _get_transitions(timezonedef):
        tz_transitions = {}
        transitions = timezonedef.find('{%s}Transitions' % TNS)
        if transitions is not None:
            for transition in transitions.findall('{%s}Transition' % TNS):
                to = transition.find('{%s}To' % TNS)
                if to.get('Kind') != 'Group':
                    raise ValueError('Unexpected "Kind" XML attr: %s' % to.get('Kind'))
                tg_id = xml_text_to_value(to.text, int)
                tz_transitions[tg_id] = None
            for transition in transitions.findall('{%s}AbsoluteDateTransition' % TNS):
                to = transition.find('{%s}To' % TNS)
                if to.get('Kind') != 'Group':
                    raise ValueError('Unexpected "Kind" XML attr: %s' % to.get('Kind'))
                tg_id = xml_text_to_value(to.text, int)
                try:
                    t_date = xml_text_to_value(transition.find('{%s}DateTime' % TNS).text, EWSDateTime).date()
                except NaiveDateTimeNotAllowed as e:
                    # We encountered a naive datetime. Don't worry. we just need the date
                    t_date = e.local_dt.date()
                tz_transitions[tg_id] = t_date
        return tz_transitions

Ancestors

Class variables

var SERVICE_NAME
var element_container_name
var supported_from

Methods

def call(self, timezones=None, return_full_timezone_data=False)
Expand source code
def call(self, timezones=None, return_full_timezone_data=False):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(
        timezones=timezones,
        return_full_timezone_data=return_full_timezone_data
    )))
def get_payload(self, timezones, return_full_timezone_data)
Expand source code
def get_payload(self, timezones, return_full_timezone_data):
    payload = create_element(
        'm:%s' % self.SERVICE_NAME,
        attrs=dict(ReturnFullTimeZoneData='true' if return_full_timezone_data else 'false'),
    )
    if timezones is not None:
        is_empty, timezones = peek(timezones)
        if not is_empty:
            tz_ids = create_element('m:Ids')
            for timezone in timezones:
                tz_id = set_xml_value(create_element('t:Id'), timezone.ms_id, version=self.protocol.version)
                tz_ids.append(tz_id)
            payload.append(tz_ids)
    return payload

Inherited members

class GetStreamingEvents (*args, **kwargs)
Expand source code
class GetStreamingEvents(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getstreamingevents-operation
    """

    SERVICE_NAME = 'GetStreamingEvents'
    element_container_name = '{%s}Notifications' % MNS
    streaming = True
    prefer_affinity = True

    # Connection status values
    OK = 'OK'
    CLOSED = 'Closed'

    def __init__(self, *args, **kwargs):
        # These values are set each time call() is consumed
        self.connection_status = None
        self.error_subscription_ids = []
        super().__init__(*args, **kwargs)

    def call(self, subscription_ids, connection_timeout):
        if connection_timeout < 1:
            raise ValueError("'connection_timeout' must be a positive integer")
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
                subscription_ids=subscription_ids, connection_timeout=connection_timeout,
        )))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Notification.from_xml(elem=elem, account=None)

    @classmethod
    def _get_soap_parts(cls, response, **parse_opts):
        # Pass the response unaltered. We want to use our custom document yielder
        return None, response

    def _get_soap_messages(self, body, **parse_opts):
        # 'body' is actually the raw response passed on by '_get_soap_parts'. We want to continuously read the content,
        # looking for complete XML documents. When we have a full document, we want to parse it as if it was a normal
        # XML response.
        r = body
        for i, doc in enumerate(DocumentYielder(r.iter_content()), start=1):
            xml_log.debug('''Response XML (docs received: %(i)s): %(xml_response)s''', dict(i=i, xml_response=doc))
            response = DummyResponse(url=None, headers=None, request_headers=None, content=doc)
            try:
                _, body = super()._get_soap_parts(response=response, **parse_opts)
            except Exception:
                r.close()  # Release memory
                raise
            # TODO: We're skipping ._update_api_version() here because we don't have access to the 'api_version' used.
            # TODO: We should be doing a lot of error handling for ._get_soap_messages().
            yield from super()._get_soap_messages(body=body, **parse_opts)
            if self.connection_status == self.CLOSED:
                # Don't wait for the TCP connection to timeout
                break

    def _get_element_container(self, message, name=None):
        error_ids_elem = message.find('{%s}ErrorSubscriptionIds' % MNS)
        if error_ids_elem is not None:
            self.error_subscription_ids = get_xml_attrs(error_ids_elem, '{%s}ErrorSubscriptionId' % MNS)
            log.debug('These subscription IDs are invalid: %s', self.error_subscription_ids)
        self.connection_status = get_xml_attr(message, '{%s}ConnectionStatus' % MNS)  # Either 'OK' or 'Closed'
        log.debug('Connection status is: %s', self.connection_status)
        # Upstream expects to find a 'name' tag but our response does not always have it. Return an empty element.
        if message.find(name) is None:
            return []
        return super()._get_element_container(message=message, name=name)

    def get_payload(self, subscription_ids, connection_timeout):
        getstreamingevents = create_element('m:%s' % self.SERVICE_NAME)
        subscriptions_elem = create_element('m:SubscriptionIds')
        for subscription_id in subscription_ids:
            add_xml_child(subscriptions_elem, 't:SubscriptionId', subscription_id)
        if not len(subscriptions_elem):
            raise ValueError('"subscription_ids" must not be empty')

        getstreamingevents.append(subscriptions_elem)
        add_xml_child(getstreamingevents, 'm:ConnectionTimeout', connection_timeout)
        return getstreamingevents

Ancestors

Class variables

var CLOSED
var OK
var SERVICE_NAME
var element_container_name
var prefer_affinity
var streaming

Methods

def call(self, subscription_ids, connection_timeout)
Expand source code
def call(self, subscription_ids, connection_timeout):
    if connection_timeout < 1:
        raise ValueError("'connection_timeout' must be a positive integer")
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            subscription_ids=subscription_ids, connection_timeout=connection_timeout,
    )))
def get_payload(self, subscription_ids, connection_timeout)
Expand source code
def get_payload(self, subscription_ids, connection_timeout):
    getstreamingevents = create_element('m:%s' % self.SERVICE_NAME)
    subscriptions_elem = create_element('m:SubscriptionIds')
    for subscription_id in subscription_ids:
        add_xml_child(subscriptions_elem, 't:SubscriptionId', subscription_id)
    if not len(subscriptions_elem):
        raise ValueError('"subscription_ids" must not be empty')

    getstreamingevents.append(subscriptions_elem)
    add_xml_child(getstreamingevents, 'm:ConnectionTimeout', connection_timeout)
    return getstreamingevents

Inherited members

class GetUserAvailability (protocol, chunk_size=None, timeout=None)
Expand source code
class GetUserAvailability(EWSService):
    """Get detailed availability information for a list of users.
    MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getuseravailability-operation
    """

    SERVICE_NAME = 'GetUserAvailability'

    def call(self, timezone, mailbox_data, free_busy_view_options):
        # TODO: Also supports SuggestionsViewOptions, see
        #  https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/suggestionsviewoptions
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            timezone=timezone,
            mailbox_data=mailbox_data,
            free_busy_view_options=free_busy_view_options
        )))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield FreeBusyView.from_xml(elem=elem, account=None)

    def get_payload(self, timezone, mailbox_data, free_busy_view_options):
        payload = create_element('m:%sRequest' % self.SERVICE_NAME)
        set_xml_value(payload, timezone, version=self.protocol.version)
        mailbox_data_array = create_element('m:MailboxDataArray')
        set_xml_value(mailbox_data_array, mailbox_data, version=self.protocol.version)
        payload.append(mailbox_data_array)
        set_xml_value(payload, free_busy_view_options, version=self.protocol.version)
        return payload

    @staticmethod
    def _response_messages_tag():
        return '{%s}FreeBusyResponseArray' % MNS

    @classmethod
    def _response_message_tag(cls):
        return '{%s}FreeBusyResponse' % MNS

    def _get_elements_in_response(self, response):
        for msg in response:
            # Just check the response code and raise errors
            self._get_element_container(message=msg.find('{%s}ResponseMessage' % MNS))
            yield from self._get_elements_in_container(container=msg)

    @classmethod
    def _get_elements_in_container(cls, container):
        return [container.find('{%s}FreeBusyView' % MNS)]

Ancestors

Class variables

var SERVICE_NAME

Methods

def call(self, timezone, mailbox_data, free_busy_view_options)
Expand source code
def call(self, timezone, mailbox_data, free_busy_view_options):
    # TODO: Also supports SuggestionsViewOptions, see
    #  https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/suggestionsviewoptions
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(
        timezone=timezone,
        mailbox_data=mailbox_data,
        free_busy_view_options=free_busy_view_options
    )))
def get_payload(self, timezone, mailbox_data, free_busy_view_options)
Expand source code
def get_payload(self, timezone, mailbox_data, free_busy_view_options):
    payload = create_element('m:%sRequest' % self.SERVICE_NAME)
    set_xml_value(payload, timezone, version=self.protocol.version)
    mailbox_data_array = create_element('m:MailboxDataArray')
    set_xml_value(mailbox_data_array, mailbox_data, version=self.protocol.version)
    payload.append(mailbox_data_array)
    set_xml_value(payload, free_busy_view_options, version=self.protocol.version)
    return payload

Inherited members

class GetUserConfiguration (*args, **kwargs)
Expand source code
class GetUserConfiguration(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getuserconfiguration-operation
    """

    SERVICE_NAME = 'GetUserConfiguration'

    def call(self, user_configuration_name, properties):
        if properties not in PROPERTIES_CHOICES:
            raise ValueError("'properties' %r must be one of %s" % (properties, PROPERTIES_CHOICES))
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
                user_configuration_name=user_configuration_name, properties=properties
        )))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield UserConfiguration.from_xml(elem=elem, account=self.account)

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(UserConfiguration.response_tag())

    def get_payload(self, user_configuration_name, properties):
        getuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(getuserconfiguration, user_configuration_name, version=self.account.version)
        user_configuration_properties = create_element('m:UserConfigurationProperties')
        set_xml_value(user_configuration_properties, properties, version=self.account.version)
        getuserconfiguration.append(user_configuration_properties)
        return getuserconfiguration

Ancestors

Class variables

var SERVICE_NAME

Methods

def call(self, user_configuration_name, properties)
Expand source code
def call(self, user_configuration_name, properties):
    if properties not in PROPERTIES_CHOICES:
        raise ValueError("'properties' %r must be one of %s" % (properties, PROPERTIES_CHOICES))
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            user_configuration_name=user_configuration_name, properties=properties
    )))
def get_payload(self, user_configuration_name, properties)
Expand source code
def get_payload(self, user_configuration_name, properties):
    getuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(getuserconfiguration, user_configuration_name, version=self.account.version)
    user_configuration_properties = create_element('m:UserConfigurationProperties')
    set_xml_value(user_configuration_properties, properties, version=self.account.version)
    getuserconfiguration.append(user_configuration_properties)
    return getuserconfiguration

Inherited members

class GetUserOofSettings (*args, **kwargs)
Expand source code
class GetUserOofSettings(EWSAccountService):
    """Get automatic reply settings for the specified mailbox.
    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getuseroofsettings-operation
    """

    SERVICE_NAME = 'GetUserOofSettings'
    element_container_name = '{%s}OofSettings' % TNS

    def call(self, mailbox):
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(mailbox=mailbox)))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield OofSettings.from_xml(elem=elem, account=self.account)

    def get_payload(self, mailbox):
        payload = create_element('m:%sRequest' % self.SERVICE_NAME)
        return set_xml_value(payload, AvailabilityMailbox.from_mailbox(mailbox), version=self.account.version)

    @classmethod
    def _get_elements_in_container(cls, container):
        # This service only returns one result, directly in 'container'
        return [container]

    def _get_element_container(self, message, name=None):
        # This service returns the result container outside the response message
        super()._get_element_container(message=message.find(self._response_message_tag()), name=None)
        return message.find(name)

    @classmethod
    def _response_message_tag(cls):
        return '{%s}ResponseMessage' % MNS

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, mailbox)
Expand source code
def call(self, mailbox):
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(mailbox=mailbox)))
def get_payload(self, mailbox)
Expand source code
def get_payload(self, mailbox):
    payload = create_element('m:%sRequest' % self.SERVICE_NAME)
    return set_xml_value(payload, AvailabilityMailbox.from_mailbox(mailbox), version=self.account.version)

Inherited members

class MarkAsJunk (*args, **kwargs)
Expand source code
class MarkAsJunk(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/markasjunk-operation"""

    SERVICE_NAME = 'MarkAsJunk'

    def call(self, items, is_junk, move_item):
        return self._elems_to_objs(
            self._chunked_get_elements(self.get_payload, items=items, is_junk=is_junk, move_item=move_item)
        )

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, (Exception, type(None))):
                yield elem
                continue
            yield MovedItemId.id_from_xml(elem)

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(MovedItemId.response_tag())

    def get_payload(self, items, is_junk, move_item):
        # Takes a list of items and returns either success or raises an error message
        mark_as_junk = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=dict(IsJunk='true' if is_junk else 'false', MoveItem='true' if move_item else 'false')
        )
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        mark_as_junk.append(item_ids)
        return mark_as_junk

Ancestors

Class variables

var SERVICE_NAME

Methods

def call(self, items, is_junk, move_item)
Expand source code
def call(self, items, is_junk, move_item):
    return self._elems_to_objs(
        self._chunked_get_elements(self.get_payload, items=items, is_junk=is_junk, move_item=move_item)
    )
def get_payload(self, items, is_junk, move_item)
Expand source code
def get_payload(self, items, is_junk, move_item):
    # Takes a list of items and returns either success or raises an error message
    mark_as_junk = create_element(
        'm:%s' % self.SERVICE_NAME,
        attrs=dict(IsJunk='true' if is_junk else 'false', MoveItem='true' if move_item else 'false')
    )
    item_ids = create_item_ids_element(items=items, version=self.account.version)
    mark_as_junk.append(item_ids)
    return mark_as_junk

Inherited members

class MoveFolder (*args, **kwargs)
Expand source code
class MoveFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/movefolder-operation"""

    SERVICE_NAME = "MoveFolder"
    element_container_name = '{%s}Folders' % MNS

    def call(self, folders, to_folder):
        from ..folders import BaseFolder, FolderId
        if not isinstance(to_folder, (BaseFolder, FolderId)):
            raise ValueError("'to_folder' %r must be a Folder or FolderId instance" % to_folder)
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=folders, to_folder=to_folder))

    def _elems_to_objs(self, elems):
        from ..folders import FolderId
        for elem in elems:
            if isinstance(elem, (Exception, type(None))):
                yield elem
                continue
            yield FolderId.from_xml(elem=elem.find(FolderId.response_tag()), account=self.account)

    def get_payload(self, folders, to_folder):
        # Takes a list of folders and returns their new folder IDs
        movefolder = create_element('m:%s' % self.SERVICE_NAME)
        tofolderid = create_element('m:ToFolderId')
        set_xml_value(tofolderid, to_folder, version=self.account.version)
        movefolder.append(tofolderid)
        folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
        movefolder.append(folder_ids)
        return movefolder

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, folders, to_folder)
Expand source code
def call(self, folders, to_folder):
    from ..folders import BaseFolder, FolderId
    if not isinstance(to_folder, (BaseFolder, FolderId)):
        raise ValueError("'to_folder' %r must be a Folder or FolderId instance" % to_folder)
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=folders, to_folder=to_folder))
def get_payload(self, folders, to_folder)
Expand source code
def get_payload(self, folders, to_folder):
    # Takes a list of folders and returns their new folder IDs
    movefolder = create_element('m:%s' % self.SERVICE_NAME)
    tofolderid = create_element('m:ToFolderId')
    set_xml_value(tofolderid, to_folder, version=self.account.version)
    movefolder.append(tofolderid)
    folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
    movefolder.append(folder_ids)
    return movefolder

Inherited members

class MoveItem (*args, **kwargs)
Expand source code
class MoveItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/moveitem-operation"""

    SERVICE_NAME = 'MoveItem'
    element_container_name = '{%s}Items' % MNS

    def call(self, items, to_folder):
        from ..folders import BaseFolder, FolderId
        if not isinstance(to_folder, (BaseFolder, FolderId)):
            raise ValueError("'to_folder' %r must be a Folder or FolderId instance" % to_folder)
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, to_folder=to_folder))

    def _elems_to_objs(self, elems):
        from ..items import Item
        for elem in elems:
            if isinstance(elem, (Exception, type(None))):
                yield elem
                continue
            yield Item.id_from_xml(elem)

    def get_payload(self, items, to_folder):
        # Takes a list of items and returns their new item IDs
        moveitem = create_element('m:%s' % self.SERVICE_NAME)
        tofolderid = create_element('m:ToFolderId')
        set_xml_value(tofolderid, to_folder, version=self.account.version)
        moveitem.append(tofolderid)
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        moveitem.append(item_ids)
        return moveitem

Ancestors

Subclasses

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, items, to_folder)
Expand source code
def call(self, items, to_folder):
    from ..folders import BaseFolder, FolderId
    if not isinstance(to_folder, (BaseFolder, FolderId)):
        raise ValueError("'to_folder' %r must be a Folder or FolderId instance" % to_folder)
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, to_folder=to_folder))
def get_payload(self, items, to_folder)
Expand source code
def get_payload(self, items, to_folder):
    # Takes a list of items and returns their new item IDs
    moveitem = create_element('m:%s' % self.SERVICE_NAME)
    tofolderid = create_element('m:ToFolderId')
    set_xml_value(tofolderid, to_folder, version=self.account.version)
    moveitem.append(tofolderid)
    item_ids = create_item_ids_element(items=items, version=self.account.version)
    moveitem.append(item_ids)
    return moveitem

Inherited members

class ResolveNames (*args, **kwargs)
Expand source code
class ResolveNames(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/resolvenames-operation"""

    SERVICE_NAME = 'ResolveNames'
    element_container_name = '{%s}ResolutionSet' % MNS
    ERRORS_TO_CATCH_IN_RESPONSE = ErrorNameResolutionNoResults
    WARNINGS_TO_IGNORE_IN_RESPONSE = ErrorNameResolutionMultipleResults
    # Note: paging information is returned as attrs on the 'ResolutionSet' element, but this service does not
    # support the 'IndexedPageItemView' element, so it's not really a paging service. According to docs, at most
    # 100 candidates are returned for a lookup.
    supports_paging = False

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.return_full_contact_data = False  # A hack to communicate parsing args to _elems_to_objs()

    def call(self, unresolved_entries, parent_folders=None, return_full_contact_data=False, search_scope=None,
             contact_data_shape=None):
        from ..items import SHAPE_CHOICES, SEARCH_SCOPE_CHOICES
        if self.chunk_size > 100:
            log.warning(
                'Chunk size %s is dangerously high. %s supports returning at most 100 candidates for a lookup',
                self.chunk_size, self.SERVICE_NAME
            )
        if search_scope and search_scope not in SEARCH_SCOPE_CHOICES:
            raise ValueError("'search_scope' %s must be one if %s" % (search_scope, SEARCH_SCOPE_CHOICES))
        if contact_data_shape and contact_data_shape not in SHAPE_CHOICES:
            raise ValueError("'shape' %s must be one if %s" % (contact_data_shape, SHAPE_CHOICES))
        self.return_full_contact_data = return_full_contact_data
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=unresolved_entries,
            parent_folders=parent_folders,
            return_full_contact_data=return_full_contact_data,
            search_scope=search_scope,
            contact_data_shape=contact_data_shape,
        ))

    def _elems_to_objs(self, elems):
        from ..items import Contact
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            if self.return_full_contact_data:
                mailbox_elem = elem.find(Mailbox.response_tag())
                contact_elem = elem.find(Contact.response_tag())
                yield (
                    None if mailbox_elem is None else Mailbox.from_xml(elem=mailbox_elem, account=None),
                    None if contact_elem is None else Contact.from_xml(elem=contact_elem, account=None),
                )
            else:
                yield Mailbox.from_xml(elem=elem.find(Mailbox.response_tag()), account=None)

    def get_payload(self, unresolved_entries, parent_folders, return_full_contact_data, search_scope,
                    contact_data_shape):
        payload = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=dict(ReturnFullContactData='true' if return_full_contact_data else 'false'),
        )
        if search_scope:
            payload.set('SearchScope', search_scope)
        if contact_data_shape:
            if self.protocol.version.build < EXCHANGE_2010_SP2:
                raise NotImplementedError(
                    "'contact_data_shape' is only supported for Exchange 2010 SP2 servers and later")
            payload.set('ContactDataShape', contact_data_shape)
        if parent_folders:
            parentfolderids = create_element('m:ParentFolderIds')
            set_xml_value(parentfolderids, parent_folders, version=self.protocol.version)
        for entry in unresolved_entries:
            add_xml_child(payload, 'm:UnresolvedEntry', entry)
        if not len(payload):
            raise ValueError('"unresolved_entries" must not be empty')
        return payload

Ancestors

Class variables

var ERRORS_TO_CATCH_IN_RESPONSE

Global error type within this module.

var SERVICE_NAME
var WARNINGS_TO_IGNORE_IN_RESPONSE

Global error type within this module.

var element_container_name
var supports_paging

Methods

def call(self, unresolved_entries, parent_folders=None, return_full_contact_data=False, search_scope=None, contact_data_shape=None)
Expand source code
def call(self, unresolved_entries, parent_folders=None, return_full_contact_data=False, search_scope=None,
         contact_data_shape=None):
    from ..items import SHAPE_CHOICES, SEARCH_SCOPE_CHOICES
    if self.chunk_size > 100:
        log.warning(
            'Chunk size %s is dangerously high. %s supports returning at most 100 candidates for a lookup',
            self.chunk_size, self.SERVICE_NAME
        )
    if search_scope and search_scope not in SEARCH_SCOPE_CHOICES:
        raise ValueError("'search_scope' %s must be one if %s" % (search_scope, SEARCH_SCOPE_CHOICES))
    if contact_data_shape and contact_data_shape not in SHAPE_CHOICES:
        raise ValueError("'shape' %s must be one if %s" % (contact_data_shape, SHAPE_CHOICES))
    self.return_full_contact_data = return_full_contact_data
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload,
        items=unresolved_entries,
        parent_folders=parent_folders,
        return_full_contact_data=return_full_contact_data,
        search_scope=search_scope,
        contact_data_shape=contact_data_shape,
    ))
def get_payload(self, unresolved_entries, parent_folders, return_full_contact_data, search_scope, contact_data_shape)
Expand source code
def get_payload(self, unresolved_entries, parent_folders, return_full_contact_data, search_scope,
                contact_data_shape):
    payload = create_element(
        'm:%s' % self.SERVICE_NAME,
        attrs=dict(ReturnFullContactData='true' if return_full_contact_data else 'false'),
    )
    if search_scope:
        payload.set('SearchScope', search_scope)
    if contact_data_shape:
        if self.protocol.version.build < EXCHANGE_2010_SP2:
            raise NotImplementedError(
                "'contact_data_shape' is only supported for Exchange 2010 SP2 servers and later")
        payload.set('ContactDataShape', contact_data_shape)
    if parent_folders:
        parentfolderids = create_element('m:ParentFolderIds')
        set_xml_value(parentfolderids, parent_folders, version=self.protocol.version)
    for entry in unresolved_entries:
        add_xml_child(payload, 'm:UnresolvedEntry', entry)
    if not len(payload):
        raise ValueError('"unresolved_entries" must not be empty')
    return payload

Inherited members

class SendItem (*args, **kwargs)
Expand source code
class SendItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/senditem-operation"""

    SERVICE_NAME = 'SendItem'
    returns_elements = False

    def call(self, items, saved_item_folder):
        from ..folders import BaseFolder, FolderId
        if saved_item_folder and not isinstance(saved_item_folder, (BaseFolder, FolderId)):
            raise ValueError("'saved_item_folder' %r must be a Folder or FolderId instance" % saved_item_folder)
        return self._chunked_get_elements(self.get_payload, items=items, saved_item_folder=saved_item_folder)

    def get_payload(self, items, saved_item_folder):
        senditem = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=dict(SaveItemToFolder='true' if saved_item_folder else 'false'),
        )
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        senditem.append(item_ids)
        if saved_item_folder:
            saveditemfolderid = create_element('m:SavedItemFolderId')
            set_xml_value(saveditemfolderid, saved_item_folder, version=self.account.version)
            senditem.append(saveditemfolderid)
        return senditem

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, items, saved_item_folder)
Expand source code
def call(self, items, saved_item_folder):
    from ..folders import BaseFolder, FolderId
    if saved_item_folder and not isinstance(saved_item_folder, (BaseFolder, FolderId)):
        raise ValueError("'saved_item_folder' %r must be a Folder or FolderId instance" % saved_item_folder)
    return self._chunked_get_elements(self.get_payload, items=items, saved_item_folder=saved_item_folder)
def get_payload(self, items, saved_item_folder)
Expand source code
def get_payload(self, items, saved_item_folder):
    senditem = create_element(
        'm:%s' % self.SERVICE_NAME,
        attrs=dict(SaveItemToFolder='true' if saved_item_folder else 'false'),
    )
    item_ids = create_item_ids_element(items=items, version=self.account.version)
    senditem.append(item_ids)
    if saved_item_folder:
        saveditemfolderid = create_element('m:SavedItemFolderId')
        set_xml_value(saveditemfolderid, saved_item_folder, version=self.account.version)
        senditem.append(saveditemfolderid)
    return senditem

Inherited members

class SendNotification (protocol, chunk_size=None, timeout=None)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/sendnotification

This is not an actual EWS service you can call. We only use it to parse the XML body of push notifications.

Expand source code
class SendNotification(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/sendnotification

    This is not an actual EWS service you can call. We only use it to parse the XML body of push notifications.
    """

    SERVICE_NAME = 'SendNotification'

    def call(self):
        raise NotImplementedError()

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Notification.from_xml(elem=elem, account=None)

    @classmethod
    def _response_tag(cls):
        """Return the name of the element containing the service response."""
        return '{%s}%s' % (MNS, cls.SERVICE_NAME)

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(Notification.response_tag())

Ancestors

Class variables

var SERVICE_NAME

Methods

def call(self)
Expand source code
def call(self):
    raise NotImplementedError()

Inherited members

class SetUserOofSettings (*args, **kwargs)
Expand source code
class SetUserOofSettings(EWSAccountService):
    """Set automatic replies for the specified mailbox.
    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/setuseroofsettings-operation
    """

    SERVICE_NAME = 'SetUserOofSettings'
    returns_elements = False

    def call(self, oof_settings, mailbox):
        if not isinstance(oof_settings, OofSettings):
            raise ValueError("'oof_settings' %r must be an OofSettings instance" % oof_settings)
        if not isinstance(mailbox, Mailbox):
            raise ValueError("'mailbox' %r must be an Mailbox instance" % mailbox)
        return self._get_elements(payload=self.get_payload(oof_settings=oof_settings, mailbox=mailbox))

    def get_payload(self, oof_settings, mailbox):
        payload = create_element('m:%sRequest' % self.SERVICE_NAME)
        set_xml_value(payload, AvailabilityMailbox.from_mailbox(mailbox), version=self.account.version)
        set_xml_value(payload, oof_settings, version=self.account.version)
        return payload

    def _get_element_container(self, message, name=None):
        message = message.find(self._response_message_tag())
        return super()._get_element_container(message=message, name=name)

    @classmethod
    def _response_message_tag(cls):
        return '{%s}ResponseMessage' % MNS

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, oof_settings, mailbox)
Expand source code
def call(self, oof_settings, mailbox):
    if not isinstance(oof_settings, OofSettings):
        raise ValueError("'oof_settings' %r must be an OofSettings instance" % oof_settings)
    if not isinstance(mailbox, Mailbox):
        raise ValueError("'mailbox' %r must be an Mailbox instance" % mailbox)
    return self._get_elements(payload=self.get_payload(oof_settings=oof_settings, mailbox=mailbox))
def get_payload(self, oof_settings, mailbox)
Expand source code
def get_payload(self, oof_settings, mailbox):
    payload = create_element('m:%sRequest' % self.SERVICE_NAME)
    set_xml_value(payload, AvailabilityMailbox.from_mailbox(mailbox), version=self.account.version)
    set_xml_value(payload, oof_settings, version=self.account.version)
    return payload

Inherited members

class SubscribeToPull (*args, **kwargs)
Expand source code
class SubscribeToPull(Subscribe):
    subscription_request_elem_tag = 'm:PullSubscriptionRequest'

    def call(self, folders, event_types, watermark, timeout):
        yield from self._partial_call(
            payload_func=self.get_payload, folders=folders, event_types=event_types, timeout=timeout,
            watermark=watermark,
        )

    def get_payload(self, folders, event_types, watermark, timeout):
        subscribe = create_element('m:%s' % self.SERVICE_NAME)
        request_elem = self._partial_payload(folders=folders, event_types=event_types)
        if watermark:
            add_xml_child(request_elem, 'm:Watermark', watermark)
        add_xml_child(request_elem, 't:Timeout', timeout)  # In minutes
        subscribe.append(request_elem)
        return subscribe

Ancestors

Class variables

var subscription_request_elem_tag

Methods

def call(self, folders, event_types, watermark, timeout)
Expand source code
def call(self, folders, event_types, watermark, timeout):
    yield from self._partial_call(
        payload_func=self.get_payload, folders=folders, event_types=event_types, timeout=timeout,
        watermark=watermark,
    )
def get_payload(self, folders, event_types, watermark, timeout)
Expand source code
def get_payload(self, folders, event_types, watermark, timeout):
    subscribe = create_element('m:%s' % self.SERVICE_NAME)
    request_elem = self._partial_payload(folders=folders, event_types=event_types)
    if watermark:
        add_xml_child(request_elem, 'm:Watermark', watermark)
    add_xml_child(request_elem, 't:Timeout', timeout)  # In minutes
    subscribe.append(request_elem)
    return subscribe

Inherited members

class SubscribeToPush (*args, **kwargs)
Expand source code
class SubscribeToPush(Subscribe):
    subscription_request_elem_tag = 'm:PushSubscriptionRequest'

    def call(self, folders, event_types, watermark, status_frequency, url):
        yield from self._partial_call(
            payload_func=self.get_payload, folders=folders, event_types=event_types,
            status_frequency=status_frequency, url=url, watermark=watermark,
        )

    def get_payload(self, folders, event_types, watermark, status_frequency, url):
        subscribe = create_element('m:%s' % self.SERVICE_NAME)
        request_elem = self._partial_payload(folders=folders, event_types=event_types)
        if watermark:
            add_xml_child(request_elem, 'm:Watermark', watermark)
        add_xml_child(request_elem, 't:StatusFrequency', status_frequency)  # In minutes
        add_xml_child(request_elem, 't:URL', url)
        subscribe.append(request_elem)
        return subscribe

Ancestors

Class variables

var subscription_request_elem_tag

Methods

def call(self, folders, event_types, watermark, status_frequency, url)
Expand source code
def call(self, folders, event_types, watermark, status_frequency, url):
    yield from self._partial_call(
        payload_func=self.get_payload, folders=folders, event_types=event_types,
        status_frequency=status_frequency, url=url, watermark=watermark,
    )
def get_payload(self, folders, event_types, watermark, status_frequency, url)
Expand source code
def get_payload(self, folders, event_types, watermark, status_frequency, url):
    subscribe = create_element('m:%s' % self.SERVICE_NAME)
    request_elem = self._partial_payload(folders=folders, event_types=event_types)
    if watermark:
        add_xml_child(request_elem, 'm:Watermark', watermark)
    add_xml_child(request_elem, 't:StatusFrequency', status_frequency)  # In minutes
    add_xml_child(request_elem, 't:URL', url)
    subscribe.append(request_elem)
    return subscribe

Inherited members

class SubscribeToStreaming (*args, **kwargs)
Expand source code
class SubscribeToStreaming(Subscribe):
    subscription_request_elem_tag = 'm:StreamingSubscriptionRequest'

    def call(self, folders, event_types):
        yield from self._partial_call(payload_func=self.get_payload, folders=folders, event_types=event_types)

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield elem.text

    @classmethod
    def _get_elements_in_container(cls, container):
        return [container.find('{%s}SubscriptionId' % MNS)]

    def get_payload(self, folders, event_types):
        subscribe = create_element('m:%s' % self.SERVICE_NAME)
        request_elem = self._partial_payload(folders=folders, event_types=event_types)
        subscribe.append(request_elem)
        return subscribe

Ancestors

Class variables

var subscription_request_elem_tag

Methods

def call(self, folders, event_types)
Expand source code
def call(self, folders, event_types):
    yield from self._partial_call(payload_func=self.get_payload, folders=folders, event_types=event_types)
def get_payload(self, folders, event_types)
Expand source code
def get_payload(self, folders, event_types):
    subscribe = create_element('m:%s' % self.SERVICE_NAME)
    request_elem = self._partial_payload(folders=folders, event_types=event_types)
    subscribe.append(request_elem)
    return subscribe

Inherited members

class SyncFolderHierarchy (*args, **kwargs)
Expand source code
class SyncFolderHierarchy(SyncFolder):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderhierarchy-operation
    """

    SERVICE_NAME = 'SyncFolderHierarchy'
    shape_tag = 'm:FolderShape'
    last_in_range_name = '{%s}IncludesLastFolderInRange' % MNS

    def call(self, folder, shape, additional_fields, sync_state):
        self.sync_state = sync_state
        change_types = self._change_types_map()
        for elem in self._get_elements(payload=self.get_payload(
                folder=folder,
                shape=shape,
                additional_fields=additional_fields,
                sync_state=sync_state,
        )):
            if isinstance(elem, Exception):
                yield elem
                continue
            change_type = change_types[elem.tag]
            if change_type == self.DELETE:
                folder = FolderId.from_xml(elem=elem.find(FolderId.response_tag()), account=self.account)
            else:
                # We can't find() the element because we don't know which tag to look for. The change element can
                # contain multiple folder types, each with their own tag.
                folder_elem = elem[0]
                folder = parse_folder_elem(elem=folder_elem, folder=folder, account=self.account)
            yield change_type, folder

    def get_payload(self, folder, shape, additional_fields, sync_state):
        return self._partial_get_payload(
            folder=folder, shape=shape, additional_fields=additional_fields, sync_state=sync_state
        )

Ancestors

Class variables

var SERVICE_NAME
var last_in_range_name
var shape_tag

Methods

def call(self, folder, shape, additional_fields, sync_state)
Expand source code
def call(self, folder, shape, additional_fields, sync_state):
    self.sync_state = sync_state
    change_types = self._change_types_map()
    for elem in self._get_elements(payload=self.get_payload(
            folder=folder,
            shape=shape,
            additional_fields=additional_fields,
            sync_state=sync_state,
    )):
        if isinstance(elem, Exception):
            yield elem
            continue
        change_type = change_types[elem.tag]
        if change_type == self.DELETE:
            folder = FolderId.from_xml(elem=elem.find(FolderId.response_tag()), account=self.account)
        else:
            # We can't find() the element because we don't know which tag to look for. The change element can
            # contain multiple folder types, each with their own tag.
            folder_elem = elem[0]
            folder = parse_folder_elem(elem=folder_elem, folder=folder, account=self.account)
        yield change_type, folder
def get_payload(self, folder, shape, additional_fields, sync_state)
Expand source code
def get_payload(self, folder, shape, additional_fields, sync_state):
    return self._partial_get_payload(
        folder=folder, shape=shape, additional_fields=additional_fields, sync_state=sync_state
    )

Inherited members

class SyncFolderItems (*args, **kwargs)
Expand source code
class SyncFolderItems(SyncFolder):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderitems-operation
    """

    SERVICE_NAME = 'SyncFolderItems'
    SYNC_SCOPES = {
        'NormalItems',
        'NormalAndAssociatedItems',
    }
    # Extra change type
    READ_FLAG_CHANGE = 'read_flag_change'
    CHANGE_TYPES = SyncFolder.CHANGE_TYPES + (READ_FLAG_CHANGE,)
    shape_tag = 'm:ItemShape'
    last_in_range_name = '{%s}IncludesLastItemInRange' % MNS

    def _change_types_map(self):
        res = super()._change_types_map()
        res['{%s}ReadFlagChange' % TNS] = self.READ_FLAG_CHANGE
        return res

    def call(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope):
        self.sync_state = sync_state
        if max_changes_returned is None:
            max_changes_returned = self.chunk_size
        if max_changes_returned <= 0:
            raise ValueError("'max_changes_returned' %s must be a positive integer" % max_changes_returned)
        if sync_scope is not None and sync_scope not in self.SYNC_SCOPES:
            raise ValueError("'sync_scope' %s must be one of %r" % (sync_scope, self.SYNC_SCOPES))
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
                folder=folder,
                shape=shape,
                additional_fields=additional_fields,
                sync_state=sync_state,
                ignore=ignore,
                max_changes_returned=max_changes_returned,
                sync_scope=sync_scope,
        )))

    def _elems_to_objs(self, elems):
        from ..folders.base import BaseFolder
        change_types = self._change_types_map()
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            change_type = change_types[elem.tag]
            if change_type == self.READ_FLAG_CHANGE:
                item = (
                    ItemId.from_xml(elem=elem.find(ItemId.response_tag()), account=self.account),
                    xml_text_to_value(elem.find('{%s}IsRead' % TNS).text, bool)
                )
            elif change_type == self.DELETE:
                item = ItemId.from_xml(elem=elem.find(ItemId.response_tag()), account=self.account)
            else:
                # We can't find() the element because we don't know which tag to look for. The change element can
                # contain multiple item types, each with their own tag.
                item_elem = elem[0]
                item = BaseFolder.item_model_from_tag(item_elem.tag).from_xml(elem=item_elem, account=self.account)
            yield change_type, item

    def get_payload(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope):
        syncfolderitems = self._partial_get_payload(
            folder=folder, shape=shape, additional_fields=additional_fields, sync_state=sync_state
        )
        is_empty, ignore = (True, None) if ignore is None else peek(ignore)
        if not is_empty:
            item_ids = create_item_ids_element(items=ignore, version=self.account.version, tag='m:Ignore')
            syncfolderitems.append(item_ids)
        add_xml_child(syncfolderitems, 'm:MaxChangesReturned', max_changes_returned)
        if sync_scope:
            add_xml_child(syncfolderitems, 'm:SyncScope', sync_scope)
        return syncfolderitems

Ancestors

Class variables

var CHANGE_TYPES
var READ_FLAG_CHANGE
var SERVICE_NAME
var SYNC_SCOPES
var last_in_range_name
var shape_tag

Methods

def call(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope)
Expand source code
def call(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope):
    self.sync_state = sync_state
    if max_changes_returned is None:
        max_changes_returned = self.chunk_size
    if max_changes_returned <= 0:
        raise ValueError("'max_changes_returned' %s must be a positive integer" % max_changes_returned)
    if sync_scope is not None and sync_scope not in self.SYNC_SCOPES:
        raise ValueError("'sync_scope' %s must be one of %r" % (sync_scope, self.SYNC_SCOPES))
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            folder=folder,
            shape=shape,
            additional_fields=additional_fields,
            sync_state=sync_state,
            ignore=ignore,
            max_changes_returned=max_changes_returned,
            sync_scope=sync_scope,
    )))
def get_payload(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope)
Expand source code
def get_payload(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope):
    syncfolderitems = self._partial_get_payload(
        folder=folder, shape=shape, additional_fields=additional_fields, sync_state=sync_state
    )
    is_empty, ignore = (True, None) if ignore is None else peek(ignore)
    if not is_empty:
        item_ids = create_item_ids_element(items=ignore, version=self.account.version, tag='m:Ignore')
        syncfolderitems.append(item_ids)
    add_xml_child(syncfolderitems, 'm:MaxChangesReturned', max_changes_returned)
    if sync_scope:
        add_xml_child(syncfolderitems, 'm:SyncScope', sync_scope)
    return syncfolderitems

Inherited members

class Unsubscribe (*args, **kwargs)

Unsubscribing is only valid for pull and streaming notifications.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/unsubscribe-operation

Expand source code
class Unsubscribe(EWSAccountService):
    """Unsubscribing is only valid for pull and streaming notifications.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/unsubscribe-operation
    """

    SERVICE_NAME = 'Unsubscribe'
    returns_elements = False

    def call(self, subscription_id):
        return self._get_elements(payload=self.get_payload(subscription_id=subscription_id))

    def get_payload(self, subscription_id):
        unsubscribe = create_element('m:%s' % self.SERVICE_NAME)
        add_xml_child(unsubscribe, 'm:SubscriptionId', subscription_id)
        return unsubscribe

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, subscription_id)
Expand source code
def call(self, subscription_id):
    return self._get_elements(payload=self.get_payload(subscription_id=subscription_id))
def get_payload(self, subscription_id)
Expand source code
def get_payload(self, subscription_id):
    unsubscribe = create_element('m:%s' % self.SERVICE_NAME)
    add_xml_child(unsubscribe, 'm:SubscriptionId', subscription_id)
    return unsubscribe

Inherited members

class UpdateFolder (*args, **kwargs)
Expand source code
class UpdateFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updatefolder-operation"""

    SERVICE_NAME = 'UpdateFolder'
    element_container_name = '{%s}Folders' % MNS

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.folders = []  # A hack to communicate parsing args to _elems_to_objs()

    def call(self, folders):
        # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same
        # class as the folder instance it was requested with.
        self.folders = list(folders)  # Convert to a list, in case 'folders' is a generator. We're iterating twice.
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=self.folders))

    def _elems_to_objs(self, elems):
        for (folder, _), elem in zip(self.folders, elems):
            if isinstance(elem, Exception):
                yield elem
                continue
            yield parse_folder_elem(elem=elem, folder=folder, account=self.account)

    @staticmethod
    def _sort_fieldnames(folder_model, fieldnames):
        # Take a list of fieldnames and return the fields in the order they are mentioned in folder_model.FIELDS.
        # Loop over FIELDS and not supported_fields(). Upstream should make sure not to update a non-supported field.
        for f in folder_model.FIELDS:
            if f.name in fieldnames:
                yield f.name

    def _set_folder_elem(self, folder_model, field_path, value):
        setfolderfield = create_element('t:SetFolderField')
        set_xml_value(setfolderfield, field_path, version=self.account.version)
        folder = create_element(folder_model.request_tag())
        field_elem = field_path.field.to_xml(value, version=self.account.version)
        set_xml_value(folder, field_elem, version=self.account.version)
        setfolderfield.append(folder)
        return setfolderfield

    def _delete_folder_elem(self, field_path):
        deletefolderfield = create_element('t:DeleteFolderField')
        return set_xml_value(deletefolderfield, field_path, version=self.account.version)

    def _get_folder_update_elems(self, folder, fieldnames):
        folder_model = folder.__class__
        fieldnames_set = set(fieldnames)

        for fieldname in self._sort_fieldnames(folder_model=folder_model, fieldnames=fieldnames_set):
            field = folder_model.get_field_by_fieldname(fieldname)
            if field.is_read_only:
                raise ValueError('%s is a read-only field' % field.name)
            value = field.clean(getattr(folder, field.name), version=self.account.version)  # Make sure the value is OK

            if value is None or (field.is_list and not value):
                # A value of None or [] means we want to remove this field from the item
                if field.is_required or field.is_required_after_save:
                    raise ValueError('%s is a required field and may not be deleted' % field.name)
                for field_path in FieldPath(field=field).expand(version=self.account.version):
                    yield self._delete_folder_elem(field_path=field_path)
                continue

            yield self._set_folder_elem(folder_model=folder_model, field_path=FieldPath(field=field), value=value)

    def get_payload(self, folders):
        from ..folders import BaseFolder, FolderId
        updatefolder = create_element('m:%s' % self.SERVICE_NAME)
        folderchanges = create_element('m:FolderChanges')
        version = self.account.version
        for folder, fieldnames in folders:
            folderchange = create_element('t:FolderChange')
            if not isinstance(folder, (BaseFolder, FolderId)):
                folder = to_item_id(folder, FolderId, version=version)
            set_xml_value(folderchange, folder, version=version)
            updates = create_element('t:Updates')
            for elem in self._get_folder_update_elems(folder=folder, fieldnames=fieldnames):
                updates.append(elem)
            folderchange.append(updates)
            folderchanges.append(folderchange)
        if not len(folderchanges):
            raise ValueError('"folders" must not be empty')
        updatefolder.append(folderchanges)
        return updatefolder

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, folders)
Expand source code
def call(self, folders):
    # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same
    # class as the folder instance it was requested with.
    self.folders = list(folders)  # Convert to a list, in case 'folders' is a generator. We're iterating twice.
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=self.folders))
def get_payload(self, folders)
Expand source code
def get_payload(self, folders):
    from ..folders import BaseFolder, FolderId
    updatefolder = create_element('m:%s' % self.SERVICE_NAME)
    folderchanges = create_element('m:FolderChanges')
    version = self.account.version
    for folder, fieldnames in folders:
        folderchange = create_element('t:FolderChange')
        if not isinstance(folder, (BaseFolder, FolderId)):
            folder = to_item_id(folder, FolderId, version=version)
        set_xml_value(folderchange, folder, version=version)
        updates = create_element('t:Updates')
        for elem in self._get_folder_update_elems(folder=folder, fieldnames=fieldnames):
            updates.append(elem)
        folderchange.append(updates)
        folderchanges.append(folderchange)
    if not len(folderchanges):
        raise ValueError('"folders" must not be empty')
    updatefolder.append(folderchanges)
    return updatefolder

Inherited members

class UpdateItem (*args, **kwargs)
Expand source code
class UpdateItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation"""

    SERVICE_NAME = 'UpdateItem'
    element_container_name = '{%s}Items' % MNS

    def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
             suppress_read_receipts):
        from ..items import CONFLICT_RESOLUTION_CHOICES, MESSAGE_DISPOSITION_CHOICES, \
            SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES, SEND_ONLY
        if conflict_resolution not in CONFLICT_RESOLUTION_CHOICES:
            raise ValueError("'conflict_resolution' %s must be one of %s" % (
                conflict_resolution, CONFLICT_RESOLUTION_CHOICES
            ))
        if message_disposition not in MESSAGE_DISPOSITION_CHOICES:
            raise ValueError("'message_disposition' %s must be one of %s" % (
                message_disposition, MESSAGE_DISPOSITION_CHOICES
            ))
        if send_meeting_invitations_or_cancellations not in SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES:
            raise ValueError("'send_meeting_invitations_or_cancellations' %s must be one of %s" % (
                send_meeting_invitations_or_cancellations, SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES
            ))
        if suppress_read_receipts not in (True, False):
            raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts)
        if message_disposition == SEND_ONLY:
            raise ValueError('Cannot send-only existing objects. Use SendItem service instead')
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=items,
            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 _elems_to_objs(self, elems):
        from ..items import Item
        for elem in elems:
            if isinstance(elem, (Exception, type(None))):
                yield elem
                continue
            yield Item.id_from_xml(elem)

    def _delete_item_elem(self, field_path):
        deleteitemfield = create_element('t:DeleteItemField')
        return set_xml_value(deleteitemfield, field_path, version=self.account.version)

    def _set_item_elem(self, item_model, field_path, value):
        setitemfield = create_element('t:SetItemField')
        set_xml_value(setitemfield, field_path, version=self.account.version)
        item_elem = create_element(item_model.request_tag())
        field_elem = field_path.field.to_xml(value, version=self.account.version)
        set_xml_value(item_elem, field_elem, version=self.account.version)
        setitemfield.append(item_elem)
        return setitemfield

    @staticmethod
    def _sorted_fields(item_model, fieldnames):
        # Take a list of fieldnames and return the (unique) fields in the order they are mentioned in item_class.FIELDS.
        # Checks that all fieldnames are valid.
        unique_fieldnames = list(OrderedDict.fromkeys(fieldnames))  # Make field names unique ,but keep ordering
        # Loop over FIELDS and not supported_fields(). Upstream should make sure not to update a non-supported field.
        for f in item_model.FIELDS:
            if f.name in unique_fieldnames:
                unique_fieldnames.remove(f.name)
                yield f
        if unique_fieldnames:
            raise ValueError("Field name(s) %s are not valid for a '%s' item" % (
                ', '.join("'%s'" % f for f in unique_fieldnames), item_model.__name__))

    def _get_item_update_elems(self, item, fieldnames):
        from ..items import CalendarItem
        fieldnames_copy = list(fieldnames)

        if item.__class__ == CalendarItem:
            # For CalendarItem items where we update 'start' or 'end', we want to update internal timezone fields
            item.clean_timezone_fields(version=self.account.version)  # Possibly also sets timezone values
            for field_name in ('start', 'end'):
                if field_name in fieldnames_copy:
                    tz_field_name = item.tz_field_for_field_name(field_name).name
                    if tz_field_name not in fieldnames_copy:
                        fieldnames_copy.append(tz_field_name)

        for field in self._sorted_fields(item_model=item.__class__, fieldnames=fieldnames_copy):
            if field.is_read_only:
                raise ValueError('%s is a read-only field' % field.name)
            value = self._get_item_value(item, field)
            if value is None or (field.is_list and not value):
                # A value of None or [] means we want to remove this field from the item
                yield from self._get_delete_item_elems(field=field)
            else:
                yield from self._get_set_item_elems(item_model=item.__class__, field=field, value=value)

    def _get_item_value(self, item, field):
        from ..items import CalendarItem
        value = field.clean(getattr(item, field.name), version=self.account.version)  # Make sure the value is OK
        if item.__class__ == CalendarItem:
            # For CalendarItem items where we update 'start' or 'end', we want to send values in the local timezone
            if field.name in ('start', 'end'):
                if type(value) is EWSDate:
                    # EWS always expects a datetime
                    return item.date_to_datetime(field_name=field.name)
                tz_field_name = item.tz_field_for_field_name(field.name).name
                return value.astimezone(getattr(item, tz_field_name))
        return value

    def _get_delete_item_elems(self, field):
        if field.is_required or field.is_required_after_save:
            raise ValueError('%s is a required field and may not be deleted' % field.name)
        for field_path in FieldPath(field=field).expand(version=self.account.version):
            yield self._delete_item_elem(field_path=field_path)

    def _get_set_item_elems(self, item_model, field, value):
        if isinstance(field, IndexedField):
            # Generate either set or delete elements for all combinations of labels and subfields
            supported_labels = field.value_cls.get_field_by_fieldname('label')\
                .supported_choices(version=self.account.version)
            seen_labels = set()
            subfields = field.value_cls.supported_fields(version=self.account.version)
            for v in value:
                seen_labels.add(v.label)
                for subfield in subfields:
                    field_path = FieldPath(field=field, label=v.label, subfield=subfield)
                    subfield_value = getattr(v, subfield.name)
                    if not subfield_value:
                        # Generate delete elements for blank subfield values
                        yield self._delete_item_elem(field_path=field_path)
                    else:
                        # Generate set elements for non-null subfield values
                        yield self._set_item_elem(
                            item_model=item_model,
                            field_path=field_path,
                            value=field.value_cls(**{'label': v.label, subfield.name: subfield_value}),
                        )
                # Generate delete elements for all subfields of all labels not mentioned in the list of values
                for label in (label for label in supported_labels if label not in seen_labels):
                    for subfield in subfields:
                        yield self._delete_item_elem(field_path=FieldPath(field=field, label=label, subfield=subfield))
        else:
            yield self._set_item_elem(item_model=item_model, field_path=FieldPath(field=field), value=value)

    def get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
                    suppress_read_receipts):
        # Takes a list of (Item, fieldnames) tuples where 'Item' is a instance of a subclass of Item and 'fieldnames'
        # are the attribute names that were updated. Returns the XML for an UpdateItem call.
        # an UpdateItem request.
        if self.account.version.build >= EXCHANGE_2013_SP1:
            updateitem = create_element(
                'm:%s' % self.SERVICE_NAME,
                attrs=OrderedDict([
                    ('ConflictResolution', conflict_resolution),
                    ('MessageDisposition', message_disposition),
                    ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
                    ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'),
                ])
            )
        else:
            updateitem = create_element(
                'm:%s' % self.SERVICE_NAME,
                attrs=OrderedDict([
                    ('ConflictResolution', conflict_resolution),
                    ('MessageDisposition', message_disposition),
                    ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
                ])
            )
        itemchanges = create_element('m:ItemChanges')
        version = self.account.version
        for item, fieldnames in items:
            if not item.account:
                item.account = self.account
            if not fieldnames:
                raise ValueError('"fieldnames" must not be empty')
            itemchange = create_element('t:ItemChange')
            set_xml_value(itemchange, to_item_id(item, ItemId, version=version), version=version)
            updates = create_element('t:Updates')
            for elem in self._get_item_update_elems(item=item, fieldnames=fieldnames):
                updates.append(elem)
            itemchange.append(updates)
            itemchanges.append(itemchange)
        if not len(itemchanges):
            raise ValueError('"items" must not be empty')
        updateitem.append(itemchanges)
        return updateitem

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, suppress_read_receipts)
Expand source code
def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
         suppress_read_receipts):
    from ..items import CONFLICT_RESOLUTION_CHOICES, MESSAGE_DISPOSITION_CHOICES, \
        SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES, SEND_ONLY
    if conflict_resolution not in CONFLICT_RESOLUTION_CHOICES:
        raise ValueError("'conflict_resolution' %s must be one of %s" % (
            conflict_resolution, CONFLICT_RESOLUTION_CHOICES
        ))
    if message_disposition not in MESSAGE_DISPOSITION_CHOICES:
        raise ValueError("'message_disposition' %s must be one of %s" % (
            message_disposition, MESSAGE_DISPOSITION_CHOICES
        ))
    if send_meeting_invitations_or_cancellations not in SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES:
        raise ValueError("'send_meeting_invitations_or_cancellations' %s must be one of %s" % (
            send_meeting_invitations_or_cancellations, SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES
        ))
    if suppress_read_receipts not in (True, False):
        raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts)
    if message_disposition == SEND_ONLY:
        raise ValueError('Cannot send-only existing objects. Use SendItem service instead')
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload,
        items=items,
        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 get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, suppress_read_receipts)
Expand source code
def get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
                suppress_read_receipts):
    # Takes a list of (Item, fieldnames) tuples where 'Item' is a instance of a subclass of Item and 'fieldnames'
    # are the attribute names that were updated. Returns the XML for an UpdateItem call.
    # an UpdateItem request.
    if self.account.version.build >= EXCHANGE_2013_SP1:
        updateitem = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=OrderedDict([
                ('ConflictResolution', conflict_resolution),
                ('MessageDisposition', message_disposition),
                ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
                ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'),
            ])
        )
    else:
        updateitem = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=OrderedDict([
                ('ConflictResolution', conflict_resolution),
                ('MessageDisposition', message_disposition),
                ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
            ])
        )
    itemchanges = create_element('m:ItemChanges')
    version = self.account.version
    for item, fieldnames in items:
        if not item.account:
            item.account = self.account
        if not fieldnames:
            raise ValueError('"fieldnames" must not be empty')
        itemchange = create_element('t:ItemChange')
        set_xml_value(itemchange, to_item_id(item, ItemId, version=version), version=version)
        updates = create_element('t:Updates')
        for elem in self._get_item_update_elems(item=item, fieldnames=fieldnames):
            updates.append(elem)
        itemchange.append(updates)
        itemchanges.append(itemchange)
    if not len(itemchanges):
        raise ValueError('"items" must not be empty')
    updateitem.append(itemchanges)
    return updateitem

Inherited members

class UpdateUserConfiguration (*args, **kwargs)
Expand source code
class UpdateUserConfiguration(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateuserconfiguration-operation
    """

    SERVICE_NAME = 'UpdateUserConfiguration'
    returns_elements = False

    def call(self, user_configuration):
        return self._get_elements(payload=self.get_payload(user_configuration=user_configuration))

    def get_payload(self, user_configuration):
        updateuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(updateuserconfiguration, user_configuration, version=self.account.version)
        return updateuserconfiguration

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, user_configuration)
Expand source code
def call(self, user_configuration):
    return self._get_elements(payload=self.get_payload(user_configuration=user_configuration))
def get_payload(self, user_configuration)
Expand source code
def get_payload(self, user_configuration):
    updateuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(updateuserconfiguration, user_configuration, version=self.account.version)
    return updateuserconfiguration

Inherited members

class UploadItems (*args, **kwargs)
Expand source code
class UploadItems(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/uploaditems-operation
    """

    SERVICE_NAME = 'UploadItems'
    element_container_name = '{%s}ItemId' % MNS

    def call(self, items):
        # _pool_requests expects 'items', not 'data'
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items))

    def get_payload(self, items):
        """Upload given items to given account.

        'items' is an iterable of tuples where the first element is a Folder instance representing the ParentFolder
        that the item will be placed in and the second element is a tuple containing an optional ItemId, an optional
        Item.is_associated boolean, and a Data string returned from an ExportItems.
        call.

        :param items:
        """
        uploaditems = create_element('m:%s' % self.SERVICE_NAME)
        itemselement = create_element('m:Items')
        uploaditems.append(itemselement)
        for parent_folder, (item_id, is_associated, data_str) in items:
            # TODO: The full spec also allows the "UpdateOrCreate" create action.
            item = create_element('t:Item', attrs=dict(CreateAction='Update' if item_id else 'CreateNew'))
            if is_associated is not None:
                item.set('IsAssociated', 'true' if is_associated else 'false')
            parentfolderid = ParentFolderId(parent_folder.id, parent_folder.changekey)
            set_xml_value(item, parentfolderid, version=self.account.version)
            if item_id:
                itemid = to_item_id(item_id, ItemId, version=self.account.version)
                set_xml_value(item, itemid, version=self.account.version)
            add_xml_child(item, 't:Data', data_str)
            itemselement.append(item)
        return uploaditems

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield elem.get(ItemId.ID_ATTR), elem.get(ItemId.CHANGEKEY_ATTR)

    @classmethod
    def _get_elements_in_container(cls, container):
        return [container]

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, items)
Expand source code
def call(self, items):
    # _pool_requests expects 'items', not 'data'
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items))
def get_payload(self, items)

Upload given items to given account.

'items' is an iterable of tuples where the first element is a Folder instance representing the ParentFolder that the item will be placed in and the second element is a tuple containing an optional ItemId, an optional Item.is_associated boolean, and a Data string returned from an ExportItems. call.

:param items:

Expand source code
def get_payload(self, items):
    """Upload given items to given account.

    'items' is an iterable of tuples where the first element is a Folder instance representing the ParentFolder
    that the item will be placed in and the second element is a tuple containing an optional ItemId, an optional
    Item.is_associated boolean, and a Data string returned from an ExportItems.
    call.

    :param items:
    """
    uploaditems = create_element('m:%s' % self.SERVICE_NAME)
    itemselement = create_element('m:Items')
    uploaditems.append(itemselement)
    for parent_folder, (item_id, is_associated, data_str) in items:
        # TODO: The full spec also allows the "UpdateOrCreate" create action.
        item = create_element('t:Item', attrs=dict(CreateAction='Update' if item_id else 'CreateNew'))
        if is_associated is not None:
            item.set('IsAssociated', 'true' if is_associated else 'false')
        parentfolderid = ParentFolderId(parent_folder.id, parent_folder.changekey)
        set_xml_value(item, parentfolderid, version=self.account.version)
        if item_id:
            itemid = to_item_id(item_id, ItemId, version=self.account.version)
            set_xml_value(item, itemid, version=self.account.version)
        add_xml_child(item, 't:Data', data_str)
        itemselement.append(item)
    return uploaditems

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/mark_as_junk.html000066400000000000000000000321641414601472700243650ustar00rootroot00000000000000 exchangelib.services.mark_as_junk API documentation

Module exchangelib.services.mark_as_junk

Expand source code
from .common import EWSAccountService, create_item_ids_element
from ..properties import MovedItemId
from ..util import create_element


class MarkAsJunk(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/markasjunk-operation"""

    SERVICE_NAME = 'MarkAsJunk'

    def call(self, items, is_junk, move_item):
        return self._elems_to_objs(
            self._chunked_get_elements(self.get_payload, items=items, is_junk=is_junk, move_item=move_item)
        )

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, (Exception, type(None))):
                yield elem
                continue
            yield MovedItemId.id_from_xml(elem)

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(MovedItemId.response_tag())

    def get_payload(self, items, is_junk, move_item):
        # Takes a list of items and returns either success or raises an error message
        mark_as_junk = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=dict(IsJunk='true' if is_junk else 'false', MoveItem='true' if move_item else 'false')
        )
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        mark_as_junk.append(item_ids)
        return mark_as_junk

Classes

class MarkAsJunk (*args, **kwargs)
Expand source code
class MarkAsJunk(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/markasjunk-operation"""

    SERVICE_NAME = 'MarkAsJunk'

    def call(self, items, is_junk, move_item):
        return self._elems_to_objs(
            self._chunked_get_elements(self.get_payload, items=items, is_junk=is_junk, move_item=move_item)
        )

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, (Exception, type(None))):
                yield elem
                continue
            yield MovedItemId.id_from_xml(elem)

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(MovedItemId.response_tag())

    def get_payload(self, items, is_junk, move_item):
        # Takes a list of items and returns either success or raises an error message
        mark_as_junk = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=dict(IsJunk='true' if is_junk else 'false', MoveItem='true' if move_item else 'false')
        )
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        mark_as_junk.append(item_ids)
        return mark_as_junk

Ancestors

Class variables

var SERVICE_NAME

Methods

def call(self, items, is_junk, move_item)
Expand source code
def call(self, items, is_junk, move_item):
    return self._elems_to_objs(
        self._chunked_get_elements(self.get_payload, items=items, is_junk=is_junk, move_item=move_item)
    )
def get_payload(self, items, is_junk, move_item)
Expand source code
def get_payload(self, items, is_junk, move_item):
    # Takes a list of items and returns either success or raises an error message
    mark_as_junk = create_element(
        'm:%s' % self.SERVICE_NAME,
        attrs=dict(IsJunk='true' if is_junk else 'false', MoveItem='true' if move_item else 'false')
    )
    item_ids = create_item_ids_element(items=items, version=self.account.version)
    mark_as_junk.append(item_ids)
    return mark_as_junk

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/move_folder.html000066400000000000000000000341141414601472700242170ustar00rootroot00000000000000 exchangelib.services.move_folder API documentation

Module exchangelib.services.move_folder

Expand source code
from .common import EWSAccountService, create_folder_ids_element
from ..util import create_element, set_xml_value, MNS


class MoveFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/movefolder-operation"""

    SERVICE_NAME = "MoveFolder"
    element_container_name = '{%s}Folders' % MNS

    def call(self, folders, to_folder):
        from ..folders import BaseFolder, FolderId
        if not isinstance(to_folder, (BaseFolder, FolderId)):
            raise ValueError("'to_folder' %r must be a Folder or FolderId instance" % to_folder)
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=folders, to_folder=to_folder))

    def _elems_to_objs(self, elems):
        from ..folders import FolderId
        for elem in elems:
            if isinstance(elem, (Exception, type(None))):
                yield elem
                continue
            yield FolderId.from_xml(elem=elem.find(FolderId.response_tag()), account=self.account)

    def get_payload(self, folders, to_folder):
        # Takes a list of folders and returns their new folder IDs
        movefolder = create_element('m:%s' % self.SERVICE_NAME)
        tofolderid = create_element('m:ToFolderId')
        set_xml_value(tofolderid, to_folder, version=self.account.version)
        movefolder.append(tofolderid)
        folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
        movefolder.append(folder_ids)
        return movefolder

Classes

class MoveFolder (*args, **kwargs)
Expand source code
class MoveFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/movefolder-operation"""

    SERVICE_NAME = "MoveFolder"
    element_container_name = '{%s}Folders' % MNS

    def call(self, folders, to_folder):
        from ..folders import BaseFolder, FolderId
        if not isinstance(to_folder, (BaseFolder, FolderId)):
            raise ValueError("'to_folder' %r must be a Folder or FolderId instance" % to_folder)
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=folders, to_folder=to_folder))

    def _elems_to_objs(self, elems):
        from ..folders import FolderId
        for elem in elems:
            if isinstance(elem, (Exception, type(None))):
                yield elem
                continue
            yield FolderId.from_xml(elem=elem.find(FolderId.response_tag()), account=self.account)

    def get_payload(self, folders, to_folder):
        # Takes a list of folders and returns their new folder IDs
        movefolder = create_element('m:%s' % self.SERVICE_NAME)
        tofolderid = create_element('m:ToFolderId')
        set_xml_value(tofolderid, to_folder, version=self.account.version)
        movefolder.append(tofolderid)
        folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
        movefolder.append(folder_ids)
        return movefolder

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, folders, to_folder)
Expand source code
def call(self, folders, to_folder):
    from ..folders import BaseFolder, FolderId
    if not isinstance(to_folder, (BaseFolder, FolderId)):
        raise ValueError("'to_folder' %r must be a Folder or FolderId instance" % to_folder)
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=folders, to_folder=to_folder))
def get_payload(self, folders, to_folder)
Expand source code
def get_payload(self, folders, to_folder):
    # Takes a list of folders and returns their new folder IDs
    movefolder = create_element('m:%s' % self.SERVICE_NAME)
    tofolderid = create_element('m:ToFolderId')
    set_xml_value(tofolderid, to_folder, version=self.account.version)
    movefolder.append(tofolderid)
    folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version)
    movefolder.append(folder_ids)
    return movefolder

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/move_item.html000066400000000000000000000335751414601472700237140ustar00rootroot00000000000000 exchangelib.services.move_item API documentation

Module exchangelib.services.move_item

Expand source code
from .common import EWSAccountService, create_item_ids_element
from ..util import create_element, set_xml_value, MNS


class MoveItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/moveitem-operation"""

    SERVICE_NAME = 'MoveItem'
    element_container_name = '{%s}Items' % MNS

    def call(self, items, to_folder):
        from ..folders import BaseFolder, FolderId
        if not isinstance(to_folder, (BaseFolder, FolderId)):
            raise ValueError("'to_folder' %r must be a Folder or FolderId instance" % to_folder)
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, to_folder=to_folder))

    def _elems_to_objs(self, elems):
        from ..items import Item
        for elem in elems:
            if isinstance(elem, (Exception, type(None))):
                yield elem
                continue
            yield Item.id_from_xml(elem)

    def get_payload(self, items, to_folder):
        # Takes a list of items and returns their new item IDs
        moveitem = create_element('m:%s' % self.SERVICE_NAME)
        tofolderid = create_element('m:ToFolderId')
        set_xml_value(tofolderid, to_folder, version=self.account.version)
        moveitem.append(tofolderid)
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        moveitem.append(item_ids)
        return moveitem

Classes

class MoveItem (*args, **kwargs)
Expand source code
class MoveItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/moveitem-operation"""

    SERVICE_NAME = 'MoveItem'
    element_container_name = '{%s}Items' % MNS

    def call(self, items, to_folder):
        from ..folders import BaseFolder, FolderId
        if not isinstance(to_folder, (BaseFolder, FolderId)):
            raise ValueError("'to_folder' %r must be a Folder or FolderId instance" % to_folder)
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, to_folder=to_folder))

    def _elems_to_objs(self, elems):
        from ..items import Item
        for elem in elems:
            if isinstance(elem, (Exception, type(None))):
                yield elem
                continue
            yield Item.id_from_xml(elem)

    def get_payload(self, items, to_folder):
        # Takes a list of items and returns their new item IDs
        moveitem = create_element('m:%s' % self.SERVICE_NAME)
        tofolderid = create_element('m:ToFolderId')
        set_xml_value(tofolderid, to_folder, version=self.account.version)
        moveitem.append(tofolderid)
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        moveitem.append(item_ids)
        return moveitem

Ancestors

Subclasses

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, items, to_folder)
Expand source code
def call(self, items, to_folder):
    from ..folders import BaseFolder, FolderId
    if not isinstance(to_folder, (BaseFolder, FolderId)):
        raise ValueError("'to_folder' %r must be a Folder or FolderId instance" % to_folder)
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, to_folder=to_folder))
def get_payload(self, items, to_folder)
Expand source code
def get_payload(self, items, to_folder):
    # Takes a list of items and returns their new item IDs
    moveitem = create_element('m:%s' % self.SERVICE_NAME)
    tofolderid = create_element('m:ToFolderId')
    set_xml_value(tofolderid, to_folder, version=self.account.version)
    moveitem.append(tofolderid)
    item_ids = create_item_ids_element(items=items, version=self.account.version)
    moveitem.append(item_ids)
    return moveitem

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/resolve_names.html000066400000000000000000000541101414601472700245560ustar00rootroot00000000000000 exchangelib.services.resolve_names API documentation

Module exchangelib.services.resolve_names

Expand source code
import logging

from .common import EWSService
from ..errors import ErrorNameResolutionNoResults, ErrorNameResolutionMultipleResults
from ..properties import Mailbox
from ..util import create_element, set_xml_value, add_xml_child, MNS
from ..version import EXCHANGE_2010_SP2

log = logging.getLogger(__name__)


class ResolveNames(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/resolvenames-operation"""

    SERVICE_NAME = 'ResolveNames'
    element_container_name = '{%s}ResolutionSet' % MNS
    ERRORS_TO_CATCH_IN_RESPONSE = ErrorNameResolutionNoResults
    WARNINGS_TO_IGNORE_IN_RESPONSE = ErrorNameResolutionMultipleResults
    # Note: paging information is returned as attrs on the 'ResolutionSet' element, but this service does not
    # support the 'IndexedPageItemView' element, so it's not really a paging service. According to docs, at most
    # 100 candidates are returned for a lookup.
    supports_paging = False

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.return_full_contact_data = False  # A hack to communicate parsing args to _elems_to_objs()

    def call(self, unresolved_entries, parent_folders=None, return_full_contact_data=False, search_scope=None,
             contact_data_shape=None):
        from ..items import SHAPE_CHOICES, SEARCH_SCOPE_CHOICES
        if self.chunk_size > 100:
            log.warning(
                'Chunk size %s is dangerously high. %s supports returning at most 100 candidates for a lookup',
                self.chunk_size, self.SERVICE_NAME
            )
        if search_scope and search_scope not in SEARCH_SCOPE_CHOICES:
            raise ValueError("'search_scope' %s must be one if %s" % (search_scope, SEARCH_SCOPE_CHOICES))
        if contact_data_shape and contact_data_shape not in SHAPE_CHOICES:
            raise ValueError("'shape' %s must be one if %s" % (contact_data_shape, SHAPE_CHOICES))
        self.return_full_contact_data = return_full_contact_data
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=unresolved_entries,
            parent_folders=parent_folders,
            return_full_contact_data=return_full_contact_data,
            search_scope=search_scope,
            contact_data_shape=contact_data_shape,
        ))

    def _elems_to_objs(self, elems):
        from ..items import Contact
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            if self.return_full_contact_data:
                mailbox_elem = elem.find(Mailbox.response_tag())
                contact_elem = elem.find(Contact.response_tag())
                yield (
                    None if mailbox_elem is None else Mailbox.from_xml(elem=mailbox_elem, account=None),
                    None if contact_elem is None else Contact.from_xml(elem=contact_elem, account=None),
                )
            else:
                yield Mailbox.from_xml(elem=elem.find(Mailbox.response_tag()), account=None)

    def get_payload(self, unresolved_entries, parent_folders, return_full_contact_data, search_scope,
                    contact_data_shape):
        payload = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=dict(ReturnFullContactData='true' if return_full_contact_data else 'false'),
        )
        if search_scope:
            payload.set('SearchScope', search_scope)
        if contact_data_shape:
            if self.protocol.version.build < EXCHANGE_2010_SP2:
                raise NotImplementedError(
                    "'contact_data_shape' is only supported for Exchange 2010 SP2 servers and later")
            payload.set('ContactDataShape', contact_data_shape)
        if parent_folders:
            parentfolderids = create_element('m:ParentFolderIds')
            set_xml_value(parentfolderids, parent_folders, version=self.protocol.version)
        for entry in unresolved_entries:
            add_xml_child(payload, 'm:UnresolvedEntry', entry)
        if not len(payload):
            raise ValueError('"unresolved_entries" must not be empty')
        return payload

Classes

class ResolveNames (*args, **kwargs)
Expand source code
class ResolveNames(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/resolvenames-operation"""

    SERVICE_NAME = 'ResolveNames'
    element_container_name = '{%s}ResolutionSet' % MNS
    ERRORS_TO_CATCH_IN_RESPONSE = ErrorNameResolutionNoResults
    WARNINGS_TO_IGNORE_IN_RESPONSE = ErrorNameResolutionMultipleResults
    # Note: paging information is returned as attrs on the 'ResolutionSet' element, but this service does not
    # support the 'IndexedPageItemView' element, so it's not really a paging service. According to docs, at most
    # 100 candidates are returned for a lookup.
    supports_paging = False

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.return_full_contact_data = False  # A hack to communicate parsing args to _elems_to_objs()

    def call(self, unresolved_entries, parent_folders=None, return_full_contact_data=False, search_scope=None,
             contact_data_shape=None):
        from ..items import SHAPE_CHOICES, SEARCH_SCOPE_CHOICES
        if self.chunk_size > 100:
            log.warning(
                'Chunk size %s is dangerously high. %s supports returning at most 100 candidates for a lookup',
                self.chunk_size, self.SERVICE_NAME
            )
        if search_scope and search_scope not in SEARCH_SCOPE_CHOICES:
            raise ValueError("'search_scope' %s must be one if %s" % (search_scope, SEARCH_SCOPE_CHOICES))
        if contact_data_shape and contact_data_shape not in SHAPE_CHOICES:
            raise ValueError("'shape' %s must be one if %s" % (contact_data_shape, SHAPE_CHOICES))
        self.return_full_contact_data = return_full_contact_data
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=unresolved_entries,
            parent_folders=parent_folders,
            return_full_contact_data=return_full_contact_data,
            search_scope=search_scope,
            contact_data_shape=contact_data_shape,
        ))

    def _elems_to_objs(self, elems):
        from ..items import Contact
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            if self.return_full_contact_data:
                mailbox_elem = elem.find(Mailbox.response_tag())
                contact_elem = elem.find(Contact.response_tag())
                yield (
                    None if mailbox_elem is None else Mailbox.from_xml(elem=mailbox_elem, account=None),
                    None if contact_elem is None else Contact.from_xml(elem=contact_elem, account=None),
                )
            else:
                yield Mailbox.from_xml(elem=elem.find(Mailbox.response_tag()), account=None)

    def get_payload(self, unresolved_entries, parent_folders, return_full_contact_data, search_scope,
                    contact_data_shape):
        payload = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=dict(ReturnFullContactData='true' if return_full_contact_data else 'false'),
        )
        if search_scope:
            payload.set('SearchScope', search_scope)
        if contact_data_shape:
            if self.protocol.version.build < EXCHANGE_2010_SP2:
                raise NotImplementedError(
                    "'contact_data_shape' is only supported for Exchange 2010 SP2 servers and later")
            payload.set('ContactDataShape', contact_data_shape)
        if parent_folders:
            parentfolderids = create_element('m:ParentFolderIds')
            set_xml_value(parentfolderids, parent_folders, version=self.protocol.version)
        for entry in unresolved_entries:
            add_xml_child(payload, 'm:UnresolvedEntry', entry)
        if not len(payload):
            raise ValueError('"unresolved_entries" must not be empty')
        return payload

Ancestors

Class variables

var ERRORS_TO_CATCH_IN_RESPONSE

Global error type within this module.

var SERVICE_NAME
var WARNINGS_TO_IGNORE_IN_RESPONSE

Global error type within this module.

var element_container_name
var supports_paging

Methods

def call(self, unresolved_entries, parent_folders=None, return_full_contact_data=False, search_scope=None, contact_data_shape=None)
Expand source code
def call(self, unresolved_entries, parent_folders=None, return_full_contact_data=False, search_scope=None,
         contact_data_shape=None):
    from ..items import SHAPE_CHOICES, SEARCH_SCOPE_CHOICES
    if self.chunk_size > 100:
        log.warning(
            'Chunk size %s is dangerously high. %s supports returning at most 100 candidates for a lookup',
            self.chunk_size, self.SERVICE_NAME
        )
    if search_scope and search_scope not in SEARCH_SCOPE_CHOICES:
        raise ValueError("'search_scope' %s must be one if %s" % (search_scope, SEARCH_SCOPE_CHOICES))
    if contact_data_shape and contact_data_shape not in SHAPE_CHOICES:
        raise ValueError("'shape' %s must be one if %s" % (contact_data_shape, SHAPE_CHOICES))
    self.return_full_contact_data = return_full_contact_data
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload,
        items=unresolved_entries,
        parent_folders=parent_folders,
        return_full_contact_data=return_full_contact_data,
        search_scope=search_scope,
        contact_data_shape=contact_data_shape,
    ))
def get_payload(self, unresolved_entries, parent_folders, return_full_contact_data, search_scope, contact_data_shape)
Expand source code
def get_payload(self, unresolved_entries, parent_folders, return_full_contact_data, search_scope,
                contact_data_shape):
    payload = create_element(
        'm:%s' % self.SERVICE_NAME,
        attrs=dict(ReturnFullContactData='true' if return_full_contact_data else 'false'),
    )
    if search_scope:
        payload.set('SearchScope', search_scope)
    if contact_data_shape:
        if self.protocol.version.build < EXCHANGE_2010_SP2:
            raise NotImplementedError(
                "'contact_data_shape' is only supported for Exchange 2010 SP2 servers and later")
        payload.set('ContactDataShape', contact_data_shape)
    if parent_folders:
        parentfolderids = create_element('m:ParentFolderIds')
        set_xml_value(parentfolderids, parent_folders, version=self.protocol.version)
    for entry in unresolved_entries:
        add_xml_child(payload, 'm:UnresolvedEntry', entry)
    if not len(payload):
        raise ValueError('"unresolved_entries" must not be empty')
    return payload

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/send_item.html000066400000000000000000000333121414601472700236640ustar00rootroot00000000000000 exchangelib.services.send_item API documentation

Module exchangelib.services.send_item

Expand source code
from .common import EWSAccountService, create_item_ids_element
from ..util import create_element, set_xml_value


class SendItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/senditem-operation"""

    SERVICE_NAME = 'SendItem'
    returns_elements = False

    def call(self, items, saved_item_folder):
        from ..folders import BaseFolder, FolderId
        if saved_item_folder and not isinstance(saved_item_folder, (BaseFolder, FolderId)):
            raise ValueError("'saved_item_folder' %r must be a Folder or FolderId instance" % saved_item_folder)
        return self._chunked_get_elements(self.get_payload, items=items, saved_item_folder=saved_item_folder)

    def get_payload(self, items, saved_item_folder):
        senditem = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=dict(SaveItemToFolder='true' if saved_item_folder else 'false'),
        )
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        senditem.append(item_ids)
        if saved_item_folder:
            saveditemfolderid = create_element('m:SavedItemFolderId')
            set_xml_value(saveditemfolderid, saved_item_folder, version=self.account.version)
            senditem.append(saveditemfolderid)
        return senditem

Classes

class SendItem (*args, **kwargs)
Expand source code
class SendItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/senditem-operation"""

    SERVICE_NAME = 'SendItem'
    returns_elements = False

    def call(self, items, saved_item_folder):
        from ..folders import BaseFolder, FolderId
        if saved_item_folder and not isinstance(saved_item_folder, (BaseFolder, FolderId)):
            raise ValueError("'saved_item_folder' %r must be a Folder or FolderId instance" % saved_item_folder)
        return self._chunked_get_elements(self.get_payload, items=items, saved_item_folder=saved_item_folder)

    def get_payload(self, items, saved_item_folder):
        senditem = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=dict(SaveItemToFolder='true' if saved_item_folder else 'false'),
        )
        item_ids = create_item_ids_element(items=items, version=self.account.version)
        senditem.append(item_ids)
        if saved_item_folder:
            saveditemfolderid = create_element('m:SavedItemFolderId')
            set_xml_value(saveditemfolderid, saved_item_folder, version=self.account.version)
            senditem.append(saveditemfolderid)
        return senditem

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, items, saved_item_folder)
Expand source code
def call(self, items, saved_item_folder):
    from ..folders import BaseFolder, FolderId
    if saved_item_folder and not isinstance(saved_item_folder, (BaseFolder, FolderId)):
        raise ValueError("'saved_item_folder' %r must be a Folder or FolderId instance" % saved_item_folder)
    return self._chunked_get_elements(self.get_payload, items=items, saved_item_folder=saved_item_folder)
def get_payload(self, items, saved_item_folder)
Expand source code
def get_payload(self, items, saved_item_folder):
    senditem = create_element(
        'm:%s' % self.SERVICE_NAME,
        attrs=dict(SaveItemToFolder='true' if saved_item_folder else 'false'),
    )
    item_ids = create_item_ids_element(items=items, version=self.account.version)
    senditem.append(item_ids)
    if saved_item_folder:
        saveditemfolderid = create_element('m:SavedItemFolderId')
        set_xml_value(saveditemfolderid, saved_item_folder, version=self.account.version)
        senditem.append(saveditemfolderid)
    return senditem

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/send_notification.html000066400000000000000000000264431414601472700254230ustar00rootroot00000000000000 exchangelib.services.send_notification API documentation

Module exchangelib.services.send_notification

Expand source code
from .common import EWSService
from ..properties import Notification
from ..util import MNS


class SendNotification(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/sendnotification

    This is not an actual EWS service you can call. We only use it to parse the XML body of push notifications.
    """

    SERVICE_NAME = 'SendNotification'

    def call(self):
        raise NotImplementedError()

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Notification.from_xml(elem=elem, account=None)

    @classmethod
    def _response_tag(cls):
        """Return the name of the element containing the service response."""
        return '{%s}%s' % (MNS, cls.SERVICE_NAME)

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(Notification.response_tag())

Classes

class SendNotification (protocol, chunk_size=None, timeout=None)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/sendnotification

This is not an actual EWS service you can call. We only use it to parse the XML body of push notifications.

Expand source code
class SendNotification(EWSService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/sendnotification

    This is not an actual EWS service you can call. We only use it to parse the XML body of push notifications.
    """

    SERVICE_NAME = 'SendNotification'

    def call(self):
        raise NotImplementedError()

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield Notification.from_xml(elem=elem, account=None)

    @classmethod
    def _response_tag(cls):
        """Return the name of the element containing the service response."""
        return '{%s}%s' % (MNS, cls.SERVICE_NAME)

    @classmethod
    def _get_elements_in_container(cls, container):
        return container.findall(Notification.response_tag())

Ancestors

Class variables

var SERVICE_NAME

Methods

def call(self)
Expand source code
def call(self):
    raise NotImplementedError()

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/set_user_oof_settings.html000066400000000000000000000342321414601472700263330ustar00rootroot00000000000000 exchangelib.services.set_user_oof_settings API documentation

Module exchangelib.services.set_user_oof_settings

Expand source code
from .common import EWSAccountService
from ..properties import AvailabilityMailbox, Mailbox
from ..settings import OofSettings
from ..util import create_element, set_xml_value, MNS


class SetUserOofSettings(EWSAccountService):
    """Set automatic replies for the specified mailbox.
    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/setuseroofsettings-operation
    """

    SERVICE_NAME = 'SetUserOofSettings'
    returns_elements = False

    def call(self, oof_settings, mailbox):
        if not isinstance(oof_settings, OofSettings):
            raise ValueError("'oof_settings' %r must be an OofSettings instance" % oof_settings)
        if not isinstance(mailbox, Mailbox):
            raise ValueError("'mailbox' %r must be an Mailbox instance" % mailbox)
        return self._get_elements(payload=self.get_payload(oof_settings=oof_settings, mailbox=mailbox))

    def get_payload(self, oof_settings, mailbox):
        payload = create_element('m:%sRequest' % self.SERVICE_NAME)
        set_xml_value(payload, AvailabilityMailbox.from_mailbox(mailbox), version=self.account.version)
        set_xml_value(payload, oof_settings, version=self.account.version)
        return payload

    def _get_element_container(self, message, name=None):
        message = message.find(self._response_message_tag())
        return super()._get_element_container(message=message, name=name)

    @classmethod
    def _response_message_tag(cls):
        return '{%s}ResponseMessage' % MNS

Classes

class SetUserOofSettings (*args, **kwargs)
Expand source code
class SetUserOofSettings(EWSAccountService):
    """Set automatic replies for the specified mailbox.
    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/setuseroofsettings-operation
    """

    SERVICE_NAME = 'SetUserOofSettings'
    returns_elements = False

    def call(self, oof_settings, mailbox):
        if not isinstance(oof_settings, OofSettings):
            raise ValueError("'oof_settings' %r must be an OofSettings instance" % oof_settings)
        if not isinstance(mailbox, Mailbox):
            raise ValueError("'mailbox' %r must be an Mailbox instance" % mailbox)
        return self._get_elements(payload=self.get_payload(oof_settings=oof_settings, mailbox=mailbox))

    def get_payload(self, oof_settings, mailbox):
        payload = create_element('m:%sRequest' % self.SERVICE_NAME)
        set_xml_value(payload, AvailabilityMailbox.from_mailbox(mailbox), version=self.account.version)
        set_xml_value(payload, oof_settings, version=self.account.version)
        return payload

    def _get_element_container(self, message, name=None):
        message = message.find(self._response_message_tag())
        return super()._get_element_container(message=message, name=name)

    @classmethod
    def _response_message_tag(cls):
        return '{%s}ResponseMessage' % MNS

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, oof_settings, mailbox)
Expand source code
def call(self, oof_settings, mailbox):
    if not isinstance(oof_settings, OofSettings):
        raise ValueError("'oof_settings' %r must be an OofSettings instance" % oof_settings)
    if not isinstance(mailbox, Mailbox):
        raise ValueError("'mailbox' %r must be an Mailbox instance" % mailbox)
    return self._get_elements(payload=self.get_payload(oof_settings=oof_settings, mailbox=mailbox))
def get_payload(self, oof_settings, mailbox)
Expand source code
def get_payload(self, oof_settings, mailbox):
    payload = create_element('m:%sRequest' % self.SERVICE_NAME)
    set_xml_value(payload, AvailabilityMailbox.from_mailbox(mailbox), version=self.account.version)
    set_xml_value(payload, oof_settings, version=self.account.version)
    return payload

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/subscribe.html000066400000000000000000001012171414601472700236760ustar00rootroot00000000000000 exchangelib.services.subscribe API documentation

Module exchangelib.services.subscribe

The 'Subscribe' service has two different modes, pull and push, with different signatures. Implement as two distinct classes.

Expand source code
"""The 'Subscribe' service has two different modes, pull and push, with different signatures. Implement as two distinct
classes.
"""
import abc

from .common import EWSAccountService, create_folder_ids_element, add_xml_child
from ..util import create_element, MNS


class Subscribe(EWSAccountService, metaclass=abc.ABCMeta):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/subscribe-operation"""

    SERVICE_NAME = 'Subscribe'
    EVENT_TYPES = (
        'CopiedEvent',
        'CreatedEvent',
        'DeletedEvent',
        'ModifiedEvent',
        'MovedEvent',
        'NewMailEvent',
        'FreeBusyChangedEvent',
    )
    subscription_request_elem_tag = None

    def _partial_call(self, payload_func, folders, event_types, **kwargs):
        if set(event_types) - set(self.EVENT_TYPES):
            raise ValueError("'event_types' values must consist of values in %s" % str(self.EVENT_TYPES))
        return self._elems_to_objs(self._get_elements(
            payload=payload_func(folders=folders, event_types=event_types, **kwargs)
        ))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            subscription_elem, watermark_elem = elem
            yield subscription_elem.text, watermark_elem.text

    @classmethod
    def _get_elements_in_container(cls, container):
        return [(container.find('{%s}SubscriptionId' % MNS), container.find('{%s}Watermark' % MNS))]

    def _partial_payload(self, folders, event_types):
        request_elem = create_element(self.subscription_request_elem_tag)
        folder_ids = create_folder_ids_element(tag='t:FolderIds', folders=folders, version=self.account.version)
        request_elem.append(folder_ids)
        event_types_elem = create_element('t:EventTypes')
        for event_type in event_types:
            add_xml_child(event_types_elem, 't:EventType', event_type)
        if not len(event_types_elem):
            raise ValueError('"event_types" must not be empty')
        request_elem.append(event_types_elem)
        return request_elem


class SubscribeToPull(Subscribe):
    subscription_request_elem_tag = 'm:PullSubscriptionRequest'

    def call(self, folders, event_types, watermark, timeout):
        yield from self._partial_call(
            payload_func=self.get_payload, folders=folders, event_types=event_types, timeout=timeout,
            watermark=watermark,
        )

    def get_payload(self, folders, event_types, watermark, timeout):
        subscribe = create_element('m:%s' % self.SERVICE_NAME)
        request_elem = self._partial_payload(folders=folders, event_types=event_types)
        if watermark:
            add_xml_child(request_elem, 'm:Watermark', watermark)
        add_xml_child(request_elem, 't:Timeout', timeout)  # In minutes
        subscribe.append(request_elem)
        return subscribe


class SubscribeToPush(Subscribe):
    subscription_request_elem_tag = 'm:PushSubscriptionRequest'

    def call(self, folders, event_types, watermark, status_frequency, url):
        yield from self._partial_call(
            payload_func=self.get_payload, folders=folders, event_types=event_types,
            status_frequency=status_frequency, url=url, watermark=watermark,
        )

    def get_payload(self, folders, event_types, watermark, status_frequency, url):
        subscribe = create_element('m:%s' % self.SERVICE_NAME)
        request_elem = self._partial_payload(folders=folders, event_types=event_types)
        if watermark:
            add_xml_child(request_elem, 'm:Watermark', watermark)
        add_xml_child(request_elem, 't:StatusFrequency', status_frequency)  # In minutes
        add_xml_child(request_elem, 't:URL', url)
        subscribe.append(request_elem)
        return subscribe


class SubscribeToStreaming(Subscribe):
    subscription_request_elem_tag = 'm:StreamingSubscriptionRequest'

    def call(self, folders, event_types):
        yield from self._partial_call(payload_func=self.get_payload, folders=folders, event_types=event_types)

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield elem.text

    @classmethod
    def _get_elements_in_container(cls, container):
        return [container.find('{%s}SubscriptionId' % MNS)]

    def get_payload(self, folders, event_types):
        subscribe = create_element('m:%s' % self.SERVICE_NAME)
        request_elem = self._partial_payload(folders=folders, event_types=event_types)
        subscribe.append(request_elem)
        return subscribe

Classes

class Subscribe (*args, **kwargs)
Expand source code
class Subscribe(EWSAccountService, metaclass=abc.ABCMeta):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/subscribe-operation"""

    SERVICE_NAME = 'Subscribe'
    EVENT_TYPES = (
        'CopiedEvent',
        'CreatedEvent',
        'DeletedEvent',
        'ModifiedEvent',
        'MovedEvent',
        'NewMailEvent',
        'FreeBusyChangedEvent',
    )
    subscription_request_elem_tag = None

    def _partial_call(self, payload_func, folders, event_types, **kwargs):
        if set(event_types) - set(self.EVENT_TYPES):
            raise ValueError("'event_types' values must consist of values in %s" % str(self.EVENT_TYPES))
        return self._elems_to_objs(self._get_elements(
            payload=payload_func(folders=folders, event_types=event_types, **kwargs)
        ))

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            subscription_elem, watermark_elem = elem
            yield subscription_elem.text, watermark_elem.text

    @classmethod
    def _get_elements_in_container(cls, container):
        return [(container.find('{%s}SubscriptionId' % MNS), container.find('{%s}Watermark' % MNS))]

    def _partial_payload(self, folders, event_types):
        request_elem = create_element(self.subscription_request_elem_tag)
        folder_ids = create_folder_ids_element(tag='t:FolderIds', folders=folders, version=self.account.version)
        request_elem.append(folder_ids)
        event_types_elem = create_element('t:EventTypes')
        for event_type in event_types:
            add_xml_child(event_types_elem, 't:EventType', event_type)
        if not len(event_types_elem):
            raise ValueError('"event_types" must not be empty')
        request_elem.append(event_types_elem)
        return request_elem

Ancestors

Subclasses

Class variables

var EVENT_TYPES
var SERVICE_NAME
var subscription_request_elem_tag

Inherited members

class SubscribeToPull (*args, **kwargs)
Expand source code
class SubscribeToPull(Subscribe):
    subscription_request_elem_tag = 'm:PullSubscriptionRequest'

    def call(self, folders, event_types, watermark, timeout):
        yield from self._partial_call(
            payload_func=self.get_payload, folders=folders, event_types=event_types, timeout=timeout,
            watermark=watermark,
        )

    def get_payload(self, folders, event_types, watermark, timeout):
        subscribe = create_element('m:%s' % self.SERVICE_NAME)
        request_elem = self._partial_payload(folders=folders, event_types=event_types)
        if watermark:
            add_xml_child(request_elem, 'm:Watermark', watermark)
        add_xml_child(request_elem, 't:Timeout', timeout)  # In minutes
        subscribe.append(request_elem)
        return subscribe

Ancestors

Class variables

var subscription_request_elem_tag

Methods

def call(self, folders, event_types, watermark, timeout)
Expand source code
def call(self, folders, event_types, watermark, timeout):
    yield from self._partial_call(
        payload_func=self.get_payload, folders=folders, event_types=event_types, timeout=timeout,
        watermark=watermark,
    )
def get_payload(self, folders, event_types, watermark, timeout)
Expand source code
def get_payload(self, folders, event_types, watermark, timeout):
    subscribe = create_element('m:%s' % self.SERVICE_NAME)
    request_elem = self._partial_payload(folders=folders, event_types=event_types)
    if watermark:
        add_xml_child(request_elem, 'm:Watermark', watermark)
    add_xml_child(request_elem, 't:Timeout', timeout)  # In minutes
    subscribe.append(request_elem)
    return subscribe

Inherited members

class SubscribeToPush (*args, **kwargs)
Expand source code
class SubscribeToPush(Subscribe):
    subscription_request_elem_tag = 'm:PushSubscriptionRequest'

    def call(self, folders, event_types, watermark, status_frequency, url):
        yield from self._partial_call(
            payload_func=self.get_payload, folders=folders, event_types=event_types,
            status_frequency=status_frequency, url=url, watermark=watermark,
        )

    def get_payload(self, folders, event_types, watermark, status_frequency, url):
        subscribe = create_element('m:%s' % self.SERVICE_NAME)
        request_elem = self._partial_payload(folders=folders, event_types=event_types)
        if watermark:
            add_xml_child(request_elem, 'm:Watermark', watermark)
        add_xml_child(request_elem, 't:StatusFrequency', status_frequency)  # In minutes
        add_xml_child(request_elem, 't:URL', url)
        subscribe.append(request_elem)
        return subscribe

Ancestors

Class variables

var subscription_request_elem_tag

Methods

def call(self, folders, event_types, watermark, status_frequency, url)
Expand source code
def call(self, folders, event_types, watermark, status_frequency, url):
    yield from self._partial_call(
        payload_func=self.get_payload, folders=folders, event_types=event_types,
        status_frequency=status_frequency, url=url, watermark=watermark,
    )
def get_payload(self, folders, event_types, watermark, status_frequency, url)
Expand source code
def get_payload(self, folders, event_types, watermark, status_frequency, url):
    subscribe = create_element('m:%s' % self.SERVICE_NAME)
    request_elem = self._partial_payload(folders=folders, event_types=event_types)
    if watermark:
        add_xml_child(request_elem, 'm:Watermark', watermark)
    add_xml_child(request_elem, 't:StatusFrequency', status_frequency)  # In minutes
    add_xml_child(request_elem, 't:URL', url)
    subscribe.append(request_elem)
    return subscribe

Inherited members

class SubscribeToStreaming (*args, **kwargs)
Expand source code
class SubscribeToStreaming(Subscribe):
    subscription_request_elem_tag = 'm:StreamingSubscriptionRequest'

    def call(self, folders, event_types):
        yield from self._partial_call(payload_func=self.get_payload, folders=folders, event_types=event_types)

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield elem.text

    @classmethod
    def _get_elements_in_container(cls, container):
        return [container.find('{%s}SubscriptionId' % MNS)]

    def get_payload(self, folders, event_types):
        subscribe = create_element('m:%s' % self.SERVICE_NAME)
        request_elem = self._partial_payload(folders=folders, event_types=event_types)
        subscribe.append(request_elem)
        return subscribe

Ancestors

Class variables

var subscription_request_elem_tag

Methods

def call(self, folders, event_types)
Expand source code
def call(self, folders, event_types):
    yield from self._partial_call(payload_func=self.get_payload, folders=folders, event_types=event_types)
def get_payload(self, folders, event_types)
Expand source code
def get_payload(self, folders, event_types):
    subscribe = create_element('m:%s' % self.SERVICE_NAME)
    request_elem = self._partial_payload(folders=folders, event_types=event_types)
    subscribe.append(request_elem)
    return subscribe

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/sync_folder_hierarchy.html000066400000000000000000000610431414601472700262640ustar00rootroot00000000000000 exchangelib.services.sync_folder_hierarchy API documentation

Module exchangelib.services.sync_folder_hierarchy

Expand source code
import abc
import logging

from .common import EWSAccountService, add_xml_child, create_folder_ids_element, create_shape_element, parse_folder_elem
from ..properties import FolderId
from ..util import create_element, xml_text_to_value, MNS, TNS

log = logging.getLogger(__name__)


class SyncFolder(EWSAccountService, metaclass=abc.ABCMeta):
    """Base class for SyncFolderHierarchy and SyncFolderItems."""

    element_container_name = '{%s}Changes' % MNS
    # Change types
    CREATE = 'create'
    UPDATE = 'update'
    DELETE = 'delete'
    CHANGE_TYPES = (CREATE, UPDATE, DELETE)
    shape_tag = None
    last_in_range_name = None

    def __init__(self, *args, **kwargs):
        # These values are reset and set each time call() is consumed
        self.sync_state = None
        self.includes_last_item_in_range = None
        super().__init__(*args, **kwargs)

    def _change_types_map(self):
        return {
            '{%s}Create' % TNS: self.CREATE,
            '{%s}Update' % TNS: self.UPDATE,
            '{%s}Delete' % TNS: self.DELETE,
        }

    def _get_element_container(self, message, name=None):
        self.sync_state = message.find('{%s}SyncState' % MNS).text
        log.debug('Sync state is: %s', self.sync_state)
        self.includes_last_item_in_range = xml_text_to_value(
            message.find(self.last_in_range_name).text, bool
        )
        log.debug('Includes last item in range: %s', self.includes_last_item_in_range)
        return super()._get_element_container(message=message, name=name)

    def _partial_get_payload(self, folder, shape, additional_fields, sync_state):
        svc_elem = create_element('m:%s' % self.SERVICE_NAME)
        foldershape = create_shape_element(
            tag=self.shape_tag, shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        svc_elem.append(foldershape)
        folder_id = create_folder_ids_element(tag='m:SyncFolderId', folders=[folder], version=self.account.version)
        svc_elem.append(folder_id)
        if sync_state:
            add_xml_child(svc_elem, 'm:SyncState', sync_state)
        return svc_elem


class SyncFolderHierarchy(SyncFolder):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderhierarchy-operation
    """

    SERVICE_NAME = 'SyncFolderHierarchy'
    shape_tag = 'm:FolderShape'
    last_in_range_name = '{%s}IncludesLastFolderInRange' % MNS

    def call(self, folder, shape, additional_fields, sync_state):
        self.sync_state = sync_state
        change_types = self._change_types_map()
        for elem in self._get_elements(payload=self.get_payload(
                folder=folder,
                shape=shape,
                additional_fields=additional_fields,
                sync_state=sync_state,
        )):
            if isinstance(elem, Exception):
                yield elem
                continue
            change_type = change_types[elem.tag]
            if change_type == self.DELETE:
                folder = FolderId.from_xml(elem=elem.find(FolderId.response_tag()), account=self.account)
            else:
                # We can't find() the element because we don't know which tag to look for. The change element can
                # contain multiple folder types, each with their own tag.
                folder_elem = elem[0]
                folder = parse_folder_elem(elem=folder_elem, folder=folder, account=self.account)
            yield change_type, folder

    def get_payload(self, folder, shape, additional_fields, sync_state):
        return self._partial_get_payload(
            folder=folder, shape=shape, additional_fields=additional_fields, sync_state=sync_state
        )

Classes

class SyncFolder (*args, **kwargs)

Base class for SyncFolderHierarchy and SyncFolderItems.

Expand source code
class SyncFolder(EWSAccountService, metaclass=abc.ABCMeta):
    """Base class for SyncFolderHierarchy and SyncFolderItems."""

    element_container_name = '{%s}Changes' % MNS
    # Change types
    CREATE = 'create'
    UPDATE = 'update'
    DELETE = 'delete'
    CHANGE_TYPES = (CREATE, UPDATE, DELETE)
    shape_tag = None
    last_in_range_name = None

    def __init__(self, *args, **kwargs):
        # These values are reset and set each time call() is consumed
        self.sync_state = None
        self.includes_last_item_in_range = None
        super().__init__(*args, **kwargs)

    def _change_types_map(self):
        return {
            '{%s}Create' % TNS: self.CREATE,
            '{%s}Update' % TNS: self.UPDATE,
            '{%s}Delete' % TNS: self.DELETE,
        }

    def _get_element_container(self, message, name=None):
        self.sync_state = message.find('{%s}SyncState' % MNS).text
        log.debug('Sync state is: %s', self.sync_state)
        self.includes_last_item_in_range = xml_text_to_value(
            message.find(self.last_in_range_name).text, bool
        )
        log.debug('Includes last item in range: %s', self.includes_last_item_in_range)
        return super()._get_element_container(message=message, name=name)

    def _partial_get_payload(self, folder, shape, additional_fields, sync_state):
        svc_elem = create_element('m:%s' % self.SERVICE_NAME)
        foldershape = create_shape_element(
            tag=self.shape_tag, shape=shape, additional_fields=additional_fields, version=self.account.version
        )
        svc_elem.append(foldershape)
        folder_id = create_folder_ids_element(tag='m:SyncFolderId', folders=[folder], version=self.account.version)
        svc_elem.append(folder_id)
        if sync_state:
            add_xml_child(svc_elem, 'm:SyncState', sync_state)
        return svc_elem

Ancestors

Subclasses

Class variables

var CHANGE_TYPES
var CREATE
var DELETE
var UPDATE
var element_container_name
var last_in_range_name
var shape_tag

Inherited members

class SyncFolderHierarchy (*args, **kwargs)
Expand source code
class SyncFolderHierarchy(SyncFolder):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderhierarchy-operation
    """

    SERVICE_NAME = 'SyncFolderHierarchy'
    shape_tag = 'm:FolderShape'
    last_in_range_name = '{%s}IncludesLastFolderInRange' % MNS

    def call(self, folder, shape, additional_fields, sync_state):
        self.sync_state = sync_state
        change_types = self._change_types_map()
        for elem in self._get_elements(payload=self.get_payload(
                folder=folder,
                shape=shape,
                additional_fields=additional_fields,
                sync_state=sync_state,
        )):
            if isinstance(elem, Exception):
                yield elem
                continue
            change_type = change_types[elem.tag]
            if change_type == self.DELETE:
                folder = FolderId.from_xml(elem=elem.find(FolderId.response_tag()), account=self.account)
            else:
                # We can't find() the element because we don't know which tag to look for. The change element can
                # contain multiple folder types, each with their own tag.
                folder_elem = elem[0]
                folder = parse_folder_elem(elem=folder_elem, folder=folder, account=self.account)
            yield change_type, folder

    def get_payload(self, folder, shape, additional_fields, sync_state):
        return self._partial_get_payload(
            folder=folder, shape=shape, additional_fields=additional_fields, sync_state=sync_state
        )

Ancestors

Class variables

var SERVICE_NAME
var last_in_range_name
var shape_tag

Methods

def call(self, folder, shape, additional_fields, sync_state)
Expand source code
def call(self, folder, shape, additional_fields, sync_state):
    self.sync_state = sync_state
    change_types = self._change_types_map()
    for elem in self._get_elements(payload=self.get_payload(
            folder=folder,
            shape=shape,
            additional_fields=additional_fields,
            sync_state=sync_state,
    )):
        if isinstance(elem, Exception):
            yield elem
            continue
        change_type = change_types[elem.tag]
        if change_type == self.DELETE:
            folder = FolderId.from_xml(elem=elem.find(FolderId.response_tag()), account=self.account)
        else:
            # We can't find() the element because we don't know which tag to look for. The change element can
            # contain multiple folder types, each with their own tag.
            folder_elem = elem[0]
            folder = parse_folder_elem(elem=folder_elem, folder=folder, account=self.account)
        yield change_type, folder
def get_payload(self, folder, shape, additional_fields, sync_state)
Expand source code
def get_payload(self, folder, shape, additional_fields, sync_state):
    return self._partial_get_payload(
        folder=folder, shape=shape, additional_fields=additional_fields, sync_state=sync_state
    )

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/sync_folder_items.html000066400000000000000000000517631414601472700254370ustar00rootroot00000000000000 exchangelib.services.sync_folder_items API documentation

Module exchangelib.services.sync_folder_items

Expand source code
from .common import add_xml_child, create_item_ids_element
from .sync_folder_hierarchy import SyncFolder
from ..properties import ItemId
from ..util import xml_text_to_value, peek, TNS, MNS


class SyncFolderItems(SyncFolder):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderitems-operation
    """

    SERVICE_NAME = 'SyncFolderItems'
    SYNC_SCOPES = {
        'NormalItems',
        'NormalAndAssociatedItems',
    }
    # Extra change type
    READ_FLAG_CHANGE = 'read_flag_change'
    CHANGE_TYPES = SyncFolder.CHANGE_TYPES + (READ_FLAG_CHANGE,)
    shape_tag = 'm:ItemShape'
    last_in_range_name = '{%s}IncludesLastItemInRange' % MNS

    def _change_types_map(self):
        res = super()._change_types_map()
        res['{%s}ReadFlagChange' % TNS] = self.READ_FLAG_CHANGE
        return res

    def call(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope):
        self.sync_state = sync_state
        if max_changes_returned is None:
            max_changes_returned = self.chunk_size
        if max_changes_returned <= 0:
            raise ValueError("'max_changes_returned' %s must be a positive integer" % max_changes_returned)
        if sync_scope is not None and sync_scope not in self.SYNC_SCOPES:
            raise ValueError("'sync_scope' %s must be one of %r" % (sync_scope, self.SYNC_SCOPES))
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
                folder=folder,
                shape=shape,
                additional_fields=additional_fields,
                sync_state=sync_state,
                ignore=ignore,
                max_changes_returned=max_changes_returned,
                sync_scope=sync_scope,
        )))

    def _elems_to_objs(self, elems):
        from ..folders.base import BaseFolder
        change_types = self._change_types_map()
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            change_type = change_types[elem.tag]
            if change_type == self.READ_FLAG_CHANGE:
                item = (
                    ItemId.from_xml(elem=elem.find(ItemId.response_tag()), account=self.account),
                    xml_text_to_value(elem.find('{%s}IsRead' % TNS).text, bool)
                )
            elif change_type == self.DELETE:
                item = ItemId.from_xml(elem=elem.find(ItemId.response_tag()), account=self.account)
            else:
                # We can't find() the element because we don't know which tag to look for. The change element can
                # contain multiple item types, each with their own tag.
                item_elem = elem[0]
                item = BaseFolder.item_model_from_tag(item_elem.tag).from_xml(elem=item_elem, account=self.account)
            yield change_type, item

    def get_payload(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope):
        syncfolderitems = self._partial_get_payload(
            folder=folder, shape=shape, additional_fields=additional_fields, sync_state=sync_state
        )
        is_empty, ignore = (True, None) if ignore is None else peek(ignore)
        if not is_empty:
            item_ids = create_item_ids_element(items=ignore, version=self.account.version, tag='m:Ignore')
            syncfolderitems.append(item_ids)
        add_xml_child(syncfolderitems, 'm:MaxChangesReturned', max_changes_returned)
        if sync_scope:
            add_xml_child(syncfolderitems, 'm:SyncScope', sync_scope)
        return syncfolderitems

Classes

class SyncFolderItems (*args, **kwargs)
Expand source code
class SyncFolderItems(SyncFolder):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderitems-operation
    """

    SERVICE_NAME = 'SyncFolderItems'
    SYNC_SCOPES = {
        'NormalItems',
        'NormalAndAssociatedItems',
    }
    # Extra change type
    READ_FLAG_CHANGE = 'read_flag_change'
    CHANGE_TYPES = SyncFolder.CHANGE_TYPES + (READ_FLAG_CHANGE,)
    shape_tag = 'm:ItemShape'
    last_in_range_name = '{%s}IncludesLastItemInRange' % MNS

    def _change_types_map(self):
        res = super()._change_types_map()
        res['{%s}ReadFlagChange' % TNS] = self.READ_FLAG_CHANGE
        return res

    def call(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope):
        self.sync_state = sync_state
        if max_changes_returned is None:
            max_changes_returned = self.chunk_size
        if max_changes_returned <= 0:
            raise ValueError("'max_changes_returned' %s must be a positive integer" % max_changes_returned)
        if sync_scope is not None and sync_scope not in self.SYNC_SCOPES:
            raise ValueError("'sync_scope' %s must be one of %r" % (sync_scope, self.SYNC_SCOPES))
        return self._elems_to_objs(self._get_elements(payload=self.get_payload(
                folder=folder,
                shape=shape,
                additional_fields=additional_fields,
                sync_state=sync_state,
                ignore=ignore,
                max_changes_returned=max_changes_returned,
                sync_scope=sync_scope,
        )))

    def _elems_to_objs(self, elems):
        from ..folders.base import BaseFolder
        change_types = self._change_types_map()
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            change_type = change_types[elem.tag]
            if change_type == self.READ_FLAG_CHANGE:
                item = (
                    ItemId.from_xml(elem=elem.find(ItemId.response_tag()), account=self.account),
                    xml_text_to_value(elem.find('{%s}IsRead' % TNS).text, bool)
                )
            elif change_type == self.DELETE:
                item = ItemId.from_xml(elem=elem.find(ItemId.response_tag()), account=self.account)
            else:
                # We can't find() the element because we don't know which tag to look for. The change element can
                # contain multiple item types, each with their own tag.
                item_elem = elem[0]
                item = BaseFolder.item_model_from_tag(item_elem.tag).from_xml(elem=item_elem, account=self.account)
            yield change_type, item

    def get_payload(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope):
        syncfolderitems = self._partial_get_payload(
            folder=folder, shape=shape, additional_fields=additional_fields, sync_state=sync_state
        )
        is_empty, ignore = (True, None) if ignore is None else peek(ignore)
        if not is_empty:
            item_ids = create_item_ids_element(items=ignore, version=self.account.version, tag='m:Ignore')
            syncfolderitems.append(item_ids)
        add_xml_child(syncfolderitems, 'm:MaxChangesReturned', max_changes_returned)
        if sync_scope:
            add_xml_child(syncfolderitems, 'm:SyncScope', sync_scope)
        return syncfolderitems

Ancestors

Class variables

var CHANGE_TYPES
var READ_FLAG_CHANGE
var SERVICE_NAME
var SYNC_SCOPES
var last_in_range_name
var shape_tag

Methods

def call(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope)
Expand source code
def call(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope):
    self.sync_state = sync_state
    if max_changes_returned is None:
        max_changes_returned = self.chunk_size
    if max_changes_returned <= 0:
        raise ValueError("'max_changes_returned' %s must be a positive integer" % max_changes_returned)
    if sync_scope is not None and sync_scope not in self.SYNC_SCOPES:
        raise ValueError("'sync_scope' %s must be one of %r" % (sync_scope, self.SYNC_SCOPES))
    return self._elems_to_objs(self._get_elements(payload=self.get_payload(
            folder=folder,
            shape=shape,
            additional_fields=additional_fields,
            sync_state=sync_state,
            ignore=ignore,
            max_changes_returned=max_changes_returned,
            sync_scope=sync_scope,
    )))
def get_payload(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope)
Expand source code
def get_payload(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope):
    syncfolderitems = self._partial_get_payload(
        folder=folder, shape=shape, additional_fields=additional_fields, sync_state=sync_state
    )
    is_empty, ignore = (True, None) if ignore is None else peek(ignore)
    if not is_empty:
        item_ids = create_item_ids_element(items=ignore, version=self.account.version, tag='m:Ignore')
        syncfolderitems.append(item_ids)
    add_xml_child(syncfolderitems, 'm:MaxChangesReturned', max_changes_returned)
    if sync_scope:
        add_xml_child(syncfolderitems, 'm:SyncScope', sync_scope)
    return syncfolderitems

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/unsubscribe.html000066400000000000000000000277011414601472700242460ustar00rootroot00000000000000 exchangelib.services.unsubscribe API documentation

Module exchangelib.services.unsubscribe

Expand source code
from .common import EWSAccountService, add_xml_child
from ..util import create_element


class Unsubscribe(EWSAccountService):
    """Unsubscribing is only valid for pull and streaming notifications.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/unsubscribe-operation
    """

    SERVICE_NAME = 'Unsubscribe'
    returns_elements = False

    def call(self, subscription_id):
        return self._get_elements(payload=self.get_payload(subscription_id=subscription_id))

    def get_payload(self, subscription_id):
        unsubscribe = create_element('m:%s' % self.SERVICE_NAME)
        add_xml_child(unsubscribe, 'm:SubscriptionId', subscription_id)
        return unsubscribe

Classes

class Unsubscribe (*args, **kwargs)

Unsubscribing is only valid for pull and streaming notifications.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/unsubscribe-operation

Expand source code
class Unsubscribe(EWSAccountService):
    """Unsubscribing is only valid for pull and streaming notifications.

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/unsubscribe-operation
    """

    SERVICE_NAME = 'Unsubscribe'
    returns_elements = False

    def call(self, subscription_id):
        return self._get_elements(payload=self.get_payload(subscription_id=subscription_id))

    def get_payload(self, subscription_id):
        unsubscribe = create_element('m:%s' % self.SERVICE_NAME)
        add_xml_child(unsubscribe, 'm:SubscriptionId', subscription_id)
        return unsubscribe

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, subscription_id)
Expand source code
def call(self, subscription_id):
    return self._get_elements(payload=self.get_payload(subscription_id=subscription_id))
def get_payload(self, subscription_id)
Expand source code
def get_payload(self, subscription_id):
    unsubscribe = create_element('m:%s' % self.SERVICE_NAME)
    add_xml_child(unsubscribe, 'm:SubscriptionId', subscription_id)
    return unsubscribe

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/update_folder.html000066400000000000000000000507671414601472700245470ustar00rootroot00000000000000 exchangelib.services.update_folder API documentation

Module exchangelib.services.update_folder

Expand source code
from .common import EWSAccountService, parse_folder_elem, to_item_id
from ..fields import FieldPath
from ..util import create_element, set_xml_value, MNS


class UpdateFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updatefolder-operation"""

    SERVICE_NAME = 'UpdateFolder'
    element_container_name = '{%s}Folders' % MNS

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.folders = []  # A hack to communicate parsing args to _elems_to_objs()

    def call(self, folders):
        # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same
        # class as the folder instance it was requested with.
        self.folders = list(folders)  # Convert to a list, in case 'folders' is a generator. We're iterating twice.
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=self.folders))

    def _elems_to_objs(self, elems):
        for (folder, _), elem in zip(self.folders, elems):
            if isinstance(elem, Exception):
                yield elem
                continue
            yield parse_folder_elem(elem=elem, folder=folder, account=self.account)

    @staticmethod
    def _sort_fieldnames(folder_model, fieldnames):
        # Take a list of fieldnames and return the fields in the order they are mentioned in folder_model.FIELDS.
        # Loop over FIELDS and not supported_fields(). Upstream should make sure not to update a non-supported field.
        for f in folder_model.FIELDS:
            if f.name in fieldnames:
                yield f.name

    def _set_folder_elem(self, folder_model, field_path, value):
        setfolderfield = create_element('t:SetFolderField')
        set_xml_value(setfolderfield, field_path, version=self.account.version)
        folder = create_element(folder_model.request_tag())
        field_elem = field_path.field.to_xml(value, version=self.account.version)
        set_xml_value(folder, field_elem, version=self.account.version)
        setfolderfield.append(folder)
        return setfolderfield

    def _delete_folder_elem(self, field_path):
        deletefolderfield = create_element('t:DeleteFolderField')
        return set_xml_value(deletefolderfield, field_path, version=self.account.version)

    def _get_folder_update_elems(self, folder, fieldnames):
        folder_model = folder.__class__
        fieldnames_set = set(fieldnames)

        for fieldname in self._sort_fieldnames(folder_model=folder_model, fieldnames=fieldnames_set):
            field = folder_model.get_field_by_fieldname(fieldname)
            if field.is_read_only:
                raise ValueError('%s is a read-only field' % field.name)
            value = field.clean(getattr(folder, field.name), version=self.account.version)  # Make sure the value is OK

            if value is None or (field.is_list and not value):
                # A value of None or [] means we want to remove this field from the item
                if field.is_required or field.is_required_after_save:
                    raise ValueError('%s is a required field and may not be deleted' % field.name)
                for field_path in FieldPath(field=field).expand(version=self.account.version):
                    yield self._delete_folder_elem(field_path=field_path)
                continue

            yield self._set_folder_elem(folder_model=folder_model, field_path=FieldPath(field=field), value=value)

    def get_payload(self, folders):
        from ..folders import BaseFolder, FolderId
        updatefolder = create_element('m:%s' % self.SERVICE_NAME)
        folderchanges = create_element('m:FolderChanges')
        version = self.account.version
        for folder, fieldnames in folders:
            folderchange = create_element('t:FolderChange')
            if not isinstance(folder, (BaseFolder, FolderId)):
                folder = to_item_id(folder, FolderId, version=version)
            set_xml_value(folderchange, folder, version=version)
            updates = create_element('t:Updates')
            for elem in self._get_folder_update_elems(folder=folder, fieldnames=fieldnames):
                updates.append(elem)
            folderchange.append(updates)
            folderchanges.append(folderchange)
        if not len(folderchanges):
            raise ValueError('"folders" must not be empty')
        updatefolder.append(folderchanges)
        return updatefolder

Classes

class UpdateFolder (*args, **kwargs)
Expand source code
class UpdateFolder(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updatefolder-operation"""

    SERVICE_NAME = 'UpdateFolder'
    element_container_name = '{%s}Folders' % MNS

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.folders = []  # A hack to communicate parsing args to _elems_to_objs()

    def call(self, folders):
        # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same
        # class as the folder instance it was requested with.
        self.folders = list(folders)  # Convert to a list, in case 'folders' is a generator. We're iterating twice.
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=self.folders))

    def _elems_to_objs(self, elems):
        for (folder, _), elem in zip(self.folders, elems):
            if isinstance(elem, Exception):
                yield elem
                continue
            yield parse_folder_elem(elem=elem, folder=folder, account=self.account)

    @staticmethod
    def _sort_fieldnames(folder_model, fieldnames):
        # Take a list of fieldnames and return the fields in the order they are mentioned in folder_model.FIELDS.
        # Loop over FIELDS and not supported_fields(). Upstream should make sure not to update a non-supported field.
        for f in folder_model.FIELDS:
            if f.name in fieldnames:
                yield f.name

    def _set_folder_elem(self, folder_model, field_path, value):
        setfolderfield = create_element('t:SetFolderField')
        set_xml_value(setfolderfield, field_path, version=self.account.version)
        folder = create_element(folder_model.request_tag())
        field_elem = field_path.field.to_xml(value, version=self.account.version)
        set_xml_value(folder, field_elem, version=self.account.version)
        setfolderfield.append(folder)
        return setfolderfield

    def _delete_folder_elem(self, field_path):
        deletefolderfield = create_element('t:DeleteFolderField')
        return set_xml_value(deletefolderfield, field_path, version=self.account.version)

    def _get_folder_update_elems(self, folder, fieldnames):
        folder_model = folder.__class__
        fieldnames_set = set(fieldnames)

        for fieldname in self._sort_fieldnames(folder_model=folder_model, fieldnames=fieldnames_set):
            field = folder_model.get_field_by_fieldname(fieldname)
            if field.is_read_only:
                raise ValueError('%s is a read-only field' % field.name)
            value = field.clean(getattr(folder, field.name), version=self.account.version)  # Make sure the value is OK

            if value is None or (field.is_list and not value):
                # A value of None or [] means we want to remove this field from the item
                if field.is_required or field.is_required_after_save:
                    raise ValueError('%s is a required field and may not be deleted' % field.name)
                for field_path in FieldPath(field=field).expand(version=self.account.version):
                    yield self._delete_folder_elem(field_path=field_path)
                continue

            yield self._set_folder_elem(folder_model=folder_model, field_path=FieldPath(field=field), value=value)

    def get_payload(self, folders):
        from ..folders import BaseFolder, FolderId
        updatefolder = create_element('m:%s' % self.SERVICE_NAME)
        folderchanges = create_element('m:FolderChanges')
        version = self.account.version
        for folder, fieldnames in folders:
            folderchange = create_element('t:FolderChange')
            if not isinstance(folder, (BaseFolder, FolderId)):
                folder = to_item_id(folder, FolderId, version=version)
            set_xml_value(folderchange, folder, version=version)
            updates = create_element('t:Updates')
            for elem in self._get_folder_update_elems(folder=folder, fieldnames=fieldnames):
                updates.append(elem)
            folderchange.append(updates)
            folderchanges.append(folderchange)
        if not len(folderchanges):
            raise ValueError('"folders" must not be empty')
        updatefolder.append(folderchanges)
        return updatefolder

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, folders)
Expand source code
def call(self, folders):
    # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same
    # class as the folder instance it was requested with.
    self.folders = list(folders)  # Convert to a list, in case 'folders' is a generator. We're iterating twice.
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=self.folders))
def get_payload(self, folders)
Expand source code
def get_payload(self, folders):
    from ..folders import BaseFolder, FolderId
    updatefolder = create_element('m:%s' % self.SERVICE_NAME)
    folderchanges = create_element('m:FolderChanges')
    version = self.account.version
    for folder, fieldnames in folders:
        folderchange = create_element('t:FolderChange')
        if not isinstance(folder, (BaseFolder, FolderId)):
            folder = to_item_id(folder, FolderId, version=version)
        set_xml_value(folderchange, folder, version=version)
        updates = create_element('t:Updates')
        for elem in self._get_folder_update_elems(folder=folder, fieldnames=fieldnames):
            updates.append(elem)
        folderchange.append(updates)
        folderchanges.append(folderchange)
    if not len(folderchanges):
        raise ValueError('"folders" must not be empty')
    updatefolder.append(folderchanges)
    return updatefolder

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/update_item.html000066400000000000000000001064471414601472700242270ustar00rootroot00000000000000 exchangelib.services.update_item API documentation

Module exchangelib.services.update_item

Expand source code
from collections import OrderedDict

from .common import EWSAccountService, to_item_id
from ..ewsdatetime import EWSDate
from ..fields import FieldPath, IndexedField
from ..properties import ItemId
from ..util import create_element, set_xml_value, MNS
from ..version import EXCHANGE_2013_SP1


class UpdateItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation"""

    SERVICE_NAME = 'UpdateItem'
    element_container_name = '{%s}Items' % MNS

    def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
             suppress_read_receipts):
        from ..items import CONFLICT_RESOLUTION_CHOICES, MESSAGE_DISPOSITION_CHOICES, \
            SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES, SEND_ONLY
        if conflict_resolution not in CONFLICT_RESOLUTION_CHOICES:
            raise ValueError("'conflict_resolution' %s must be one of %s" % (
                conflict_resolution, CONFLICT_RESOLUTION_CHOICES
            ))
        if message_disposition not in MESSAGE_DISPOSITION_CHOICES:
            raise ValueError("'message_disposition' %s must be one of %s" % (
                message_disposition, MESSAGE_DISPOSITION_CHOICES
            ))
        if send_meeting_invitations_or_cancellations not in SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES:
            raise ValueError("'send_meeting_invitations_or_cancellations' %s must be one of %s" % (
                send_meeting_invitations_or_cancellations, SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES
            ))
        if suppress_read_receipts not in (True, False):
            raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts)
        if message_disposition == SEND_ONLY:
            raise ValueError('Cannot send-only existing objects. Use SendItem service instead')
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=items,
            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 _elems_to_objs(self, elems):
        from ..items import Item
        for elem in elems:
            if isinstance(elem, (Exception, type(None))):
                yield elem
                continue
            yield Item.id_from_xml(elem)

    def _delete_item_elem(self, field_path):
        deleteitemfield = create_element('t:DeleteItemField')
        return set_xml_value(deleteitemfield, field_path, version=self.account.version)

    def _set_item_elem(self, item_model, field_path, value):
        setitemfield = create_element('t:SetItemField')
        set_xml_value(setitemfield, field_path, version=self.account.version)
        item_elem = create_element(item_model.request_tag())
        field_elem = field_path.field.to_xml(value, version=self.account.version)
        set_xml_value(item_elem, field_elem, version=self.account.version)
        setitemfield.append(item_elem)
        return setitemfield

    @staticmethod
    def _sorted_fields(item_model, fieldnames):
        # Take a list of fieldnames and return the (unique) fields in the order they are mentioned in item_class.FIELDS.
        # Checks that all fieldnames are valid.
        unique_fieldnames = list(OrderedDict.fromkeys(fieldnames))  # Make field names unique ,but keep ordering
        # Loop over FIELDS and not supported_fields(). Upstream should make sure not to update a non-supported field.
        for f in item_model.FIELDS:
            if f.name in unique_fieldnames:
                unique_fieldnames.remove(f.name)
                yield f
        if unique_fieldnames:
            raise ValueError("Field name(s) %s are not valid for a '%s' item" % (
                ', '.join("'%s'" % f for f in unique_fieldnames), item_model.__name__))

    def _get_item_update_elems(self, item, fieldnames):
        from ..items import CalendarItem
        fieldnames_copy = list(fieldnames)

        if item.__class__ == CalendarItem:
            # For CalendarItem items where we update 'start' or 'end', we want to update internal timezone fields
            item.clean_timezone_fields(version=self.account.version)  # Possibly also sets timezone values
            for field_name in ('start', 'end'):
                if field_name in fieldnames_copy:
                    tz_field_name = item.tz_field_for_field_name(field_name).name
                    if tz_field_name not in fieldnames_copy:
                        fieldnames_copy.append(tz_field_name)

        for field in self._sorted_fields(item_model=item.__class__, fieldnames=fieldnames_copy):
            if field.is_read_only:
                raise ValueError('%s is a read-only field' % field.name)
            value = self._get_item_value(item, field)
            if value is None or (field.is_list and not value):
                # A value of None or [] means we want to remove this field from the item
                yield from self._get_delete_item_elems(field=field)
            else:
                yield from self._get_set_item_elems(item_model=item.__class__, field=field, value=value)

    def _get_item_value(self, item, field):
        from ..items import CalendarItem
        value = field.clean(getattr(item, field.name), version=self.account.version)  # Make sure the value is OK
        if item.__class__ == CalendarItem:
            # For CalendarItem items where we update 'start' or 'end', we want to send values in the local timezone
            if field.name in ('start', 'end'):
                if type(value) is EWSDate:
                    # EWS always expects a datetime
                    return item.date_to_datetime(field_name=field.name)
                tz_field_name = item.tz_field_for_field_name(field.name).name
                return value.astimezone(getattr(item, tz_field_name))
        return value

    def _get_delete_item_elems(self, field):
        if field.is_required or field.is_required_after_save:
            raise ValueError('%s is a required field and may not be deleted' % field.name)
        for field_path in FieldPath(field=field).expand(version=self.account.version):
            yield self._delete_item_elem(field_path=field_path)

    def _get_set_item_elems(self, item_model, field, value):
        if isinstance(field, IndexedField):
            # Generate either set or delete elements for all combinations of labels and subfields
            supported_labels = field.value_cls.get_field_by_fieldname('label')\
                .supported_choices(version=self.account.version)
            seen_labels = set()
            subfields = field.value_cls.supported_fields(version=self.account.version)
            for v in value:
                seen_labels.add(v.label)
                for subfield in subfields:
                    field_path = FieldPath(field=field, label=v.label, subfield=subfield)
                    subfield_value = getattr(v, subfield.name)
                    if not subfield_value:
                        # Generate delete elements for blank subfield values
                        yield self._delete_item_elem(field_path=field_path)
                    else:
                        # Generate set elements for non-null subfield values
                        yield self._set_item_elem(
                            item_model=item_model,
                            field_path=field_path,
                            value=field.value_cls(**{'label': v.label, subfield.name: subfield_value}),
                        )
                # Generate delete elements for all subfields of all labels not mentioned in the list of values
                for label in (label for label in supported_labels if label not in seen_labels):
                    for subfield in subfields:
                        yield self._delete_item_elem(field_path=FieldPath(field=field, label=label, subfield=subfield))
        else:
            yield self._set_item_elem(item_model=item_model, field_path=FieldPath(field=field), value=value)

    def get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
                    suppress_read_receipts):
        # Takes a list of (Item, fieldnames) tuples where 'Item' is a instance of a subclass of Item and 'fieldnames'
        # are the attribute names that were updated. Returns the XML for an UpdateItem call.
        # an UpdateItem request.
        if self.account.version.build >= EXCHANGE_2013_SP1:
            updateitem = create_element(
                'm:%s' % self.SERVICE_NAME,
                attrs=OrderedDict([
                    ('ConflictResolution', conflict_resolution),
                    ('MessageDisposition', message_disposition),
                    ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
                    ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'),
                ])
            )
        else:
            updateitem = create_element(
                'm:%s' % self.SERVICE_NAME,
                attrs=OrderedDict([
                    ('ConflictResolution', conflict_resolution),
                    ('MessageDisposition', message_disposition),
                    ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
                ])
            )
        itemchanges = create_element('m:ItemChanges')
        version = self.account.version
        for item, fieldnames in items:
            if not item.account:
                item.account = self.account
            if not fieldnames:
                raise ValueError('"fieldnames" must not be empty')
            itemchange = create_element('t:ItemChange')
            set_xml_value(itemchange, to_item_id(item, ItemId, version=version), version=version)
            updates = create_element('t:Updates')
            for elem in self._get_item_update_elems(item=item, fieldnames=fieldnames):
                updates.append(elem)
            itemchange.append(updates)
            itemchanges.append(itemchange)
        if not len(itemchanges):
            raise ValueError('"items" must not be empty')
        updateitem.append(itemchanges)
        return updateitem

Classes

class UpdateItem (*args, **kwargs)
Expand source code
class UpdateItem(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation"""

    SERVICE_NAME = 'UpdateItem'
    element_container_name = '{%s}Items' % MNS

    def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
             suppress_read_receipts):
        from ..items import CONFLICT_RESOLUTION_CHOICES, MESSAGE_DISPOSITION_CHOICES, \
            SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES, SEND_ONLY
        if conflict_resolution not in CONFLICT_RESOLUTION_CHOICES:
            raise ValueError("'conflict_resolution' %s must be one of %s" % (
                conflict_resolution, CONFLICT_RESOLUTION_CHOICES
            ))
        if message_disposition not in MESSAGE_DISPOSITION_CHOICES:
            raise ValueError("'message_disposition' %s must be one of %s" % (
                message_disposition, MESSAGE_DISPOSITION_CHOICES
            ))
        if send_meeting_invitations_or_cancellations not in SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES:
            raise ValueError("'send_meeting_invitations_or_cancellations' %s must be one of %s" % (
                send_meeting_invitations_or_cancellations, SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES
            ))
        if suppress_read_receipts not in (True, False):
            raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts)
        if message_disposition == SEND_ONLY:
            raise ValueError('Cannot send-only existing objects. Use SendItem service instead')
        return self._elems_to_objs(self._chunked_get_elements(
            self.get_payload,
            items=items,
            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 _elems_to_objs(self, elems):
        from ..items import Item
        for elem in elems:
            if isinstance(elem, (Exception, type(None))):
                yield elem
                continue
            yield Item.id_from_xml(elem)

    def _delete_item_elem(self, field_path):
        deleteitemfield = create_element('t:DeleteItemField')
        return set_xml_value(deleteitemfield, field_path, version=self.account.version)

    def _set_item_elem(self, item_model, field_path, value):
        setitemfield = create_element('t:SetItemField')
        set_xml_value(setitemfield, field_path, version=self.account.version)
        item_elem = create_element(item_model.request_tag())
        field_elem = field_path.field.to_xml(value, version=self.account.version)
        set_xml_value(item_elem, field_elem, version=self.account.version)
        setitemfield.append(item_elem)
        return setitemfield

    @staticmethod
    def _sorted_fields(item_model, fieldnames):
        # Take a list of fieldnames and return the (unique) fields in the order they are mentioned in item_class.FIELDS.
        # Checks that all fieldnames are valid.
        unique_fieldnames = list(OrderedDict.fromkeys(fieldnames))  # Make field names unique ,but keep ordering
        # Loop over FIELDS and not supported_fields(). Upstream should make sure not to update a non-supported field.
        for f in item_model.FIELDS:
            if f.name in unique_fieldnames:
                unique_fieldnames.remove(f.name)
                yield f
        if unique_fieldnames:
            raise ValueError("Field name(s) %s are not valid for a '%s' item" % (
                ', '.join("'%s'" % f for f in unique_fieldnames), item_model.__name__))

    def _get_item_update_elems(self, item, fieldnames):
        from ..items import CalendarItem
        fieldnames_copy = list(fieldnames)

        if item.__class__ == CalendarItem:
            # For CalendarItem items where we update 'start' or 'end', we want to update internal timezone fields
            item.clean_timezone_fields(version=self.account.version)  # Possibly also sets timezone values
            for field_name in ('start', 'end'):
                if field_name in fieldnames_copy:
                    tz_field_name = item.tz_field_for_field_name(field_name).name
                    if tz_field_name not in fieldnames_copy:
                        fieldnames_copy.append(tz_field_name)

        for field in self._sorted_fields(item_model=item.__class__, fieldnames=fieldnames_copy):
            if field.is_read_only:
                raise ValueError('%s is a read-only field' % field.name)
            value = self._get_item_value(item, field)
            if value is None or (field.is_list and not value):
                # A value of None or [] means we want to remove this field from the item
                yield from self._get_delete_item_elems(field=field)
            else:
                yield from self._get_set_item_elems(item_model=item.__class__, field=field, value=value)

    def _get_item_value(self, item, field):
        from ..items import CalendarItem
        value = field.clean(getattr(item, field.name), version=self.account.version)  # Make sure the value is OK
        if item.__class__ == CalendarItem:
            # For CalendarItem items where we update 'start' or 'end', we want to send values in the local timezone
            if field.name in ('start', 'end'):
                if type(value) is EWSDate:
                    # EWS always expects a datetime
                    return item.date_to_datetime(field_name=field.name)
                tz_field_name = item.tz_field_for_field_name(field.name).name
                return value.astimezone(getattr(item, tz_field_name))
        return value

    def _get_delete_item_elems(self, field):
        if field.is_required or field.is_required_after_save:
            raise ValueError('%s is a required field and may not be deleted' % field.name)
        for field_path in FieldPath(field=field).expand(version=self.account.version):
            yield self._delete_item_elem(field_path=field_path)

    def _get_set_item_elems(self, item_model, field, value):
        if isinstance(field, IndexedField):
            # Generate either set or delete elements for all combinations of labels and subfields
            supported_labels = field.value_cls.get_field_by_fieldname('label')\
                .supported_choices(version=self.account.version)
            seen_labels = set()
            subfields = field.value_cls.supported_fields(version=self.account.version)
            for v in value:
                seen_labels.add(v.label)
                for subfield in subfields:
                    field_path = FieldPath(field=field, label=v.label, subfield=subfield)
                    subfield_value = getattr(v, subfield.name)
                    if not subfield_value:
                        # Generate delete elements for blank subfield values
                        yield self._delete_item_elem(field_path=field_path)
                    else:
                        # Generate set elements for non-null subfield values
                        yield self._set_item_elem(
                            item_model=item_model,
                            field_path=field_path,
                            value=field.value_cls(**{'label': v.label, subfield.name: subfield_value}),
                        )
                # Generate delete elements for all subfields of all labels not mentioned in the list of values
                for label in (label for label in supported_labels if label not in seen_labels):
                    for subfield in subfields:
                        yield self._delete_item_elem(field_path=FieldPath(field=field, label=label, subfield=subfield))
        else:
            yield self._set_item_elem(item_model=item_model, field_path=FieldPath(field=field), value=value)

    def get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
                    suppress_read_receipts):
        # Takes a list of (Item, fieldnames) tuples where 'Item' is a instance of a subclass of Item and 'fieldnames'
        # are the attribute names that were updated. Returns the XML for an UpdateItem call.
        # an UpdateItem request.
        if self.account.version.build >= EXCHANGE_2013_SP1:
            updateitem = create_element(
                'm:%s' % self.SERVICE_NAME,
                attrs=OrderedDict([
                    ('ConflictResolution', conflict_resolution),
                    ('MessageDisposition', message_disposition),
                    ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
                    ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'),
                ])
            )
        else:
            updateitem = create_element(
                'm:%s' % self.SERVICE_NAME,
                attrs=OrderedDict([
                    ('ConflictResolution', conflict_resolution),
                    ('MessageDisposition', message_disposition),
                    ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
                ])
            )
        itemchanges = create_element('m:ItemChanges')
        version = self.account.version
        for item, fieldnames in items:
            if not item.account:
                item.account = self.account
            if not fieldnames:
                raise ValueError('"fieldnames" must not be empty')
            itemchange = create_element('t:ItemChange')
            set_xml_value(itemchange, to_item_id(item, ItemId, version=version), version=version)
            updates = create_element('t:Updates')
            for elem in self._get_item_update_elems(item=item, fieldnames=fieldnames):
                updates.append(elem)
            itemchange.append(updates)
            itemchanges.append(itemchange)
        if not len(itemchanges):
            raise ValueError('"items" must not be empty')
        updateitem.append(itemchanges)
        return updateitem

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, suppress_read_receipts)
Expand source code
def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
         suppress_read_receipts):
    from ..items import CONFLICT_RESOLUTION_CHOICES, MESSAGE_DISPOSITION_CHOICES, \
        SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES, SEND_ONLY
    if conflict_resolution not in CONFLICT_RESOLUTION_CHOICES:
        raise ValueError("'conflict_resolution' %s must be one of %s" % (
            conflict_resolution, CONFLICT_RESOLUTION_CHOICES
        ))
    if message_disposition not in MESSAGE_DISPOSITION_CHOICES:
        raise ValueError("'message_disposition' %s must be one of %s" % (
            message_disposition, MESSAGE_DISPOSITION_CHOICES
        ))
    if send_meeting_invitations_or_cancellations not in SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES:
        raise ValueError("'send_meeting_invitations_or_cancellations' %s must be one of %s" % (
            send_meeting_invitations_or_cancellations, SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES
        ))
    if suppress_read_receipts not in (True, False):
        raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts)
    if message_disposition == SEND_ONLY:
        raise ValueError('Cannot send-only existing objects. Use SendItem service instead')
    return self._elems_to_objs(self._chunked_get_elements(
        self.get_payload,
        items=items,
        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 get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, suppress_read_receipts)
Expand source code
def get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations,
                suppress_read_receipts):
    # Takes a list of (Item, fieldnames) tuples where 'Item' is a instance of a subclass of Item and 'fieldnames'
    # are the attribute names that were updated. Returns the XML for an UpdateItem call.
    # an UpdateItem request.
    if self.account.version.build >= EXCHANGE_2013_SP1:
        updateitem = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=OrderedDict([
                ('ConflictResolution', conflict_resolution),
                ('MessageDisposition', message_disposition),
                ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
                ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'),
            ])
        )
    else:
        updateitem = create_element(
            'm:%s' % self.SERVICE_NAME,
            attrs=OrderedDict([
                ('ConflictResolution', conflict_resolution),
                ('MessageDisposition', message_disposition),
                ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations),
            ])
        )
    itemchanges = create_element('m:ItemChanges')
    version = self.account.version
    for item, fieldnames in items:
        if not item.account:
            item.account = self.account
        if not fieldnames:
            raise ValueError('"fieldnames" must not be empty')
        itemchange = create_element('t:ItemChange')
        set_xml_value(itemchange, to_item_id(item, ItemId, version=version), version=version)
        updates = create_element('t:Updates')
        for elem in self._get_item_update_elems(item=item, fieldnames=fieldnames):
            updates.append(elem)
        itemchange.append(updates)
        itemchanges.append(itemchange)
    if not len(itemchanges):
        raise ValueError('"items" must not be empty')
    updateitem.append(itemchanges)
    return updateitem

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/update_user_configuration.html000066400000000000000000000306611414601472700271700ustar00rootroot00000000000000 exchangelib.services.update_user_configuration API documentation

Module exchangelib.services.update_user_configuration

Expand source code
from .common import EWSAccountService
from ..util import create_element, set_xml_value


class UpdateUserConfiguration(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateuserconfiguration-operation
    """

    SERVICE_NAME = 'UpdateUserConfiguration'
    returns_elements = False

    def call(self, user_configuration):
        return self._get_elements(payload=self.get_payload(user_configuration=user_configuration))

    def get_payload(self, user_configuration):
        updateuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(updateuserconfiguration, user_configuration, version=self.account.version)
        return updateuserconfiguration

Classes

class UpdateUserConfiguration (*args, **kwargs)
Expand source code
class UpdateUserConfiguration(EWSAccountService):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateuserconfiguration-operation
    """

    SERVICE_NAME = 'UpdateUserConfiguration'
    returns_elements = False

    def call(self, user_configuration):
        return self._get_elements(payload=self.get_payload(user_configuration=user_configuration))

    def get_payload(self, user_configuration):
        updateuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
        set_xml_value(updateuserconfiguration, user_configuration, version=self.account.version)
        return updateuserconfiguration

Ancestors

Class variables

var SERVICE_NAME
var returns_elements

Methods

def call(self, user_configuration)
Expand source code
def call(self, user_configuration):
    return self._get_elements(payload=self.get_payload(user_configuration=user_configuration))
def get_payload(self, user_configuration)
Expand source code
def get_payload(self, user_configuration):
    updateuserconfiguration = create_element('m:%s' % self.SERVICE_NAME)
    set_xml_value(updateuserconfiguration, user_configuration, version=self.account.version)
    return updateuserconfiguration

Inherited members

exchangelib-4.6.1/docs/exchangelib/services/upload_items.html000066400000000000000000000416441414601472700244110ustar00rootroot00000000000000 exchangelib.services.upload_items API documentation

Module exchangelib.services.upload_items

Expand source code
from .common import EWSAccountService, to_item_id
from ..properties import ItemId, ParentFolderId
from ..util import create_element, set_xml_value, add_xml_child, MNS


class UploadItems(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/uploaditems-operation
    """

    SERVICE_NAME = 'UploadItems'
    element_container_name = '{%s}ItemId' % MNS

    def call(self, items):
        # _pool_requests expects 'items', not 'data'
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items))

    def get_payload(self, items):
        """Upload given items to given account.

        'items' is an iterable of tuples where the first element is a Folder instance representing the ParentFolder
        that the item will be placed in and the second element is a tuple containing an optional ItemId, an optional
        Item.is_associated boolean, and a Data string returned from an ExportItems.
        call.

        :param items:
        """
        uploaditems = create_element('m:%s' % self.SERVICE_NAME)
        itemselement = create_element('m:Items')
        uploaditems.append(itemselement)
        for parent_folder, (item_id, is_associated, data_str) in items:
            # TODO: The full spec also allows the "UpdateOrCreate" create action.
            item = create_element('t:Item', attrs=dict(CreateAction='Update' if item_id else 'CreateNew'))
            if is_associated is not None:
                item.set('IsAssociated', 'true' if is_associated else 'false')
            parentfolderid = ParentFolderId(parent_folder.id, parent_folder.changekey)
            set_xml_value(item, parentfolderid, version=self.account.version)
            if item_id:
                itemid = to_item_id(item_id, ItemId, version=self.account.version)
                set_xml_value(item, itemid, version=self.account.version)
            add_xml_child(item, 't:Data', data_str)
            itemselement.append(item)
        return uploaditems

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield elem.get(ItemId.ID_ATTR), elem.get(ItemId.CHANGEKEY_ATTR)

    @classmethod
    def _get_elements_in_container(cls, container):
        return [container]

Classes

class UploadItems (*args, **kwargs)
Expand source code
class UploadItems(EWSAccountService):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/uploaditems-operation
    """

    SERVICE_NAME = 'UploadItems'
    element_container_name = '{%s}ItemId' % MNS

    def call(self, items):
        # _pool_requests expects 'items', not 'data'
        return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items))

    def get_payload(self, items):
        """Upload given items to given account.

        'items' is an iterable of tuples where the first element is a Folder instance representing the ParentFolder
        that the item will be placed in and the second element is a tuple containing an optional ItemId, an optional
        Item.is_associated boolean, and a Data string returned from an ExportItems.
        call.

        :param items:
        """
        uploaditems = create_element('m:%s' % self.SERVICE_NAME)
        itemselement = create_element('m:Items')
        uploaditems.append(itemselement)
        for parent_folder, (item_id, is_associated, data_str) in items:
            # TODO: The full spec also allows the "UpdateOrCreate" create action.
            item = create_element('t:Item', attrs=dict(CreateAction='Update' if item_id else 'CreateNew'))
            if is_associated is not None:
                item.set('IsAssociated', 'true' if is_associated else 'false')
            parentfolderid = ParentFolderId(parent_folder.id, parent_folder.changekey)
            set_xml_value(item, parentfolderid, version=self.account.version)
            if item_id:
                itemid = to_item_id(item_id, ItemId, version=self.account.version)
                set_xml_value(item, itemid, version=self.account.version)
            add_xml_child(item, 't:Data', data_str)
            itemselement.append(item)
        return uploaditems

    def _elems_to_objs(self, elems):
        for elem in elems:
            if isinstance(elem, Exception):
                yield elem
                continue
            yield elem.get(ItemId.ID_ATTR), elem.get(ItemId.CHANGEKEY_ATTR)

    @classmethod
    def _get_elements_in_container(cls, container):
        return [container]

Ancestors

Class variables

var SERVICE_NAME
var element_container_name

Methods

def call(self, items)
Expand source code
def call(self, items):
    # _pool_requests expects 'items', not 'data'
    return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items))
def get_payload(self, items)

Upload given items to given account.

'items' is an iterable of tuples where the first element is a Folder instance representing the ParentFolder that the item will be placed in and the second element is a tuple containing an optional ItemId, an optional Item.is_associated boolean, and a Data string returned from an ExportItems. call.

:param items:

Expand source code
def get_payload(self, items):
    """Upload given items to given account.

    'items' is an iterable of tuples where the first element is a Folder instance representing the ParentFolder
    that the item will be placed in and the second element is a tuple containing an optional ItemId, an optional
    Item.is_associated boolean, and a Data string returned from an ExportItems.
    call.

    :param items:
    """
    uploaditems = create_element('m:%s' % self.SERVICE_NAME)
    itemselement = create_element('m:Items')
    uploaditems.append(itemselement)
    for parent_folder, (item_id, is_associated, data_str) in items:
        # TODO: The full spec also allows the "UpdateOrCreate" create action.
        item = create_element('t:Item', attrs=dict(CreateAction='Update' if item_id else 'CreateNew'))
        if is_associated is not None:
            item.set('IsAssociated', 'true' if is_associated else 'false')
        parentfolderid = ParentFolderId(parent_folder.id, parent_folder.changekey)
        set_xml_value(item, parentfolderid, version=self.account.version)
        if item_id:
            itemid = to_item_id(item_id, ItemId, version=self.account.version)
            set_xml_value(item, itemid, version=self.account.version)
        add_xml_child(item, 't:Data', data_str)
        itemselement.append(item)
    return uploaditems

Inherited members

exchangelib-4.6.1/docs/exchangelib/settings.html000066400000000000000000000602731414601472700217400ustar00rootroot00000000000000 exchangelib.settings API documentation

Module exchangelib.settings

Expand source code
import datetime

from .ewsdatetime import UTC
from .fields import DateTimeField, MessageField, ChoiceField, Choice
from .properties import EWSElement, OutOfOffice
from .util import create_element, set_xml_value


class OofSettings(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/oofsettings"""

    ELEMENT_NAME = 'OofSettings'
    REQUEST_ELEMENT_NAME = 'UserOofSettings'

    ENABLED = 'Enabled'
    SCHEDULED = 'Scheduled'
    DISABLED = 'Disabled'
    STATE_CHOICES = (ENABLED, SCHEDULED, DISABLED)

    state = ChoiceField(field_uri='OofState', is_required=True, choices={Choice(c) for c in STATE_CHOICES})
    external_audience = ChoiceField(field_uri='ExternalAudience',
                                    choices={Choice('None'), Choice('Known'), Choice('All')}, default='All')
    start = DateTimeField(field_uri='StartTime')
    end = DateTimeField(field_uri='EndTime')
    internal_reply = MessageField(field_uri='InternalReply')
    external_reply = MessageField(field_uri='ExternalReply')

    def clean(self, version=None):
        super().clean(version=version)
        if self.state == self.SCHEDULED:
            if not self.start or not self.end:
                raise ValueError("'start' and 'end' must be set when state is '%s'" % self.SCHEDULED)
            if self.start >= self.end:
                raise ValueError("'start' must be before 'end'")
            if self.end < datetime.datetime.now(tz=UTC):
                raise ValueError("'end' must be in the future")
        if self.state != self.DISABLED and (not self.internal_reply or not self.external_reply):
            raise ValueError("'internal_reply' and 'external_reply' must be set when state is not '%s'" % self.DISABLED)

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {}
        for attr in ('state', 'external_audience', 'internal_reply', 'external_reply'):
            f = cls.get_field_by_fieldname(attr)
            kwargs[attr] = f.from_xml(elem=elem, account=account)
        kwargs.update(OutOfOffice.duration_to_start_end(elem=elem, account=account))
        cls._clear(elem)
        return cls(**kwargs)

    def to_xml(self, version):
        self.clean(version=version)
        elem = create_element('t:%s' % self.REQUEST_ELEMENT_NAME)
        for attr in ('state', 'external_audience'):
            value = getattr(self, attr)
            if value is None:
                continue
            f = self.get_field_by_fieldname(attr)
            set_xml_value(elem, f.to_xml(value, version=version), version=version)
        if self.start or self.end:
            duration = create_element('t:Duration')
            if self.start:
                f = self.get_field_by_fieldname('start')
                set_xml_value(duration, f.to_xml(self.start, version=version), version)
            if self.end:
                f = self.get_field_by_fieldname('end')
                set_xml_value(duration, f.to_xml(self.end, version=version), version)
            elem.append(duration)
        for attr in ('internal_reply', 'external_reply'):
            value = getattr(self, attr)
            if value is None:
                value = ''  # The value can be empty, but the XML element must always be present
            f = self.get_field_by_fieldname(attr)
            set_xml_value(elem, f.to_xml(value, version=version), version)
        return elem

    def __hash__(self):
        # Customize comparison
        if self.state == self.DISABLED:
            # All values except state are ignored by the server
            relevant_attrs = ('state',)
        elif self.state != self.SCHEDULED:
            # 'start' and 'end' values are ignored by the server, and the server always returns today's date
            relevant_attrs = tuple(f.name for f in self.FIELDS if f.name not in ('start', 'end'))
        else:
            relevant_attrs = tuple(f.name for f in self.FIELDS)
        return hash(tuple(getattr(self, attr) for attr in relevant_attrs))

Classes

class OofSettings (**kwargs)
Expand source code
class OofSettings(EWSElement):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/oofsettings"""

    ELEMENT_NAME = 'OofSettings'
    REQUEST_ELEMENT_NAME = 'UserOofSettings'

    ENABLED = 'Enabled'
    SCHEDULED = 'Scheduled'
    DISABLED = 'Disabled'
    STATE_CHOICES = (ENABLED, SCHEDULED, DISABLED)

    state = ChoiceField(field_uri='OofState', is_required=True, choices={Choice(c) for c in STATE_CHOICES})
    external_audience = ChoiceField(field_uri='ExternalAudience',
                                    choices={Choice('None'), Choice('Known'), Choice('All')}, default='All')
    start = DateTimeField(field_uri='StartTime')
    end = DateTimeField(field_uri='EndTime')
    internal_reply = MessageField(field_uri='InternalReply')
    external_reply = MessageField(field_uri='ExternalReply')

    def clean(self, version=None):
        super().clean(version=version)
        if self.state == self.SCHEDULED:
            if not self.start or not self.end:
                raise ValueError("'start' and 'end' must be set when state is '%s'" % self.SCHEDULED)
            if self.start >= self.end:
                raise ValueError("'start' must be before 'end'")
            if self.end < datetime.datetime.now(tz=UTC):
                raise ValueError("'end' must be in the future")
        if self.state != self.DISABLED and (not self.internal_reply or not self.external_reply):
            raise ValueError("'internal_reply' and 'external_reply' must be set when state is not '%s'" % self.DISABLED)

    @classmethod
    def from_xml(cls, elem, account):
        kwargs = {}
        for attr in ('state', 'external_audience', 'internal_reply', 'external_reply'):
            f = cls.get_field_by_fieldname(attr)
            kwargs[attr] = f.from_xml(elem=elem, account=account)
        kwargs.update(OutOfOffice.duration_to_start_end(elem=elem, account=account))
        cls._clear(elem)
        return cls(**kwargs)

    def to_xml(self, version):
        self.clean(version=version)
        elem = create_element('t:%s' % self.REQUEST_ELEMENT_NAME)
        for attr in ('state', 'external_audience'):
            value = getattr(self, attr)
            if value is None:
                continue
            f = self.get_field_by_fieldname(attr)
            set_xml_value(elem, f.to_xml(value, version=version), version=version)
        if self.start or self.end:
            duration = create_element('t:Duration')
            if self.start:
                f = self.get_field_by_fieldname('start')
                set_xml_value(duration, f.to_xml(self.start, version=version), version)
            if self.end:
                f = self.get_field_by_fieldname('end')
                set_xml_value(duration, f.to_xml(self.end, version=version), version)
            elem.append(duration)
        for attr in ('internal_reply', 'external_reply'):
            value = getattr(self, attr)
            if value is None:
                value = ''  # The value can be empty, but the XML element must always be present
            f = self.get_field_by_fieldname(attr)
            set_xml_value(elem, f.to_xml(value, version=version), version)
        return elem

    def __hash__(self):
        # Customize comparison
        if self.state == self.DISABLED:
            # All values except state are ignored by the server
            relevant_attrs = ('state',)
        elif self.state != self.SCHEDULED:
            # 'start' and 'end' values are ignored by the server, and the server always returns today's date
            relevant_attrs = tuple(f.name for f in self.FIELDS if f.name not in ('start', 'end'))
        else:
            relevant_attrs = tuple(f.name for f in self.FIELDS)
        return hash(tuple(getattr(self, attr) for attr in relevant_attrs))

Ancestors

Class variables

var DISABLED
var ELEMENT_NAME
var ENABLED
var FIELDS
var REQUEST_ELEMENT_NAME
var SCHEDULED
var STATE_CHOICES

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    kwargs = {}
    for attr in ('state', 'external_audience', 'internal_reply', 'external_reply'):
        f = cls.get_field_by_fieldname(attr)
        kwargs[attr] = f.from_xml(elem=elem, account=account)
    kwargs.update(OutOfOffice.duration_to_start_end(elem=elem, account=account))
    cls._clear(elem)
    return cls(**kwargs)

Instance variables

var end
var external_audience
var external_reply
var internal_reply
var start
var state

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)
    if self.state == self.SCHEDULED:
        if not self.start or not self.end:
            raise ValueError("'start' and 'end' must be set when state is '%s'" % self.SCHEDULED)
        if self.start >= self.end:
            raise ValueError("'start' must be before 'end'")
        if self.end < datetime.datetime.now(tz=UTC):
            raise ValueError("'end' must be in the future")
    if self.state != self.DISABLED and (not self.internal_reply or not self.external_reply):
        raise ValueError("'internal_reply' and 'external_reply' must be set when state is not '%s'" % self.DISABLED)
def to_xml(self, version)
Expand source code
def to_xml(self, version):
    self.clean(version=version)
    elem = create_element('t:%s' % self.REQUEST_ELEMENT_NAME)
    for attr in ('state', 'external_audience'):
        value = getattr(self, attr)
        if value is None:
            continue
        f = self.get_field_by_fieldname(attr)
        set_xml_value(elem, f.to_xml(value, version=version), version=version)
    if self.start or self.end:
        duration = create_element('t:Duration')
        if self.start:
            f = self.get_field_by_fieldname('start')
            set_xml_value(duration, f.to_xml(self.start, version=version), version)
        if self.end:
            f = self.get_field_by_fieldname('end')
            set_xml_value(duration, f.to_xml(self.end, version=version), version)
        elem.append(duration)
    for attr in ('internal_reply', 'external_reply'):
        value = getattr(self, attr)
        if value is None:
            value = ''  # The value can be empty, but the XML element must always be present
        f = self.get_field_by_fieldname(attr)
        set_xml_value(elem, f.to_xml(value, version=version), version)
    return elem

Inherited members

exchangelib-4.6.1/docs/exchangelib/transport.html000066400000000000000000000647221414601472700221370ustar00rootroot00000000000000 exchangelib.transport API documentation

Module exchangelib.transport

Expand source code
import logging
import time

import requests.auth
import requests_ntlm
import requests_oauthlib

from .errors import UnauthorizedError, TransportError
from .util import create_element, add_xml_child, xml_to_str, ns_translation, _back_off_if_needed, \
    _retry_after, DummyResponse, CONNECTION_ERRORS

log = logging.getLogger(__name__)

# Authentication method enums
NOAUTH = 'no authentication'
NTLM = 'NTLM'
BASIC = 'basic'
DIGEST = 'digest'
GSSAPI = 'gssapi'
SSPI = 'sspi'
OAUTH2 = 'OAuth 2.0'
CBA = 'CBA'  # Certificate Based Authentication

# The auth types that must be accompanied by a credentials object
CREDENTIALS_REQUIRED = (NTLM, BASIC, DIGEST, OAUTH2)

AUTH_TYPE_MAP = {
    NTLM: requests_ntlm.HttpNtlmAuth,
    BASIC: requests.auth.HTTPBasicAuth,
    DIGEST: requests.auth.HTTPDigestAuth,
    OAUTH2: requests_oauthlib.OAuth2,
    CBA: None,
    NOAUTH: None,
}
try:
    import requests_gssapi
    AUTH_TYPE_MAP[GSSAPI] = requests_gssapi.HTTPSPNEGOAuth
except ImportError:
    # Kerberos auth is optional
    pass
try:
    import requests_negotiate_sspi
    AUTH_TYPE_MAP[SSPI] = requests_negotiate_sspi.HttpNegotiateAuth
except ImportError:
    # SSPI auth is optional
    pass

DEFAULT_ENCODING = 'utf-8'
DEFAULT_HEADERS = {'Content-Type': 'text/xml; charset=%s' % DEFAULT_ENCODING, 'Accept-Encoding': 'gzip, deflate'}


def wrap(content, api_version, account_to_impersonate=None, timezone=None):
    """Generate the necessary boilerplate XML for a raw SOAP request. The XML is specific to the server version.
    ExchangeImpersonation allows to act as the user we want to impersonate.

    RequestServerVersion element on MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/requestserverversion

    ExchangeImpersonation element on MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/exchangeimpersonation

    TimeZoneContent element on MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/timezonecontext

    :param content:
    :param api_version:
    :param account_to_impersonate:  (Default value = None)
    :param timezone:  (Default value = None)
    """
    envelope = create_element('s:Envelope', nsmap=ns_translation)
    header = create_element('s:Header')
    requestserverversion = create_element('t:RequestServerVersion', attrs=dict(Version=api_version))
    header.append(requestserverversion)
    if account_to_impersonate:
        exchangeimpersonation = create_element('t:ExchangeImpersonation')
        connectingsid = create_element('t:ConnectingSID')
        # 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
        for attr, tag in (
            ('sid', 'SID'),
            ('upn', 'PrincipalName'),
            ('smtp_address', 'SmtpAddress'),
            ('primary_smtp_address', 'PrimarySmtpAddress'),
        ):
            val = getattr(account_to_impersonate, attr)
            if val:
                add_xml_child(connectingsid, 't:%s' % tag, val)
                break
        exchangeimpersonation.append(connectingsid)
        header.append(exchangeimpersonation)
    if timezone:
        timezonecontext = create_element('t:TimeZoneContext')
        timezonedefinition = create_element('t:TimeZoneDefinition', attrs=dict(Id=timezone.ms_id))
        timezonecontext.append(timezonedefinition)
        header.append(timezonecontext)
    envelope.append(header)
    body = create_element('s:Body')
    body.append(content)
    envelope.append(body)
    return xml_to_str(envelope, encoding=DEFAULT_ENCODING, xml_declaration=True)


def get_auth_instance(auth_type, **kwargs):
    """Return an *Auth instance suitable for the requests package.

    :param auth_type:
    :param kwargs:
    """
    model = AUTH_TYPE_MAP[auth_type]
    if model is None:
        return None
    if auth_type == GSSAPI:
        # Kerberos auth relies on credentials supplied via a ticket available externally to this library
        return model()
    if auth_type == SSPI:
        # SSPI auth does not require credentials, but can have it
        return model(**kwargs)
    return model(**kwargs)


def get_service_authtype(service_endpoint, retry_policy, api_versions, name):
    # Get auth type by tasting headers from the server. Only do POST requests. HEAD is too error prone, and some servers
    # are set up to redirect to OWA on all requests except POST to /EWS/Exchange.asmx
    #
    # We don't know the API version yet, but we need it to create a valid request because some Exchange servers only
    # respond when given a valid request. Try all known versions. Gross.
    from .protocol import BaseProtocol
    retry = 0
    wait = 10  # seconds
    t_start = time.monotonic()
    headers = DEFAULT_HEADERS.copy()
    for api_version in api_versions:
        data = dummy_xml(api_version=api_version, name=name)
        log.debug('Requesting %s from %s', data, service_endpoint)
        while True:
            _back_off_if_needed(retry_policy.back_off_until)
            log.debug('Trying to get service auth type for %s', service_endpoint)
            with BaseProtocol.raw_session(service_endpoint) as s:
                try:
                    r = s.post(url=service_endpoint, headers=headers, data=data, allow_redirects=False,
                               timeout=BaseProtocol.TIMEOUT)
                    r.close()  # Release memory
                    break
                except CONNECTION_ERRORS as e:
                    # Don't retry on TLS errors. They will most likely be persistent.
                    total_wait = time.monotonic() - t_start
                    r = DummyResponse(url=service_endpoint, headers={}, request_headers=headers)
                    if retry_policy.may_retry_on_error(response=r, wait=total_wait):
                        wait = _retry_after(r, wait)
                        log.info("Connection error on URL %s (retry %s, error: %s). Cool down %s secs",
                                 service_endpoint, retry, e, wait)
                        retry_policy.back_off(wait)
                        retry += 1
                        continue
                    raise TransportError(str(e)) from e
        if r.status_code not in (200, 401):
            log.debug('Unexpected response: %s %s', r.status_code, r.reason)
            continue
        try:
            auth_type = get_auth_method_from_response(response=r)
            log.debug('Auth type is %s', auth_type)
            return auth_type, api_version
        except UnauthorizedError:
            continue
    raise TransportError('Failed to get auth type from service')


def get_auth_method_from_response(response):
    # First, get the auth method from headers. Then, test credentials. Don't handle redirects - burden is on caller.
    log.debug('Request headers: %s', response.request.headers)
    log.debug('Response headers: %s', response.headers)
    if response.status_code == 200:
        return NOAUTH
    # Get auth type from headers
    for key, val in response.headers.items():
        if key.lower() == 'www-authenticate':
            # Requests will combine multiple HTTP headers into one in 'request.headers'
            vals = _tokenize(val.lower())
            for v in vals:
                if v.startswith('realm'):
                    realm = v.split('=')[1].strip('"')
                    log.debug('realm: %s', realm)
            # Prefer most secure auth method if more than one is offered. See discussion at
            # http://docs.oracle.com/javase/7/docs/technotes/guides/net/http-auth.html
            if 'digest' in vals:
                return DIGEST
            if 'ntlm' in vals:
                return NTLM
            if 'basic' in vals:
                return BASIC
    raise UnauthorizedError('No compatible auth type was reported by server')


def _tokenize(val):
    # Splits cookie auth values
    auth_methods = []
    auth_method = ''
    quote = False
    for c in val:
        if c in (' ', ',') and not quote:
            if auth_method not in ('', ','):
                auth_methods.append(auth_method)
            auth_method = ''
            continue
        if c == '"':
            auth_method += c
            if quote:
                auth_methods.append(auth_method)
                auth_method = ''
            quote = not quote
            continue
        auth_method += c
    if auth_method:
        auth_methods.append(auth_method)
    return auth_methods


def dummy_xml(api_version, name):
    # Generate a minimal, valid EWS request
    from .services import ResolveNames  # Avoid circular import
    return wrap(content=ResolveNames(protocol=None).get_payload(
        unresolved_entries=[name],
        parent_folders=None,
        return_full_contact_data=False,
        search_scope=None,
        contact_data_shape=None,
    ), api_version=api_version)

Functions

def dummy_xml(api_version, name)
Expand source code
def dummy_xml(api_version, name):
    # Generate a minimal, valid EWS request
    from .services import ResolveNames  # Avoid circular import
    return wrap(content=ResolveNames(protocol=None).get_payload(
        unresolved_entries=[name],
        parent_folders=None,
        return_full_contact_data=False,
        search_scope=None,
        contact_data_shape=None,
    ), api_version=api_version)
def get_auth_instance(auth_type, **kwargs)

Return an *Auth instance suitable for the requests package.

:param auth_type: :param kwargs:

Expand source code
def get_auth_instance(auth_type, **kwargs):
    """Return an *Auth instance suitable for the requests package.

    :param auth_type:
    :param kwargs:
    """
    model = AUTH_TYPE_MAP[auth_type]
    if model is None:
        return None
    if auth_type == GSSAPI:
        # Kerberos auth relies on credentials supplied via a ticket available externally to this library
        return model()
    if auth_type == SSPI:
        # SSPI auth does not require credentials, but can have it
        return model(**kwargs)
    return model(**kwargs)
def get_auth_method_from_response(response)
Expand source code
def get_auth_method_from_response(response):
    # First, get the auth method from headers. Then, test credentials. Don't handle redirects - burden is on caller.
    log.debug('Request headers: %s', response.request.headers)
    log.debug('Response headers: %s', response.headers)
    if response.status_code == 200:
        return NOAUTH
    # Get auth type from headers
    for key, val in response.headers.items():
        if key.lower() == 'www-authenticate':
            # Requests will combine multiple HTTP headers into one in 'request.headers'
            vals = _tokenize(val.lower())
            for v in vals:
                if v.startswith('realm'):
                    realm = v.split('=')[1].strip('"')
                    log.debug('realm: %s', realm)
            # Prefer most secure auth method if more than one is offered. See discussion at
            # http://docs.oracle.com/javase/7/docs/technotes/guides/net/http-auth.html
            if 'digest' in vals:
                return DIGEST
            if 'ntlm' in vals:
                return NTLM
            if 'basic' in vals:
                return BASIC
    raise UnauthorizedError('No compatible auth type was reported by server')
def get_service_authtype(service_endpoint, retry_policy, api_versions, name)
Expand source code
def get_service_authtype(service_endpoint, retry_policy, api_versions, name):
    # Get auth type by tasting headers from the server. Only do POST requests. HEAD is too error prone, and some servers
    # are set up to redirect to OWA on all requests except POST to /EWS/Exchange.asmx
    #
    # We don't know the API version yet, but we need it to create a valid request because some Exchange servers only
    # respond when given a valid request. Try all known versions. Gross.
    from .protocol import BaseProtocol
    retry = 0
    wait = 10  # seconds
    t_start = time.monotonic()
    headers = DEFAULT_HEADERS.copy()
    for api_version in api_versions:
        data = dummy_xml(api_version=api_version, name=name)
        log.debug('Requesting %s from %s', data, service_endpoint)
        while True:
            _back_off_if_needed(retry_policy.back_off_until)
            log.debug('Trying to get service auth type for %s', service_endpoint)
            with BaseProtocol.raw_session(service_endpoint) as s:
                try:
                    r = s.post(url=service_endpoint, headers=headers, data=data, allow_redirects=False,
                               timeout=BaseProtocol.TIMEOUT)
                    r.close()  # Release memory
                    break
                except CONNECTION_ERRORS as e:
                    # Don't retry on TLS errors. They will most likely be persistent.
                    total_wait = time.monotonic() - t_start
                    r = DummyResponse(url=service_endpoint, headers={}, request_headers=headers)
                    if retry_policy.may_retry_on_error(response=r, wait=total_wait):
                        wait = _retry_after(r, wait)
                        log.info("Connection error on URL %s (retry %s, error: %s). Cool down %s secs",
                                 service_endpoint, retry, e, wait)
                        retry_policy.back_off(wait)
                        retry += 1
                        continue
                    raise TransportError(str(e)) from e
        if r.status_code not in (200, 401):
            log.debug('Unexpected response: %s %s', r.status_code, r.reason)
            continue
        try:
            auth_type = get_auth_method_from_response(response=r)
            log.debug('Auth type is %s', auth_type)
            return auth_type, api_version
        except UnauthorizedError:
            continue
    raise TransportError('Failed to get auth type from service')
def wrap(content, api_version, account_to_impersonate=None, timezone=None)

Generate the necessary boilerplate XML for a raw SOAP request. The XML is specific to the server version. ExchangeImpersonation allows to act as the user we want to impersonate.

RequestServerVersion element on MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/requestserverversion

ExchangeImpersonation element on MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/exchangeimpersonation

TimeZoneContent element on MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/timezonecontext

:param content: :param api_version: :param account_to_impersonate: (Default value = None) :param timezone: (Default value = None)

Expand source code
def wrap(content, api_version, account_to_impersonate=None, timezone=None):
    """Generate the necessary boilerplate XML for a raw SOAP request. The XML is specific to the server version.
    ExchangeImpersonation allows to act as the user we want to impersonate.

    RequestServerVersion element on MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/requestserverversion

    ExchangeImpersonation element on MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/exchangeimpersonation

    TimeZoneContent element on MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/timezonecontext

    :param content:
    :param api_version:
    :param account_to_impersonate:  (Default value = None)
    :param timezone:  (Default value = None)
    """
    envelope = create_element('s:Envelope', nsmap=ns_translation)
    header = create_element('s:Header')
    requestserverversion = create_element('t:RequestServerVersion', attrs=dict(Version=api_version))
    header.append(requestserverversion)
    if account_to_impersonate:
        exchangeimpersonation = create_element('t:ExchangeImpersonation')
        connectingsid = create_element('t:ConnectingSID')
        # 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
        for attr, tag in (
            ('sid', 'SID'),
            ('upn', 'PrincipalName'),
            ('smtp_address', 'SmtpAddress'),
            ('primary_smtp_address', 'PrimarySmtpAddress'),
        ):
            val = getattr(account_to_impersonate, attr)
            if val:
                add_xml_child(connectingsid, 't:%s' % tag, val)
                break
        exchangeimpersonation.append(connectingsid)
        header.append(exchangeimpersonation)
    if timezone:
        timezonecontext = create_element('t:TimeZoneContext')
        timezonedefinition = create_element('t:TimeZoneDefinition', attrs=dict(Id=timezone.ms_id))
        timezonecontext.append(timezonedefinition)
        header.append(timezonecontext)
    envelope.append(header)
    body = create_element('s:Body')
    body.append(content)
    envelope.append(body)
    return xml_to_str(envelope, encoding=DEFAULT_ENCODING, xml_declaration=True)
exchangelib-4.6.1/docs/exchangelib/util.html000066400000000000000000003436501414601472700210600ustar00rootroot00000000000000 exchangelib.util API documentation

Module exchangelib.util

Expand source code
import datetime
import io
import itertools
import logging
import re
import socket
import time
import xml.sax.handler  # nosec
from base64 import b64decode, b64encode
from codecs import BOM_UTF8
from collections import OrderedDict
from decimal import Decimal
from functools import wraps
from threading import get_ident
from urllib.parse import urlparse

import isodate
import lxml.etree  # nosec
import requests.exceptions
from defusedxml.expatreader import DefusedExpatParser
from defusedxml.sax import _InputSource
from oauthlib.oauth2 import TokenExpiredError
from pygments import highlight
from pygments.formatters.terminal import TerminalFormatter
from pygments.lexers.html import XmlLexer

from .errors import TransportError, RateLimitError, RedirectError, RelativeRedirect, MalformedResponseError

log = logging.getLogger(__name__)
xml_log = logging.getLogger('%s.xml' % __name__)


def require_account(f):
    @wraps(f)
    def wrapper(self, *args, **kwargs):
        if not self.account:
            raise ValueError('%s must have an account' % self.__class__.__name__)
        return f(self, *args, **kwargs)
    return wrapper


def require_id(f):
    @wraps(f)
    def wrapper(self, *args, **kwargs):
        if not self.account:
            raise ValueError('%s must have an account' % self.__class__.__name__)
        if not self.id:
            raise ValueError('%s must have an ID' % self.__class__.__name__)
        return f(self, *args, **kwargs)
    return wrapper


class ParseError(lxml.etree.ParseError):
    """Used to wrap lxml ParseError in our own class."""


class ElementNotFound(Exception):
    """Raised when the expected element was not found in a response stream."""

    def __init__(self, msg, data):
        super().__init__(msg)
        self.data = data


# Regex of UTF-8 control characters that are illegal in XML 1.0 (and XML 1.1)
_ILLEGAL_XML_CHARS_RE = re.compile('[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]')

# XML namespaces
SOAPNS = 'http://schemas.xmlsoap.org/soap/envelope/'
MNS = 'http://schemas.microsoft.com/exchange/services/2006/messages'
TNS = 'http://schemas.microsoft.com/exchange/services/2006/types'
ENS = 'http://schemas.microsoft.com/exchange/services/2006/errors'
AUTODISCOVER_BASE_NS = 'http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006'
AUTODISCOVER_REQUEST_NS = 'http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006'
AUTODISCOVER_RESPONSE_NS = 'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a'

ns_translation = OrderedDict([
    ('s', SOAPNS),
    ('m', MNS),
    ('t', TNS),
])
for item in ns_translation.items():
    lxml.etree.register_namespace(*item)


def is_iterable(value, generators_allowed=False):
    """Check if value is a list-like object. Don't match generators and generator-like objects here by default, because
    callers don't necessarily guarantee that they only iterate the value once. Take care to not match string types and
    bytes.

    :param value: any type of object
    :param generators_allowed: if True, generators will be treated as iterable (Default value = False)

    :return: True or False
    """
    if generators_allowed:
        if not isinstance(value, (bytes, str)) and hasattr(value, '__iter__'):
            return True
    else:
        if isinstance(value, (tuple, list, set)):
            return True
    return False


def chunkify(iterable, chunksize):
    """Split an iterable into chunks of size ``chunksize``. The last chunk may be smaller than ``chunksize``.

    :param iterable:
    :param chunksize:
    :return:
    """
    from .queryset import QuerySet
    if hasattr(iterable, '__getitem__') and not isinstance(iterable, QuerySet):
        # tuple, list. QuerySet has __getitem__ but that evaluates the entire query greedily. We don't want that here.
        for i in range(0, len(iterable), chunksize):
            yield iterable[i:i + chunksize]
    else:
        # generator, set, map, QuerySet
        chunk = []
        for i in iterable:
            chunk.append(i)
            if len(chunk) == chunksize:
                yield chunk
                chunk = []
        if chunk:
            yield chunk


def peek(iterable):
    """Check if an iterable is empty and return status and the rewinded iterable.

    :param iterable:
    :return:
    """
    if hasattr(iterable, '__len__'):
        # tuple, list, set
        return not iterable, iterable
    # generator
    try:
        first = next(iterable)
    except StopIteration:
        return True, iterable
    # We can't rewind a generator. Instead, chain the first element and the rest of the generator
    return False, itertools.chain([first], iterable)


def xml_to_str(tree, encoding=None, xml_declaration=False):
    """Serialize an XML tree. Returns unicode if 'encoding' is None. Otherwise, we return encoded 'bytes'.

    :param tree:
    :param encoding:  (Default value = None)
    :param xml_declaration:  (Default value = False)
    :return:
    """
    if xml_declaration and not encoding:
        raise ValueError("'xml_declaration' is not supported when 'encoding' is None")
    if encoding:
        return lxml.etree.tostring(tree, encoding=encoding, xml_declaration=True)
    return lxml.etree.tostring(tree, encoding=str, xml_declaration=False)


def get_xml_attr(tree, name):
    elem = tree.find(name)
    if elem is None:  # Must compare with None, see XML docs
        return None
    return elem.text or None


def get_xml_attrs(tree, name):
    return [elem.text for elem in tree.findall(name) if elem.text is not None]


def value_to_xml_text(value):
    from .ewsdatetime import EWSTimeZone, EWSDateTime, EWSDate
    from .indexed_properties import PhoneNumber, EmailAddress
    from .properties import Mailbox, AssociatedCalendarItemId, Attendee, ConversationId
    # We can't just create a map and look up with type(value) because we want to support subtypes
    if isinstance(value, str):
        return safe_xml_value(value)
    if isinstance(value, bool):
        return '1' if value else '0'
    if isinstance(value, bytes):
        return b64encode(value).decode('ascii')
    if isinstance(value, (int, Decimal)):
        return str(value)
    if isinstance(value, datetime.time):
        return value.isoformat()
    if isinstance(value, EWSTimeZone):
        return value.ms_id
    if isinstance(value, EWSDateTime):
        return value.ewsformat()
    if isinstance(value, EWSDate):
        return value.ewsformat()
    if isinstance(value, PhoneNumber):
        return value.phone_number
    if isinstance(value, EmailAddress):
        return value.email
    if isinstance(value, Mailbox):
        return value.email_address
    if isinstance(value, Attendee):
        return value.mailbox.email_address
    if isinstance(value, ConversationId):
        return value.id
    if isinstance(value, AssociatedCalendarItemId):
        return value.id
    raise TypeError('Unsupported type: %s (%s)' % (type(value), value))


def xml_text_to_value(value, value_type):
    from .ewsdatetime import EWSDate, EWSDateTime
    if value_type == str:
        return value
    if value_type == bool:
        try:
            return {
                'true': True,
                'on': True,
                'false': False,
                'off': False,
            }[value.lower()]
        except KeyError:
            return None
    return {
        bytes: safe_b64decode,
        int: int,
        Decimal: Decimal,
        datetime.timedelta: isodate.parse_duration,
        EWSDate: EWSDate.from_string,
        EWSDateTime: EWSDateTime.from_string,
    }[value_type](value)


def set_xml_value(elem, value, version):
    from .ewsdatetime import EWSDateTime, EWSDate
    from .fields import FieldPath, FieldOrder
    from .properties import EWSElement
    from .version import Version
    if isinstance(value, (str, bool, bytes, int, Decimal, datetime.time, EWSDate, EWSDateTime)):
        elem.text = value_to_xml_text(value)
    elif isinstance(value, _element_class):
        elem.append(value)
    elif is_iterable(value, generators_allowed=True):
        for v in value:
            if isinstance(v, (FieldPath, FieldOrder)):
                elem.append(v.to_xml())
            elif isinstance(v, EWSElement):
                if not isinstance(version, Version):
                    raise ValueError("'version' %r must be a Version instance" % version)
                elem.append(v.to_xml(version=version))
            elif isinstance(v, _element_class):
                elem.append(v)
            elif isinstance(v, str):
                add_xml_child(elem, 't:String', v)
            else:
                raise ValueError('Unsupported type %s for list element %s on elem %s' % (type(v), v, elem))
    elif isinstance(value, (FieldPath, FieldOrder)):
        elem.append(value.to_xml())
    elif isinstance(value, EWSElement):
        if not isinstance(version, Version):
            raise ValueError("'version' %r must be a Version instance" % version)
        elem.append(value.to_xml(version=version))
    else:
        raise ValueError('Unsupported type %s for value %s on elem %s' % (type(value), value, elem))
    return elem


def safe_xml_value(value, replacement='?'):
    return _ILLEGAL_XML_CHARS_RE.sub(replacement, value)


def create_element(name, attrs=None, nsmap=None):
    # Python versions prior to 3.6 do not preserve dict or kwarg ordering, so we cannot pull in attrs as **kwargs if we
    # also want stable XML attribute output. Instead, let callers supply us with an OrderedDict instance.
    if ':' in name:
        ns, name = name.split(':')
        name = '{%s}%s' % (ns_translation[ns], name)
    elem = _forgiving_parser.makeelement(name, nsmap=nsmap)
    if attrs:
        # Try hard to keep attribute order, to ensure deterministic output. This simplifies testing.
        for k, v in attrs.items():
            elem.set(k, v)
    return elem


def add_xml_child(tree, name, value):
    # We're calling add_xml_child many places where we don't have the version handy. Don't pass EWSElement or list of
    # EWSElement to this function!
    tree.append(set_xml_value(elem=create_element(name), value=value, version=None))


class StreamingContentHandler(xml.sax.handler.ContentHandler):
    """A SAX content handler that returns a character data for a single element back to the parser. The parser must have
    a 'buffer' attribute we can append data to.
    """

    def __init__(self, parser, ns, element_name):
        xml.sax.handler.ContentHandler.__init__(self)
        self._parser = parser
        self._ns = ns
        self._element_name = element_name
        self._parsing = False

    def startElementNS(self, name, qname, attrs):
        if name == (self._ns, self._element_name):
            # we can expect element data next
            self._parsing = True
            self._parser.element_found = True

    def endElementNS(self, name, qname):
        if name == (self._ns, self._element_name):
            # all element data received
            self._parsing = False

    def characters(self, content):
        if not self._parsing:
            return
        self._parser.buffer.append(content)


def prepare_input_source(source):
    # Extracted from xml.sax.expatreader.saxutils.prepare_input_source
    f = source
    source = _InputSource()
    source.setByteStream(f)
    return source


def safe_b64decode(data):
    # Incoming base64-encoded data is not always padded to a multiple of 4. Python's parser is more strict and requires
    # padding. Add padding if it's needed.
    overflow = len(data) % 4
    if overflow:
        if isinstance(data, str):
            padding = '=' * (4 - overflow)
        else:
            padding = b'=' * (4 - overflow)
        data += padding
    return b64decode(data)


class StreamingBase64Parser(DefusedExpatParser):
    """A SAX parser that returns a generator of base64-decoded character content."""

    def __init__(self, *args, **kwargs):
        DefusedExpatParser.__init__(self, *args, **kwargs)
        self._namespaces = True
        self.buffer = None
        self.element_found = None

    def parse(self, r):
        raw_source = r.raw
        # Like upstream but yields the return value of self.feed()
        raw_source = prepare_input_source(raw_source)
        self.prepareParser(raw_source)
        file = raw_source.getByteStream()
        self.buffer = []
        self.element_found = False
        buffer = file.read(self._bufsize)
        collected_data = []
        while buffer:
            if not self.element_found:
                collected_data += buffer
            yield from self.feed(buffer)
            buffer = file.read(self._bufsize)
        # Any remaining data in self.buffer should be padding chars now
        self.buffer = None
        self.close()
        if not self.element_found:
            data = bytes(collected_data)
            raise ElementNotFound('The element to be streamed from was not found', data=bytes(data))

    def feed(self, data, isFinal=0):
        """Yield the current content of the character buffer."""
        DefusedExpatParser.feed(self, data=data, isFinal=isFinal)
        return self._decode_buffer()

    def _decode_buffer(self):
        remainder = ''
        for data in self.buffer:
            available = len(remainder) + len(data)
            overflow = available % 4  # Make sure we always decode a multiple of 4
            if remainder:
                data = (remainder + data)
                remainder = ''
            if overflow:
                remainder, data = data[-overflow:], data[:-overflow]
            if data:
                yield b64decode(data)
        self.buffer = [remainder] if remainder else []


_forgiving_parser = lxml.etree.XMLParser(
    resolve_entities=False,  # This setting is recommended by lxml for safety
    recover=True,  # This setting is non-default
    huge_tree=True,  # This setting enables parsing huge attachments, mime_content and other large data
)
_element_class = _forgiving_parser.makeelement('x').__class__


class BytesGeneratorIO(io.RawIOBase):
    """A BytesIO that can produce bytes from a streaming HTTP request. Expects r.iter_content() as input
    lxml tries to be smart by calling `getvalue` when present, assuming that the entire string is in memory.
    Omitting `getvalue` forces lxml to stream the request through `read` avoiding the memory duplication.
    """

    def __init__(self, bytes_generator):
        self._bytes_generator = bytes_generator
        self._next = bytearray()
        self._tell = 0
        super().__init__()

    def readable(self):
        return not self.closed

    def tell(self):
        return self._tell

    def read(self, size=-1):
        # requests `iter_content()` auto-adjusts the number of bytes based on bandwidth
        # can't assume how many bytes next returns so stash any extra in `self._next`
        if self.closed:
            raise ValueError("read from a closed file")
        if self._next is None:
            return b''
        if size is None:
            size = -1

        res = self._next
        while size < 0 or len(res) < size:
            try:
                res.extend(next(self._bytes_generator))
            except StopIteration:
                self._next = None
                break

        if size > 0 and self._next is not None:
            self._next = res[size:]
            res = res[:size]

        self._tell += len(res)
        return bytes(res)

    def close(self):
        if not self.closed:
            self._bytes_generator.close()
        super().close()


class DocumentYielder:
    """Look for XML documents in a streaming HTTP response and yield them as they become available from the stream."""

    def __init__(self, content_iterator, document_tag='Envelope'):
        self._iterator = content_iterator
        self._start_token = b'<%s' % document_tag.encode('utf-8')
        self._end_token = b'/%s>' % document_tag.encode('utf-8')

    def get_tag(self, stop_byte):
        tag_buffer = [b'<']
        while True:
            try:
                c = next(self._iterator)
            except StopIteration:
                break
            tag_buffer.append(c)
            if c == stop_byte:
                break
        return b''.join(tag_buffer)

    def __iter__(self):
        """Consumes the content iterator, looking for start and end tags. Returns each document when we have fully
        collected it.
        """
        doc_started = False
        buffer = []
        try:
            while True:
                c = next(self._iterator)
                if not doc_started and c == b'<':
                    tag = self.get_tag(stop_byte=b' ')
                    if tag.startswith(self._start_token):
                        # Start of document. Collect bytes from this point
                        buffer.append(tag)
                        doc_started = True
                elif doc_started and c == b'<':
                    tag = self.get_tag(stop_byte=b'>')
                    buffer.append(tag)
                    if tag.endswith(self._end_token):
                        # End of document. Yield a valid document and reset the buffer
                        yield b"<?xml version='1.0' encoding='utf-8'?>\n%s" % b''.join(buffer)
                        doc_started = False
                        buffer = []
                elif doc_started:
                    buffer.append(c)
        except StopIteration:
            return


def to_xml(bytes_content):
    """Convert bytes or a generator of bytes to an XML tree."""
    # Exchange servers may spit out the weirdest XML. lxml is pretty good at recovering from errors
    if isinstance(bytes_content, bytes):
        stream = io.BytesIO(bytes_content)
    else:
        stream = BytesGeneratorIO(bytes_content)
    try:
        res = lxml.etree.parse(stream, parser=_forgiving_parser)  # nosec
    except AssertionError as e:
        raise ParseError(e.args[0], '<not from file>', -1, 0)
    except lxml.etree.ParseError as e:
        if hasattr(e, 'position'):
            e.lineno, e.offset = e.position
        if not e.lineno:
            raise ParseError(str(e), '<not from file>', e.lineno, e.offset)
        try:
            stream.seek(0)
            offending_line = stream.read().splitlines()[e.lineno - 1]
        except (IndexError, io.UnsupportedOperation):
            raise ParseError(str(e), '<not from file>', e.lineno, e.offset)
        else:
            offending_excerpt = offending_line[max(0, e.offset - 20):e.offset + 20]
            msg = '%s\nOffending text: [...]%s[...]' % (str(e), offending_excerpt)
            raise ParseError(msg, '<not from file>', e.lineno, e.offset)
    except TypeError:
        try:
            stream.seek(0)
        except (IndexError, io.UnsupportedOperation):
            pass
        raise ParseError('This is not XML: %r' % stream.read(), '<not from file>', -1, 0)

    if res.getroot() is None:
        try:
            stream.seek(0)
            msg = 'No root element found: %r' % stream.read()
        except (IndexError, io.UnsupportedOperation):
            msg = 'No root element found'
        raise ParseError(msg, '<not from file>', -1, 0)
    return res


def is_xml(text, expected_prefix=b'<?xml'):
    """Lightweight test if response is an XML doc. It's better to be fast than correct here.

    :param text: The string to check
    :param expected_prefix: What to search for in the start if the string
    :return:
    """
    # BOM_UTF8 is an UTF-8 byte order mark which may precede the XML from an Exchange server
    bom_len = len(BOM_UTF8)
    prefix_len = len(expected_prefix)
    if text[:bom_len] == BOM_UTF8:
        prefix = text[bom_len:bom_len + prefix_len]
    else:
        prefix = text[:prefix_len]
    return prefix == expected_prefix


class PrettyXmlHandler(logging.StreamHandler):
    """A steaming log handler that prettifies log statements containing XML when output is a terminal."""

    @staticmethod
    def parse_bytes(xml_bytes):
        return lxml.etree.parse(io.BytesIO(xml_bytes), parser=_forgiving_parser)  # nosec

    @classmethod
    def prettify_xml(cls, xml_bytes):
        """Re-format an XML document to a consistent style."""
        return lxml.etree.tostring(
            cls.parse_bytes(xml_bytes),
            xml_declaration=True,
            encoding='utf-8',
            pretty_print=True
        ).replace(b'\t', b'    ').replace(b' xmlns:', b'\n    xmlns:')

    @staticmethod
    def highlight_xml(xml_str):
        """Highlight a string containing XML, using terminal color codes."""
        return highlight(xml_str, XmlLexer(), TerminalFormatter()).rstrip()

    def emit(self, record):
        """Pretty-print and syntax highlight a log statement if all these conditions are met:
           * This is a DEBUG message
           * We're outputting to a terminal
           * The log message args is a dict containing keys starting with 'xml_' and values as bytes

        :param record:
        """
        if record.levelno == logging.DEBUG and self.is_tty() and isinstance(record.args, dict):
            for key, value in record.args.items():
                if not key.startswith('xml_'):
                    continue
                if not isinstance(value, bytes):
                    continue
                if not is_xml(value):
                    continue
                try:
                    record.args[key] = self.highlight_xml(self.prettify_xml(value))
                except Exception as e:
                    # Something bad happened, but we don't want to crash the program just because logging failed
                    print('XML highlighting failed: %s' % e)
        return super().emit(record)

    def is_tty(self):
        """Check if we're outputting to a terminal."""
        try:
            return self.stream.isatty()
        except AttributeError:
            return False


class AnonymizingXmlHandler(PrettyXmlHandler):
    """A steaming log handler that prettifies and anonymizes log statements containing XML when output is a terminal."""
    PRIVATE_TAGS = {'RootItemId', 'ItemId', 'Id', 'RootItemChangeKey', 'ChangeKey'}

    def __init__(self, forbidden_strings, *args, **kwargs):
        self.forbidden_strings = forbidden_strings
        super().__init__(*args, **kwargs)

    def parse_bytes(self, xml_bytes):
        root = lxml.etree.parse(io.BytesIO(xml_bytes), parser=_forgiving_parser)  # nosec
        for elem in root.iter():
            # Anonymize element attribute values known to contain private data
            for attr in set(elem.keys()) & self.PRIVATE_TAGS:
                elem.set(attr, 'DEADBEEF=')
            # Anonymize anything requested by the caller
            for s in self.forbidden_strings:
                elem.text.replace(s, '[REMOVED]')
        return root


class DummyRequest:
    """A class to fake a requests Request object for functions that expect this."""

    def __init__(self, headers):
        self.headers = headers


class DummyResponse:
    """A class to fake a requests Response object for functions that expect this."""

    def __init__(self, url, headers, request_headers, content=b'', status_code=503, history=None):
        self.status_code = status_code
        self.url = url
        self.headers = headers
        self.content = content
        self.text = content.decode('utf-8', errors='ignore')
        self.request = DummyRequest(headers=request_headers)
        self.history = history

    def iter_content(self):
        return self.content

    def close(self):
        pass


def get_domain(email):
    try:
        return email.split('@')[1].lower()
    except (IndexError, AttributeError):
        raise ValueError("'%s' is not a valid email" % email)


def split_url(url):
    parsed_url = urlparse(url)
    # Use netloc instead of hostname since hostname is None if URL is relative
    return parsed_url.scheme == 'https', parsed_url.netloc.lower(), parsed_url.path


def get_redirect_url(response, allow_relative=True, require_relative=False):
    # allow_relative=False throws RelativeRedirect error if scheme and hostname are equal to the request
    # require_relative=True throws RelativeRedirect error if scheme and hostname are not equal to the request
    redirect_url = response.headers.get('location', None)
    if not redirect_url:
        raise TransportError('HTTP redirect but no location header')
    # At least some servers are kind enough to supply a new location. It may be relative
    redirect_has_ssl, redirect_server, redirect_path = split_url(redirect_url)
    # The response may have been redirected already. Get the original URL
    request_url = response.history[0] if response.history else response.url
    request_has_ssl, request_server, _ = split_url(request_url)
    response_has_ssl, response_server, response_path = split_url(response.url)

    if not redirect_server:
        # Redirect URL is relative. Inherit server and scheme from response URL
        redirect_server = response_server
        redirect_has_ssl = response_has_ssl
    if not redirect_path.startswith('/'):
        # The path is not top-level. Add response path
        redirect_path = (response_path or '/') + redirect_path
    redirect_url = '%s://%s%s' % ('https' if redirect_has_ssl else 'http', redirect_server, redirect_path)
    if redirect_url == request_url:
        # And some are mean enough to redirect to the same location
        raise TransportError('Redirect to same location: %s' % redirect_url)
    if not allow_relative and (request_has_ssl == response_has_ssl and request_server == redirect_server):
        raise RelativeRedirect(redirect_url)
    if require_relative and (request_has_ssl != response_has_ssl or request_server != redirect_server):
        raise RelativeRedirect(redirect_url)
    return redirect_url


RETRY_WAIT = 10  # Seconds to wait before retry on connection errors
MAX_REDIRECTS = 10  # Maximum number of URL redirects before we give up

# A collection of error classes we want to handle as general connection errors
CONNECTION_ERRORS = (requests.exceptions.ChunkedEncodingError, requests.exceptions.ConnectionError,
                     requests.exceptions.Timeout, socket.timeout, ConnectionResetError)

# A collection of error classes we want to handle as TLS verification errors
TLS_ERRORS = (requests.exceptions.SSLError,)
try:
    # If pyOpenSSL is installed, requests will use it and throw this class on TLS errors
    import OpenSSL.SSL
    TLS_ERRORS += (OpenSSL.SSL.Error,)
except ImportError:
    pass


def post_ratelimited(protocol, session, url, headers, data, allow_redirects=False, stream=False, timeout=None):
    """There are two error-handling policies implemented here: a fail-fast policy intended for stand-alone scripts which
    fails on all responses except HTTP 200. The other policy is intended for long-running tasks that need to respect
    rate-limiting errors from the server and paper over outages of up to 1 hour.

    Wrap POST requests in a try-catch loop with a lot of error handling logic and some basic rate-limiting. If a request
    fails, and some conditions are met, the loop waits in increasing intervals, up to 1 hour, before trying again. The
    reason for this is that servers often malfunction for short periods of time, either because of ongoing data
    migrations or other maintenance tasks, misconfigurations or heavy load, or because the connecting user has hit a
    throttling policy limit.

    If the loop exited early, consumers of this package that don't implement their own rate-limiting code could quickly
    swamp such a server with new requests. That would only make things worse. Instead, it's better if the request loop
    waits patiently until the server is functioning again.

    If the connecting user has hit a throttling policy, then the server will start to malfunction in many interesting
    ways, but never actually tell the user what is happening. There is no way to distinguish this situation from other
    malfunctions. The only cure is to stop making requests.

    The contract on sessions here is to return the session that ends up being used, or retiring the session if we
    intend to raise an exception. We give up on max_wait timeout, not number of retries.

    An additional resource on handling throttling policies and client back off strategies:
        https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/ews-throttling-in-exchange

    :param protocol:
    :param session:
    :param url:
    :param headers:
    :param data:
    :param allow_redirects:  (Default value = False)
    :param stream:  (Default value = False)
    :param timeout:

    :return:
    """
    if not timeout:
        timeout = protocol.TIMEOUT
    thread_id = get_ident()
    wait = RETRY_WAIT  # Initial retry wait. We double the value on each retry
    retry = 0
    redirects = 0
    log_msg = '''\
Retry: %(retry)s
Waited: %(wait)s
Timeout: %(timeout)s
Session: %(session_id)s
Thread: %(thread_id)s
Auth type: %(auth)s
URL: %(url)s
HTTP adapter: %(adapter)s
Allow redirects: %(allow_redirects)s
Streaming: %(stream)s
Response time: %(response_time)s
Status code: %(status_code)s
Request headers: %(request_headers)s
Response headers: %(response_headers)s'''
    xml_log_msg = '''\
Request XML: %(xml_request)s
Response XML: %(xml_response)s'''
    log_vals = dict(
        retry=retry,
        wait=wait,
        timeout=timeout,
        session_id=session.session_id,
        thread_id=thread_id,
        auth=session.auth,
        url=url,
        adapter=session.get_adapter(url),
        allow_redirects=allow_redirects,
        stream=stream,
        response_time=None,
        status_code=None,
        request_headers=headers,
        response_headers=None,
    )
    xml_log_vals = dict(
        xml_request=None,
        xml_response=None,
    )
    t_start = time.monotonic()
    try:
        while True:
            backed_off = _back_off_if_needed(protocol.retry_policy.back_off_until)
            if backed_off:
                # We may have slept for a long time. Renew the session.
                session = protocol.renew_session(session)
            log.debug('Session %s thread %s: retry %s timeout %s POST\'ing to %s after %ss wait', session.session_id,
                      thread_id, retry, timeout, url, wait)
            d_start = time.monotonic()
            # Always create a dummy response for logging purposes, in case we fail in the following
            r = DummyResponse(url=url, headers={}, request_headers=headers)
            try:
                r = session.post(url=url, headers=headers, data=data, allow_redirects=False, timeout=timeout,
                                 stream=stream)
            except TLS_ERRORS as e:
                # Don't retry on TLS errors. They will most likely be persistent.
                raise TransportError(str(e))
            except CONNECTION_ERRORS as e:
                log.debug('Session %s thread %s: connection error POST\'ing to %s', session.session_id, thread_id, url)
                r = DummyResponse(url=url, headers={'TimeoutException': e}, request_headers=headers)
            except TokenExpiredError as e:
                log.debug('Session %s thread %s: OAuth token expired; refreshing', session.session_id, thread_id)
                r = DummyResponse(url=url, headers={'TokenExpiredError': e}, request_headers=headers, status_code=401)
            except KeyError as e:
                if e.args[0] != 'www-authenticate':
                    raise
                log.debug('Session %s thread %s: auth headers missing from %s', session.session_id, thread_id, url)
                r = DummyResponse(url=url, headers={'KeyError': e}, request_headers=headers)
            finally:
                log_vals.update(
                    retry=retry,
                    wait=wait,
                    session_id=session.session_id,
                    url=str(r.url),
                    response_time=time.monotonic() - d_start,
                    status_code=r.status_code,
                    request_headers=r.request.headers,
                    response_headers=r.headers,
                )
                xml_log_vals.update(
                    xml_request=data,
                    xml_response='[STREAMING]' if stream else r.content,
                )
            log.debug(log_msg, log_vals)
            xml_log.debug(xml_log_msg, xml_log_vals)
            if _need_new_credentials(response=r):
                r.close()  # Release memory
                session = protocol.refresh_credentials(session)
                continue
            total_wait = time.monotonic() - t_start
            if protocol.retry_policy.may_retry_on_error(response=r, wait=total_wait):
                r.close()  # Release memory
                log.info("Session %s thread %s: Connection error on URL %s (code %s). Cool down %s secs",
                         session.session_id, thread_id, r.url, r.status_code, wait)
                wait = _retry_after(r, wait)
                protocol.retry_policy.back_off(wait)
                retry += 1
                wait *= 2  # Increase delay for every retry
                continue
            if r.status_code in (301, 302):
                r.close()  # Release memory
                url, redirects = _redirect_or_fail(r, redirects, allow_redirects)
                continue
            break
    except (RateLimitError, RedirectError) as e:
        log.warning(e.value)
        protocol.retire_session(session)
        raise
    except Exception as e:
        # Let higher layers handle this. Add full context for better debugging.
        log.error('%s: %s\n%s\n%s', e.__class__.__name__, str(e), log_msg % log_vals, xml_log_msg % xml_log_vals)
        protocol.retire_session(session)
        raise
    if r.status_code == 500 and r.content and is_xml(r.content):
        # Some genius at Microsoft thinks it's OK to send a valid SOAP response as an HTTP 500
        log.debug('Got status code %s but trying to parse content anyway', r.status_code)
    elif r.status_code != 200:
        protocol.retire_session(session)
        try:
            protocol.retry_policy.raise_response_errors(r)  # Always raises an exception
        except MalformedResponseError as e:
            log.error('%s: %s\n%s\n%s', e.__class__.__name__, str(e), log_msg % log_vals, xml_log_msg % xml_log_vals)
            raise
        except Exception:
            raise
    log.debug('Session %s thread %s: Useful response from %s', session.session_id, thread_id, url)
    return r, session


def _back_off_if_needed(back_off_until):
    if back_off_until:
        sleep_secs = (back_off_until - datetime.datetime.now()).total_seconds()
        # The back off value may have expired within the last few milliseconds
        if sleep_secs > 0:
            log.warning('Server requested back off until %s. Sleeping %s seconds', back_off_until, sleep_secs)
            time.sleep(sleep_secs)
            return True
    return False


def _need_new_credentials(response):
    return response.status_code == 401 \
        and response.headers.get('TokenExpiredError')


def _redirect_or_fail(response, redirects, allow_redirects):
    # Retry with no delay. If we let requests handle redirects automatically, it would issue a GET to that
    # URL. We still want to POST.
    try:
        redirect_url = get_redirect_url(response=response, allow_relative=False)
    except RelativeRedirect as e:
        log.debug("'allow_redirects' only supports relative redirects (%s -> %s)", response.url, e.value)
        raise RedirectError(url=e.value)
    if not allow_redirects:
        raise TransportError('Redirect not allowed but we were redirected (%s -> %s)' % (response.url, redirect_url))
    log.debug('HTTP redirected to %s', redirect_url)
    redirects += 1
    if redirects > MAX_REDIRECTS:
        raise TransportError('Max redirect count exceeded')
    return redirect_url, redirects


def _retry_after(r, wait):
    """Either return the Retry-After header value or the default wait, whichever is larger."""
    try:
        retry_after = int(r.headers.get('Retry-After', '0'))
    except ValueError:
        pass
    else:
        if retry_after > wait:
            return retry_after
    return wait

Functions

def add_xml_child(tree, name, value)
Expand source code
def add_xml_child(tree, name, value):
    # We're calling add_xml_child many places where we don't have the version handy. Don't pass EWSElement or list of
    # EWSElement to this function!
    tree.append(set_xml_value(elem=create_element(name), value=value, version=None))
def chunkify(iterable, chunksize)

Split an iterable into chunks of size chunksize. The last chunk may be smaller than chunksize.

:param iterable: :param chunksize: :return:

Expand source code
def chunkify(iterable, chunksize):
    """Split an iterable into chunks of size ``chunksize``. The last chunk may be smaller than ``chunksize``.

    :param iterable:
    :param chunksize:
    :return:
    """
    from .queryset import QuerySet
    if hasattr(iterable, '__getitem__') and not isinstance(iterable, QuerySet):
        # tuple, list. QuerySet has __getitem__ but that evaluates the entire query greedily. We don't want that here.
        for i in range(0, len(iterable), chunksize):
            yield iterable[i:i + chunksize]
    else:
        # generator, set, map, QuerySet
        chunk = []
        for i in iterable:
            chunk.append(i)
            if len(chunk) == chunksize:
                yield chunk
                chunk = []
        if chunk:
            yield chunk
def create_element(name, attrs=None, nsmap=None)
Expand source code
def create_element(name, attrs=None, nsmap=None):
    # Python versions prior to 3.6 do not preserve dict or kwarg ordering, so we cannot pull in attrs as **kwargs if we
    # also want stable XML attribute output. Instead, let callers supply us with an OrderedDict instance.
    if ':' in name:
        ns, name = name.split(':')
        name = '{%s}%s' % (ns_translation[ns], name)
    elem = _forgiving_parser.makeelement(name, nsmap=nsmap)
    if attrs:
        # Try hard to keep attribute order, to ensure deterministic output. This simplifies testing.
        for k, v in attrs.items():
            elem.set(k, v)
    return elem
def get_domain(email)
Expand source code
def get_domain(email):
    try:
        return email.split('@')[1].lower()
    except (IndexError, AttributeError):
        raise ValueError("'%s' is not a valid email" % email)
def get_redirect_url(response, allow_relative=True, require_relative=False)
Expand source code
def get_redirect_url(response, allow_relative=True, require_relative=False):
    # allow_relative=False throws RelativeRedirect error if scheme and hostname are equal to the request
    # require_relative=True throws RelativeRedirect error if scheme and hostname are not equal to the request
    redirect_url = response.headers.get('location', None)
    if not redirect_url:
        raise TransportError('HTTP redirect but no location header')
    # At least some servers are kind enough to supply a new location. It may be relative
    redirect_has_ssl, redirect_server, redirect_path = split_url(redirect_url)
    # The response may have been redirected already. Get the original URL
    request_url = response.history[0] if response.history else response.url
    request_has_ssl, request_server, _ = split_url(request_url)
    response_has_ssl, response_server, response_path = split_url(response.url)

    if not redirect_server:
        # Redirect URL is relative. Inherit server and scheme from response URL
        redirect_server = response_server
        redirect_has_ssl = response_has_ssl
    if not redirect_path.startswith('/'):
        # The path is not top-level. Add response path
        redirect_path = (response_path or '/') + redirect_path
    redirect_url = '%s://%s%s' % ('https' if redirect_has_ssl else 'http', redirect_server, redirect_path)
    if redirect_url == request_url:
        # And some are mean enough to redirect to the same location
        raise TransportError('Redirect to same location: %s' % redirect_url)
    if not allow_relative and (request_has_ssl == response_has_ssl and request_server == redirect_server):
        raise RelativeRedirect(redirect_url)
    if require_relative and (request_has_ssl != response_has_ssl or request_server != redirect_server):
        raise RelativeRedirect(redirect_url)
    return redirect_url
def get_xml_attr(tree, name)
Expand source code
def get_xml_attr(tree, name):
    elem = tree.find(name)
    if elem is None:  # Must compare with None, see XML docs
        return None
    return elem.text or None
def get_xml_attrs(tree, name)
Expand source code
def get_xml_attrs(tree, name):
    return [elem.text for elem in tree.findall(name) if elem.text is not None]
def is_iterable(value, generators_allowed=False)

Check if value is a list-like object. Don't match generators and generator-like objects here by default, because callers don't necessarily guarantee that they only iterate the value once. Take care to not match string types and bytes.

:param value: any type of object :param generators_allowed: if True, generators will be treated as iterable (Default value = False)

:return: True or False

Expand source code
def is_iterable(value, generators_allowed=False):
    """Check if value is a list-like object. Don't match generators and generator-like objects here by default, because
    callers don't necessarily guarantee that they only iterate the value once. Take care to not match string types and
    bytes.

    :param value: any type of object
    :param generators_allowed: if True, generators will be treated as iterable (Default value = False)

    :return: True or False
    """
    if generators_allowed:
        if not isinstance(value, (bytes, str)) and hasattr(value, '__iter__'):
            return True
    else:
        if isinstance(value, (tuple, list, set)):
            return True
    return False
def is_xml(text, expected_prefix=b'<?xml')

Lightweight test if response is an XML doc. It's better to be fast than correct here.

:param text: The string to check :param expected_prefix: What to search for in the start if the string :return:

Expand source code
def is_xml(text, expected_prefix=b'<?xml'):
    """Lightweight test if response is an XML doc. It's better to be fast than correct here.

    :param text: The string to check
    :param expected_prefix: What to search for in the start if the string
    :return:
    """
    # BOM_UTF8 is an UTF-8 byte order mark which may precede the XML from an Exchange server
    bom_len = len(BOM_UTF8)
    prefix_len = len(expected_prefix)
    if text[:bom_len] == BOM_UTF8:
        prefix = text[bom_len:bom_len + prefix_len]
    else:
        prefix = text[:prefix_len]
    return prefix == expected_prefix
def peek(iterable)

Check if an iterable is empty and return status and the rewinded iterable.

:param iterable: :return:

Expand source code
def peek(iterable):
    """Check if an iterable is empty and return status and the rewinded iterable.

    :param iterable:
    :return:
    """
    if hasattr(iterable, '__len__'):
        # tuple, list, set
        return not iterable, iterable
    # generator
    try:
        first = next(iterable)
    except StopIteration:
        return True, iterable
    # We can't rewind a generator. Instead, chain the first element and the rest of the generator
    return False, itertools.chain([first], iterable)
def post_ratelimited(protocol, session, url, headers, data, allow_redirects=False, stream=False, timeout=None)

There are two error-handling policies implemented here: a fail-fast policy intended for stand-alone scripts which fails on all responses except HTTP 200. The other policy is intended for long-running tasks that need to respect rate-limiting errors from the server and paper over outages of up to 1 hour.

Wrap POST requests in a try-catch loop with a lot of error handling logic and some basic rate-limiting. If a request fails, and some conditions are met, the loop waits in increasing intervals, up to 1 hour, before trying again. The reason for this is that servers often malfunction for short periods of time, either because of ongoing data migrations or other maintenance tasks, misconfigurations or heavy load, or because the connecting user has hit a throttling policy limit.

If the loop exited early, consumers of this package that don't implement their own rate-limiting code could quickly swamp such a server with new requests. That would only make things worse. Instead, it's better if the request loop waits patiently until the server is functioning again.

If the connecting user has hit a throttling policy, then the server will start to malfunction in many interesting ways, but never actually tell the user what is happening. There is no way to distinguish this situation from other malfunctions. The only cure is to stop making requests.

The contract on sessions here is to return the session that ends up being used, or retiring the session if we intend to raise an exception. We give up on max_wait timeout, not number of retries.

An additional resource on handling throttling policies and client back off strategies: https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/ews-throttling-in-exchange

:param protocol: :param session: :param url: :param headers: :param data: :param allow_redirects: (Default value = False) :param stream: (Default value = False) :param timeout:

:return:

Expand source code
def post_ratelimited(protocol, session, url, headers, data, allow_redirects=False, stream=False, timeout=None):
    """There are two error-handling policies implemented here: a fail-fast policy intended for stand-alone scripts which
    fails on all responses except HTTP 200. The other policy is intended for long-running tasks that need to respect
    rate-limiting errors from the server and paper over outages of up to 1 hour.

    Wrap POST requests in a try-catch loop with a lot of error handling logic and some basic rate-limiting. If a request
    fails, and some conditions are met, the loop waits in increasing intervals, up to 1 hour, before trying again. The
    reason for this is that servers often malfunction for short periods of time, either because of ongoing data
    migrations or other maintenance tasks, misconfigurations or heavy load, or because the connecting user has hit a
    throttling policy limit.

    If the loop exited early, consumers of this package that don't implement their own rate-limiting code could quickly
    swamp such a server with new requests. That would only make things worse. Instead, it's better if the request loop
    waits patiently until the server is functioning again.

    If the connecting user has hit a throttling policy, then the server will start to malfunction in many interesting
    ways, but never actually tell the user what is happening. There is no way to distinguish this situation from other
    malfunctions. The only cure is to stop making requests.

    The contract on sessions here is to return the session that ends up being used, or retiring the session if we
    intend to raise an exception. We give up on max_wait timeout, not number of retries.

    An additional resource on handling throttling policies and client back off strategies:
        https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/ews-throttling-in-exchange

    :param protocol:
    :param session:
    :param url:
    :param headers:
    :param data:
    :param allow_redirects:  (Default value = False)
    :param stream:  (Default value = False)
    :param timeout:

    :return:
    """
    if not timeout:
        timeout = protocol.TIMEOUT
    thread_id = get_ident()
    wait = RETRY_WAIT  # Initial retry wait. We double the value on each retry
    retry = 0
    redirects = 0
    log_msg = '''\
Retry: %(retry)s
Waited: %(wait)s
Timeout: %(timeout)s
Session: %(session_id)s
Thread: %(thread_id)s
Auth type: %(auth)s
URL: %(url)s
HTTP adapter: %(adapter)s
Allow redirects: %(allow_redirects)s
Streaming: %(stream)s
Response time: %(response_time)s
Status code: %(status_code)s
Request headers: %(request_headers)s
Response headers: %(response_headers)s'''
    xml_log_msg = '''\
Request XML: %(xml_request)s
Response XML: %(xml_response)s'''
    log_vals = dict(
        retry=retry,
        wait=wait,
        timeout=timeout,
        session_id=session.session_id,
        thread_id=thread_id,
        auth=session.auth,
        url=url,
        adapter=session.get_adapter(url),
        allow_redirects=allow_redirects,
        stream=stream,
        response_time=None,
        status_code=None,
        request_headers=headers,
        response_headers=None,
    )
    xml_log_vals = dict(
        xml_request=None,
        xml_response=None,
    )
    t_start = time.monotonic()
    try:
        while True:
            backed_off = _back_off_if_needed(protocol.retry_policy.back_off_until)
            if backed_off:
                # We may have slept for a long time. Renew the session.
                session = protocol.renew_session(session)
            log.debug('Session %s thread %s: retry %s timeout %s POST\'ing to %s after %ss wait', session.session_id,
                      thread_id, retry, timeout, url, wait)
            d_start = time.monotonic()
            # Always create a dummy response for logging purposes, in case we fail in the following
            r = DummyResponse(url=url, headers={}, request_headers=headers)
            try:
                r = session.post(url=url, headers=headers, data=data, allow_redirects=False, timeout=timeout,
                                 stream=stream)
            except TLS_ERRORS as e:
                # Don't retry on TLS errors. They will most likely be persistent.
                raise TransportError(str(e))
            except CONNECTION_ERRORS as e:
                log.debug('Session %s thread %s: connection error POST\'ing to %s', session.session_id, thread_id, url)
                r = DummyResponse(url=url, headers={'TimeoutException': e}, request_headers=headers)
            except TokenExpiredError as e:
                log.debug('Session %s thread %s: OAuth token expired; refreshing', session.session_id, thread_id)
                r = DummyResponse(url=url, headers={'TokenExpiredError': e}, request_headers=headers, status_code=401)
            except KeyError as e:
                if e.args[0] != 'www-authenticate':
                    raise
                log.debug('Session %s thread %s: auth headers missing from %s', session.session_id, thread_id, url)
                r = DummyResponse(url=url, headers={'KeyError': e}, request_headers=headers)
            finally:
                log_vals.update(
                    retry=retry,
                    wait=wait,
                    session_id=session.session_id,
                    url=str(r.url),
                    response_time=time.monotonic() - d_start,
                    status_code=r.status_code,
                    request_headers=r.request.headers,
                    response_headers=r.headers,
                )
                xml_log_vals.update(
                    xml_request=data,
                    xml_response='[STREAMING]' if stream else r.content,
                )
            log.debug(log_msg, log_vals)
            xml_log.debug(xml_log_msg, xml_log_vals)
            if _need_new_credentials(response=r):
                r.close()  # Release memory
                session = protocol.refresh_credentials(session)
                continue
            total_wait = time.monotonic() - t_start
            if protocol.retry_policy.may_retry_on_error(response=r, wait=total_wait):
                r.close()  # Release memory
                log.info("Session %s thread %s: Connection error on URL %s (code %s). Cool down %s secs",
                         session.session_id, thread_id, r.url, r.status_code, wait)
                wait = _retry_after(r, wait)
                protocol.retry_policy.back_off(wait)
                retry += 1
                wait *= 2  # Increase delay for every retry
                continue
            if r.status_code in (301, 302):
                r.close()  # Release memory
                url, redirects = _redirect_or_fail(r, redirects, allow_redirects)
                continue
            break
    except (RateLimitError, RedirectError) as e:
        log.warning(e.value)
        protocol.retire_session(session)
        raise
    except Exception as e:
        # Let higher layers handle this. Add full context for better debugging.
        log.error('%s: %s\n%s\n%s', e.__class__.__name__, str(e), log_msg % log_vals, xml_log_msg % xml_log_vals)
        protocol.retire_session(session)
        raise
    if r.status_code == 500 and r.content and is_xml(r.content):
        # Some genius at Microsoft thinks it's OK to send a valid SOAP response as an HTTP 500
        log.debug('Got status code %s but trying to parse content anyway', r.status_code)
    elif r.status_code != 200:
        protocol.retire_session(session)
        try:
            protocol.retry_policy.raise_response_errors(r)  # Always raises an exception
        except MalformedResponseError as e:
            log.error('%s: %s\n%s\n%s', e.__class__.__name__, str(e), log_msg % log_vals, xml_log_msg % xml_log_vals)
            raise
        except Exception:
            raise
    log.debug('Session %s thread %s: Useful response from %s', session.session_id, thread_id, url)
    return r, session
def prepare_input_source(source)
Expand source code
def prepare_input_source(source):
    # Extracted from xml.sax.expatreader.saxutils.prepare_input_source
    f = source
    source = _InputSource()
    source.setByteStream(f)
    return source
def require_account(f)
Expand source code
def require_account(f):
    @wraps(f)
    def wrapper(self, *args, **kwargs):
        if not self.account:
            raise ValueError('%s must have an account' % self.__class__.__name__)
        return f(self, *args, **kwargs)
    return wrapper
def require_id(f)
Expand source code
def require_id(f):
    @wraps(f)
    def wrapper(self, *args, **kwargs):
        if not self.account:
            raise ValueError('%s must have an account' % self.__class__.__name__)
        if not self.id:
            raise ValueError('%s must have an ID' % self.__class__.__name__)
        return f(self, *args, **kwargs)
    return wrapper
def safe_b64decode(data)
Expand source code
def safe_b64decode(data):
    # Incoming base64-encoded data is not always padded to a multiple of 4. Python's parser is more strict and requires
    # padding. Add padding if it's needed.
    overflow = len(data) % 4
    if overflow:
        if isinstance(data, str):
            padding = '=' * (4 - overflow)
        else:
            padding = b'=' * (4 - overflow)
        data += padding
    return b64decode(data)
def safe_xml_value(value, replacement='?')
Expand source code
def safe_xml_value(value, replacement='?'):
    return _ILLEGAL_XML_CHARS_RE.sub(replacement, value)
def set_xml_value(elem, value, version)
Expand source code
def set_xml_value(elem, value, version):
    from .ewsdatetime import EWSDateTime, EWSDate
    from .fields import FieldPath, FieldOrder
    from .properties import EWSElement
    from .version import Version
    if isinstance(value, (str, bool, bytes, int, Decimal, datetime.time, EWSDate, EWSDateTime)):
        elem.text = value_to_xml_text(value)
    elif isinstance(value, _element_class):
        elem.append(value)
    elif is_iterable(value, generators_allowed=True):
        for v in value:
            if isinstance(v, (FieldPath, FieldOrder)):
                elem.append(v.to_xml())
            elif isinstance(v, EWSElement):
                if not isinstance(version, Version):
                    raise ValueError("'version' %r must be a Version instance" % version)
                elem.append(v.to_xml(version=version))
            elif isinstance(v, _element_class):
                elem.append(v)
            elif isinstance(v, str):
                add_xml_child(elem, 't:String', v)
            else:
                raise ValueError('Unsupported type %s for list element %s on elem %s' % (type(v), v, elem))
    elif isinstance(value, (FieldPath, FieldOrder)):
        elem.append(value.to_xml())
    elif isinstance(value, EWSElement):
        if not isinstance(version, Version):
            raise ValueError("'version' %r must be a Version instance" % version)
        elem.append(value.to_xml(version=version))
    else:
        raise ValueError('Unsupported type %s for value %s on elem %s' % (type(value), value, elem))
    return elem
def split_url(url)
Expand source code
def split_url(url):
    parsed_url = urlparse(url)
    # Use netloc instead of hostname since hostname is None if URL is relative
    return parsed_url.scheme == 'https', parsed_url.netloc.lower(), parsed_url.path
def to_xml(bytes_content)

Convert bytes or a generator of bytes to an XML tree.

Expand source code
def to_xml(bytes_content):
    """Convert bytes or a generator of bytes to an XML tree."""
    # Exchange servers may spit out the weirdest XML. lxml is pretty good at recovering from errors
    if isinstance(bytes_content, bytes):
        stream = io.BytesIO(bytes_content)
    else:
        stream = BytesGeneratorIO(bytes_content)
    try:
        res = lxml.etree.parse(stream, parser=_forgiving_parser)  # nosec
    except AssertionError as e:
        raise ParseError(e.args[0], '<not from file>', -1, 0)
    except lxml.etree.ParseError as e:
        if hasattr(e, 'position'):
            e.lineno, e.offset = e.position
        if not e.lineno:
            raise ParseError(str(e), '<not from file>', e.lineno, e.offset)
        try:
            stream.seek(0)
            offending_line = stream.read().splitlines()[e.lineno - 1]
        except (IndexError, io.UnsupportedOperation):
            raise ParseError(str(e), '<not from file>', e.lineno, e.offset)
        else:
            offending_excerpt = offending_line[max(0, e.offset - 20):e.offset + 20]
            msg = '%s\nOffending text: [...]%s[...]' % (str(e), offending_excerpt)
            raise ParseError(msg, '<not from file>', e.lineno, e.offset)
    except TypeError:
        try:
            stream.seek(0)
        except (IndexError, io.UnsupportedOperation):
            pass
        raise ParseError('This is not XML: %r' % stream.read(), '<not from file>', -1, 0)

    if res.getroot() is None:
        try:
            stream.seek(0)
            msg = 'No root element found: %r' % stream.read()
        except (IndexError, io.UnsupportedOperation):
            msg = 'No root element found'
        raise ParseError(msg, '<not from file>', -1, 0)
    return res
def value_to_xml_text(value)
Expand source code
def value_to_xml_text(value):
    from .ewsdatetime import EWSTimeZone, EWSDateTime, EWSDate
    from .indexed_properties import PhoneNumber, EmailAddress
    from .properties import Mailbox, AssociatedCalendarItemId, Attendee, ConversationId
    # We can't just create a map and look up with type(value) because we want to support subtypes
    if isinstance(value, str):
        return safe_xml_value(value)
    if isinstance(value, bool):
        return '1' if value else '0'
    if isinstance(value, bytes):
        return b64encode(value).decode('ascii')
    if isinstance(value, (int, Decimal)):
        return str(value)
    if isinstance(value, datetime.time):
        return value.isoformat()
    if isinstance(value, EWSTimeZone):
        return value.ms_id
    if isinstance(value, EWSDateTime):
        return value.ewsformat()
    if isinstance(value, EWSDate):
        return value.ewsformat()
    if isinstance(value, PhoneNumber):
        return value.phone_number
    if isinstance(value, EmailAddress):
        return value.email
    if isinstance(value, Mailbox):
        return value.email_address
    if isinstance(value, Attendee):
        return value.mailbox.email_address
    if isinstance(value, ConversationId):
        return value.id
    if isinstance(value, AssociatedCalendarItemId):
        return value.id
    raise TypeError('Unsupported type: %s (%s)' % (type(value), value))
def xml_text_to_value(value, value_type)
Expand source code
def xml_text_to_value(value, value_type):
    from .ewsdatetime import EWSDate, EWSDateTime
    if value_type == str:
        return value
    if value_type == bool:
        try:
            return {
                'true': True,
                'on': True,
                'false': False,
                'off': False,
            }[value.lower()]
        except KeyError:
            return None
    return {
        bytes: safe_b64decode,
        int: int,
        Decimal: Decimal,
        datetime.timedelta: isodate.parse_duration,
        EWSDate: EWSDate.from_string,
        EWSDateTime: EWSDateTime.from_string,
    }[value_type](value)
def xml_to_str(tree, encoding=None, xml_declaration=False)

Serialize an XML tree. Returns unicode if 'encoding' is None. Otherwise, we return encoded 'bytes'.

:param tree: :param encoding: (Default value = None) :param xml_declaration: (Default value = False) :return:

Expand source code
def xml_to_str(tree, encoding=None, xml_declaration=False):
    """Serialize an XML tree. Returns unicode if 'encoding' is None. Otherwise, we return encoded 'bytes'.

    :param tree:
    :param encoding:  (Default value = None)
    :param xml_declaration:  (Default value = False)
    :return:
    """
    if xml_declaration and not encoding:
        raise ValueError("'xml_declaration' is not supported when 'encoding' is None")
    if encoding:
        return lxml.etree.tostring(tree, encoding=encoding, xml_declaration=True)
    return lxml.etree.tostring(tree, encoding=str, xml_declaration=False)

Classes

class AnonymizingXmlHandler (forbidden_strings, *args, **kwargs)

A steaming log handler that prettifies and anonymizes log statements containing XML when output is a terminal.

Initialize the handler.

If stream is not specified, sys.stderr is used.

Expand source code
class AnonymizingXmlHandler(PrettyXmlHandler):
    """A steaming log handler that prettifies and anonymizes log statements containing XML when output is a terminal."""
    PRIVATE_TAGS = {'RootItemId', 'ItemId', 'Id', 'RootItemChangeKey', 'ChangeKey'}

    def __init__(self, forbidden_strings, *args, **kwargs):
        self.forbidden_strings = forbidden_strings
        super().__init__(*args, **kwargs)

    def parse_bytes(self, xml_bytes):
        root = lxml.etree.parse(io.BytesIO(xml_bytes), parser=_forgiving_parser)  # nosec
        for elem in root.iter():
            # Anonymize element attribute values known to contain private data
            for attr in set(elem.keys()) & self.PRIVATE_TAGS:
                elem.set(attr, 'DEADBEEF=')
            # Anonymize anything requested by the caller
            for s in self.forbidden_strings:
                elem.text.replace(s, '[REMOVED]')
        return root

Ancestors

Class variables

var PRIVATE_TAGS

Methods

def parse_bytes(self, xml_bytes)
Expand source code
def parse_bytes(self, xml_bytes):
    root = lxml.etree.parse(io.BytesIO(xml_bytes), parser=_forgiving_parser)  # nosec
    for elem in root.iter():
        # Anonymize element attribute values known to contain private data
        for attr in set(elem.keys()) & self.PRIVATE_TAGS:
            elem.set(attr, 'DEADBEEF=')
        # Anonymize anything requested by the caller
        for s in self.forbidden_strings:
            elem.text.replace(s, '[REMOVED]')
    return root

Inherited members

class BytesGeneratorIO (bytes_generator)

A BytesIO that can produce bytes from a streaming HTTP request. Expects r.iter_content() as input lxml tries to be smart by calling getvalue when present, assuming that the entire string is in memory. Omitting getvalue forces lxml to stream the request through read avoiding the memory duplication.

Expand source code
class BytesGeneratorIO(io.RawIOBase):
    """A BytesIO that can produce bytes from a streaming HTTP request. Expects r.iter_content() as input
    lxml tries to be smart by calling `getvalue` when present, assuming that the entire string is in memory.
    Omitting `getvalue` forces lxml to stream the request through `read` avoiding the memory duplication.
    """

    def __init__(self, bytes_generator):
        self._bytes_generator = bytes_generator
        self._next = bytearray()
        self._tell = 0
        super().__init__()

    def readable(self):
        return not self.closed

    def tell(self):
        return self._tell

    def read(self, size=-1):
        # requests `iter_content()` auto-adjusts the number of bytes based on bandwidth
        # can't assume how many bytes next returns so stash any extra in `self._next`
        if self.closed:
            raise ValueError("read from a closed file")
        if self._next is None:
            return b''
        if size is None:
            size = -1

        res = self._next
        while size < 0 or len(res) < size:
            try:
                res.extend(next(self._bytes_generator))
            except StopIteration:
                self._next = None
                break

        if size > 0 and self._next is not None:
            self._next = res[size:]
            res = res[:size]

        self._tell += len(res)
        return bytes(res)

    def close(self):
        if not self.closed:
            self._bytes_generator.close()
        super().close()

Ancestors

  • io.RawIOBase
  • _io._RawIOBase
  • io.IOBase
  • _io._IOBase

Methods

def close(self)

Flush and close the IO object.

This method has no effect if the file is already closed.

Expand source code
def close(self):
    if not self.closed:
        self._bytes_generator.close()
    super().close()
def read(self, size=-1)
Expand source code
def read(self, size=-1):
    # requests `iter_content()` auto-adjusts the number of bytes based on bandwidth
    # can't assume how many bytes next returns so stash any extra in `self._next`
    if self.closed:
        raise ValueError("read from a closed file")
    if self._next is None:
        return b''
    if size is None:
        size = -1

    res = self._next
    while size < 0 or len(res) < size:
        try:
            res.extend(next(self._bytes_generator))
        except StopIteration:
            self._next = None
            break

    if size > 0 and self._next is not None:
        self._next = res[size:]
        res = res[:size]

    self._tell += len(res)
    return bytes(res)
def readable(self)

Return whether object was opened for reading.

If False, read() will raise OSError.

Expand source code
def readable(self):
    return not self.closed
def tell(self)

Return current stream position.

Expand source code
def tell(self):
    return self._tell
class DocumentYielder (content_iterator, document_tag='Envelope')

Look for XML documents in a streaming HTTP response and yield them as they become available from the stream.

Expand source code
class DocumentYielder:
    """Look for XML documents in a streaming HTTP response and yield them as they become available from the stream."""

    def __init__(self, content_iterator, document_tag='Envelope'):
        self._iterator = content_iterator
        self._start_token = b'<%s' % document_tag.encode('utf-8')
        self._end_token = b'/%s>' % document_tag.encode('utf-8')

    def get_tag(self, stop_byte):
        tag_buffer = [b'<']
        while True:
            try:
                c = next(self._iterator)
            except StopIteration:
                break
            tag_buffer.append(c)
            if c == stop_byte:
                break
        return b''.join(tag_buffer)

    def __iter__(self):
        """Consumes the content iterator, looking for start and end tags. Returns each document when we have fully
        collected it.
        """
        doc_started = False
        buffer = []
        try:
            while True:
                c = next(self._iterator)
                if not doc_started and c == b'<':
                    tag = self.get_tag(stop_byte=b' ')
                    if tag.startswith(self._start_token):
                        # Start of document. Collect bytes from this point
                        buffer.append(tag)
                        doc_started = True
                elif doc_started and c == b'<':
                    tag = self.get_tag(stop_byte=b'>')
                    buffer.append(tag)
                    if tag.endswith(self._end_token):
                        # End of document. Yield a valid document and reset the buffer
                        yield b"<?xml version='1.0' encoding='utf-8'?>\n%s" % b''.join(buffer)
                        doc_started = False
                        buffer = []
                elif doc_started:
                    buffer.append(c)
        except StopIteration:
            return

Methods

def get_tag(self, stop_byte)
Expand source code
def get_tag(self, stop_byte):
    tag_buffer = [b'<']
    while True:
        try:
            c = next(self._iterator)
        except StopIteration:
            break
        tag_buffer.append(c)
        if c == stop_byte:
            break
    return b''.join(tag_buffer)
class DummyRequest (headers)

A class to fake a requests Request object for functions that expect this.

Expand source code
class DummyRequest:
    """A class to fake a requests Request object for functions that expect this."""

    def __init__(self, headers):
        self.headers = headers
class DummyResponse (url, headers, request_headers, content=b'', status_code=503, history=None)

A class to fake a requests Response object for functions that expect this.

Expand source code
class DummyResponse:
    """A class to fake a requests Response object for functions that expect this."""

    def __init__(self, url, headers, request_headers, content=b'', status_code=503, history=None):
        self.status_code = status_code
        self.url = url
        self.headers = headers
        self.content = content
        self.text = content.decode('utf-8', errors='ignore')
        self.request = DummyRequest(headers=request_headers)
        self.history = history

    def iter_content(self):
        return self.content

    def close(self):
        pass

Methods

def close(self)
Expand source code
def close(self):
    pass
def iter_content(self)
Expand source code
def iter_content(self):
    return self.content
class ElementNotFound (msg, data)

Raised when the expected element was not found in a response stream.

Expand source code
class ElementNotFound(Exception):
    """Raised when the expected element was not found in a response stream."""

    def __init__(self, msg, data):
        super().__init__(msg)
        self.data = data

Ancestors

  • builtins.Exception
  • builtins.BaseException
class ParseError (message, code, line, column, filename=None)

Used to wrap lxml ParseError in our own class.

Expand source code
class ParseError(lxml.etree.ParseError):
    """Used to wrap lxml ParseError in our own class."""

Ancestors

  • lxml.etree.ParseError
  • lxml.etree.LxmlSyntaxError
  • lxml.etree.LxmlError
  • lxml.etree.Error
  • builtins.SyntaxError
  • builtins.Exception
  • builtins.BaseException
class PrettyXmlHandler (stream=None)

A steaming log handler that prettifies log statements containing XML when output is a terminal.

Initialize the handler.

If stream is not specified, sys.stderr is used.

Expand source code
class PrettyXmlHandler(logging.StreamHandler):
    """A steaming log handler that prettifies log statements containing XML when output is a terminal."""

    @staticmethod
    def parse_bytes(xml_bytes):
        return lxml.etree.parse(io.BytesIO(xml_bytes), parser=_forgiving_parser)  # nosec

    @classmethod
    def prettify_xml(cls, xml_bytes):
        """Re-format an XML document to a consistent style."""
        return lxml.etree.tostring(
            cls.parse_bytes(xml_bytes),
            xml_declaration=True,
            encoding='utf-8',
            pretty_print=True
        ).replace(b'\t', b'    ').replace(b' xmlns:', b'\n    xmlns:')

    @staticmethod
    def highlight_xml(xml_str):
        """Highlight a string containing XML, using terminal color codes."""
        return highlight(xml_str, XmlLexer(), TerminalFormatter()).rstrip()

    def emit(self, record):
        """Pretty-print and syntax highlight a log statement if all these conditions are met:
           * This is a DEBUG message
           * We're outputting to a terminal
           * The log message args is a dict containing keys starting with 'xml_' and values as bytes

        :param record:
        """
        if record.levelno == logging.DEBUG and self.is_tty() and isinstance(record.args, dict):
            for key, value in record.args.items():
                if not key.startswith('xml_'):
                    continue
                if not isinstance(value, bytes):
                    continue
                if not is_xml(value):
                    continue
                try:
                    record.args[key] = self.highlight_xml(self.prettify_xml(value))
                except Exception as e:
                    # Something bad happened, but we don't want to crash the program just because logging failed
                    print('XML highlighting failed: %s' % e)
        return super().emit(record)

    def is_tty(self):
        """Check if we're outputting to a terminal."""
        try:
            return self.stream.isatty()
        except AttributeError:
            return False

Ancestors

  • logging.StreamHandler
  • logging.Handler
  • logging.Filterer

Subclasses

Static methods

def highlight_xml(xml_str)

Highlight a string containing XML, using terminal color codes.

Expand source code
@staticmethod
def highlight_xml(xml_str):
    """Highlight a string containing XML, using terminal color codes."""
    return highlight(xml_str, XmlLexer(), TerminalFormatter()).rstrip()
def parse_bytes(xml_bytes)
Expand source code
@staticmethod
def parse_bytes(xml_bytes):
    return lxml.etree.parse(io.BytesIO(xml_bytes), parser=_forgiving_parser)  # nosec
def prettify_xml(xml_bytes)

Re-format an XML document to a consistent style.

Expand source code
@classmethod
def prettify_xml(cls, xml_bytes):
    """Re-format an XML document to a consistent style."""
    return lxml.etree.tostring(
        cls.parse_bytes(xml_bytes),
        xml_declaration=True,
        encoding='utf-8',
        pretty_print=True
    ).replace(b'\t', b'    ').replace(b' xmlns:', b'\n    xmlns:')

Methods

def emit(self, record)

Pretty-print and syntax highlight a log statement if all these conditions are met: * This is a DEBUG message * We're outputting to a terminal * The log message args is a dict containing keys starting with 'xml_' and values as bytes

:param record:

Expand source code
def emit(self, record):
    """Pretty-print and syntax highlight a log statement if all these conditions are met:
       * This is a DEBUG message
       * We're outputting to a terminal
       * The log message args is a dict containing keys starting with 'xml_' and values as bytes

    :param record:
    """
    if record.levelno == logging.DEBUG and self.is_tty() and isinstance(record.args, dict):
        for key, value in record.args.items():
            if not key.startswith('xml_'):
                continue
            if not isinstance(value, bytes):
                continue
            if not is_xml(value):
                continue
            try:
                record.args[key] = self.highlight_xml(self.prettify_xml(value))
            except Exception as e:
                # Something bad happened, but we don't want to crash the program just because logging failed
                print('XML highlighting failed: %s' % e)
    return super().emit(record)
def is_tty(self)

Check if we're outputting to a terminal.

Expand source code
def is_tty(self):
    """Check if we're outputting to a terminal."""
    try:
        return self.stream.isatty()
    except AttributeError:
        return False
class StreamingBase64Parser (*args, **kwargs)

A SAX parser that returns a generator of base64-decoded character content.

Expand source code
class StreamingBase64Parser(DefusedExpatParser):
    """A SAX parser that returns a generator of base64-decoded character content."""

    def __init__(self, *args, **kwargs):
        DefusedExpatParser.__init__(self, *args, **kwargs)
        self._namespaces = True
        self.buffer = None
        self.element_found = None

    def parse(self, r):
        raw_source = r.raw
        # Like upstream but yields the return value of self.feed()
        raw_source = prepare_input_source(raw_source)
        self.prepareParser(raw_source)
        file = raw_source.getByteStream()
        self.buffer = []
        self.element_found = False
        buffer = file.read(self._bufsize)
        collected_data = []
        while buffer:
            if not self.element_found:
                collected_data += buffer
            yield from self.feed(buffer)
            buffer = file.read(self._bufsize)
        # Any remaining data in self.buffer should be padding chars now
        self.buffer = None
        self.close()
        if not self.element_found:
            data = bytes(collected_data)
            raise ElementNotFound('The element to be streamed from was not found', data=bytes(data))

    def feed(self, data, isFinal=0):
        """Yield the current content of the character buffer."""
        DefusedExpatParser.feed(self, data=data, isFinal=isFinal)
        return self._decode_buffer()

    def _decode_buffer(self):
        remainder = ''
        for data in self.buffer:
            available = len(remainder) + len(data)
            overflow = available % 4  # Make sure we always decode a multiple of 4
            if remainder:
                data = (remainder + data)
                remainder = ''
            if overflow:
                remainder, data = data[-overflow:], data[:-overflow]
            if data:
                yield b64decode(data)
        self.buffer = [remainder] if remainder else []

Ancestors

  • defusedxml.expatreader.DefusedExpatParser
  • xml.sax.expatreader.ExpatParser
  • xml.sax.xmlreader.IncrementalParser
  • xml.sax.xmlreader.XMLReader
  • xml.sax.xmlreader.Locator

Methods

def feed(self, data, isFinal=0)

Yield the current content of the character buffer.

Expand source code
def feed(self, data, isFinal=0):
    """Yield the current content of the character buffer."""
    DefusedExpatParser.feed(self, data=data, isFinal=isFinal)
    return self._decode_buffer()
def parse(self, r)

Parse an XML document from a URL or an InputSource.

Expand source code
def parse(self, r):
    raw_source = r.raw
    # Like upstream but yields the return value of self.feed()
    raw_source = prepare_input_source(raw_source)
    self.prepareParser(raw_source)
    file = raw_source.getByteStream()
    self.buffer = []
    self.element_found = False
    buffer = file.read(self._bufsize)
    collected_data = []
    while buffer:
        if not self.element_found:
            collected_data += buffer
        yield from self.feed(buffer)
        buffer = file.read(self._bufsize)
    # Any remaining data in self.buffer should be padding chars now
    self.buffer = None
    self.close()
    if not self.element_found:
        data = bytes(collected_data)
        raise ElementNotFound('The element to be streamed from was not found', data=bytes(data))
class StreamingContentHandler (parser, ns, element_name)

A SAX content handler that returns a character data for a single element back to the parser. The parser must have a 'buffer' attribute we can append data to.

Expand source code
class StreamingContentHandler(xml.sax.handler.ContentHandler):
    """A SAX content handler that returns a character data for a single element back to the parser. The parser must have
    a 'buffer' attribute we can append data to.
    """

    def __init__(self, parser, ns, element_name):
        xml.sax.handler.ContentHandler.__init__(self)
        self._parser = parser
        self._ns = ns
        self._element_name = element_name
        self._parsing = False

    def startElementNS(self, name, qname, attrs):
        if name == (self._ns, self._element_name):
            # we can expect element data next
            self._parsing = True
            self._parser.element_found = True

    def endElementNS(self, name, qname):
        if name == (self._ns, self._element_name):
            # all element data received
            self._parsing = False

    def characters(self, content):
        if not self._parsing:
            return
        self._parser.buffer.append(content)

Ancestors

  • xml.sax.handler.ContentHandler

Methods

def characters(self, content)

Receive notification of character data.

The Parser will call this method to report each chunk of character data. SAX parsers may return all contiguous character data in a single chunk, or they may split it into several chunks; however, all of the characters in any single event must come from the same external entity so that the Locator provides useful information.

Expand source code
def characters(self, content):
    if not self._parsing:
        return
    self._parser.buffer.append(content)
def endElementNS(self, name, qname)

Signals the end of an element in namespace mode.

The name parameter contains the name of the element type, just as with the startElementNS event.

Expand source code
def endElementNS(self, name, qname):
    if name == (self._ns, self._element_name):
        # all element data received
        self._parsing = False
def startElementNS(self, name, qname, attrs)

Signals the start of an element in namespace mode.

The name parameter contains the name of the element type as a (uri, localname) tuple, the qname parameter the raw XML 1.0 name used in the source document, and the attrs parameter holds an instance of the Attributes class containing the attributes of the element.

The uri part of the name tuple is None for elements which have no namespace.

Expand source code
def startElementNS(self, name, qname, attrs):
    if name == (self._ns, self._element_name):
        # we can expect element data next
        self._parsing = True
        self._parser.element_found = True
exchangelib-4.6.1/docs/exchangelib/version.html000066400000000000000000001270131414601472700215610ustar00rootroot00000000000000 exchangelib.version API documentation

Module exchangelib.version

Expand source code
import logging
import re

from .errors import TransportError, ResponseMessageError
from .util import xml_to_str, TNS

log = logging.getLogger(__name__)

# Legend for dict:
#   Key: shortname
#   Values: (EWS API version ID, full name)

# 'shortname' comes from types.xsd and is the official version of the server, corresponding to the version numbers
# supplied in SOAP headers. 'API version' is the version name supplied in the RequestServerVersion element in SOAP
# headers and describes the EWS API version the server implements. Valid values for this element are described here:
#    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/requestserverversion

VERSIONS = {
    'Exchange2007': ('Exchange2007', 'Microsoft Exchange Server 2007'),
    'Exchange2007_SP1': ('Exchange2007_SP1', 'Microsoft Exchange Server 2007 SP1'),
    'Exchange2007_SP2': ('Exchange2007_SP1', 'Microsoft Exchange Server 2007 SP2'),
    'Exchange2007_SP3': ('Exchange2007_SP1', 'Microsoft Exchange Server 2007 SP3'),
    'Exchange2010': ('Exchange2010', 'Microsoft Exchange Server 2010'),
    'Exchange2010_SP1': ('Exchange2010_SP1', 'Microsoft Exchange Server 2010 SP1'),
    'Exchange2010_SP2': ('Exchange2010_SP2', 'Microsoft Exchange Server 2010 SP2'),
    'Exchange2010_SP3': ('Exchange2010_SP2', 'Microsoft Exchange Server 2010 SP3'),
    'Exchange2013': ('Exchange2013', 'Microsoft Exchange Server 2013'),
    'Exchange2013_SP1': ('Exchange2013_SP1', 'Microsoft Exchange Server 2013 SP1'),
    'Exchange2015': ('Exchange2015', 'Microsoft Exchange Server 2015'),
    'Exchange2015_SP1': ('Exchange2015_SP1', 'Microsoft Exchange Server 2015 SP1'),
    'Exchange2016': ('Exchange2016', 'Microsoft Exchange Server 2016'),
    'Exchange2019': ('Exchange2019', 'Microsoft Exchange Server 2019'),
}

# Build a list of unique API versions, used when guessing API version supported by the server. Use reverse order so we
# get the newest API version supported by the server.
API_VERSIONS = sorted({v[0] for v in VERSIONS.values()}, reverse=True)


class Build:
    """Holds methods for working with build numbers."""

    # List of build numbers here: https://docs.microsoft.com/en-us/exchange/new-features/build-numbers-and-release-dates
    API_VERSION_MAP = {
        8: {
            0: 'Exchange2007',
            1: 'Exchange2007_SP1',
            2: 'Exchange2007_SP1',
            3: 'Exchange2007_SP1',
        },
        14: {
            0: 'Exchange2010',
            1: 'Exchange2010_SP1',
            2: 'Exchange2010_SP2',
            3: 'Exchange2010_SP2',
        },
        15: {
            0: 'Exchange2013',  # Minor builds starting from 847 are Exchange2013_SP1, see api_version()
            1: 'Exchange2016',
            2: 'Exchange2019',
            20: 'Exchange2016',  # This is Office365. See issue #221
        },
    }

    __slots__ = 'major_version', 'minor_version', 'major_build', 'minor_build'

    def __init__(self, major_version, minor_version, major_build=0, minor_build=0):
        if not isinstance(major_version, int):
            raise ValueError("'major_version' must be an integer")
        if not isinstance(minor_version, int):
            raise ValueError("'minor_version' must be an integer")
        if not isinstance(major_build, int):
            raise ValueError("'major_build' must be an integer")
        if not isinstance(minor_build, int):
            raise ValueError("'minor_build' must be an integer")
        self.major_version = major_version
        self.minor_version = minor_version
        self.major_build = major_build
        self.minor_build = minor_build
        if major_version < 8:
            raise ValueError("Exchange major versions below 8 don't support EWS (%s)" % self)

    @classmethod
    def from_xml(cls, elem):
        xml_elems_map = {
            'major_version': 'MajorVersion',
            'minor_version': 'MinorVersion',
            'major_build': 'MajorBuildNumber',
            'minor_build': 'MinorBuildNumber',
        }
        kwargs = {}
        for k, xml_elem in xml_elems_map.items():
            v = elem.get(xml_elem)
            if v is None:
                raise ValueError()
            kwargs[k] = int(v)  # Also raises ValueError
        return cls(**kwargs)

    @classmethod
    def from_hex_string(cls, s):
        """Parse a server version string as returned in an autodiscover response. The process is described here:
        https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/serverversion-pox#example

        The string is a hex string that, converted to a 32-bit binary, encodes the server version. The rules are:
            * The first 4 bits contain the version number structure version. Can be ignored
            * The next 6 bits contain the major version number
            * The next 6 bits contain the minor version number
            * The next bit contains a flag. Can be ignored
            * The next 15 bits contain the major build number

        :param s:
        """
        bin_s = '{:032b}'.format(int(s, 16))  # Convert string to 32-bit binary string
        major_version = int(bin_s[4:10], 2)
        minor_version = int(bin_s[10:16], 2)
        build_number = int(bin_s[17:32], 2)
        return cls(major_version=major_version, minor_version=minor_version, major_build=build_number)

    def api_version(self):
        if EXCHANGE_2013_SP1 <= self < EXCHANGE_2016:
            return 'Exchange2013_SP1'
        try:
            return self.API_VERSION_MAP[self.major_version][self.minor_version]
        except KeyError:
            raise ValueError('API version for build %s is unknown' % self)

    def fullname(self):
        return VERSIONS[self.api_version()][1]

    def __cmp__(self, other):
        # __cmp__ is not a magic method in Python3. We'll just use it here to implement comparison operators
        c = (self.major_version > other.major_version) - (self.major_version < other.major_version)
        if c != 0:
            return c
        c = (self.minor_version > other.minor_version) - (self.minor_version < other.minor_version)
        if c != 0:
            return c
        c = (self.major_build > other.major_build) - (self.major_build < other.major_build)
        if c != 0:
            return c
        return (self.minor_build > other.minor_build) - (self.minor_build < other.minor_build)

    def __eq__(self, other):
        return self.__cmp__(other) == 0

    def __hash__(self):
        return hash(repr(self))

    def __ne__(self, other):
        return self.__cmp__(other) != 0

    def __lt__(self, other):
        return self.__cmp__(other) < 0

    def __le__(self, other):
        return self.__cmp__(other) <= 0

    def __gt__(self, other):
        return self.__cmp__(other) > 0

    def __ge__(self, other):
        return self.__cmp__(other) >= 0

    def __str__(self):
        return '%s.%s.%s.%s' % (self.major_version, self.minor_version, self.major_build, self.minor_build)

    def __repr__(self):
        return self.__class__.__name__ \
               + repr((self.major_version, self.minor_version, self.major_build, self.minor_build))


# Helpers for comparison operations elsewhere in this package
EXCHANGE_2007 = Build(8, 0)
EXCHANGE_2007_SP1 = Build(8, 1)
EXCHANGE_2010 = Build(14, 0)
EXCHANGE_2010_SP1 = Build(14, 1)
EXCHANGE_2010_SP2 = Build(14, 2)
EXCHANGE_2013 = Build(15, 0)
EXCHANGE_2013_SP1 = Build(15, 0, 847)
EXCHANGE_2016 = Build(15, 1)
EXCHANGE_2019 = Build(15, 2)
EXCHANGE_O365 = Build(15, 20)


class Version:
    """Holds information about the server version."""

    __slots__ = 'build', 'api_version'

    def __init__(self, build, api_version=None):
        if not isinstance(build, (Build, type(None))):
            raise ValueError("'build' must be a Build instance")
        self.build = build
        if api_version is None:
            self.api_version = build.api_version()
        else:
            if not isinstance(api_version, str):
                raise ValueError("'api_version' must be a string")
            self.api_version = api_version

    @property
    def fullname(self):
        return VERSIONS[self.api_version][1]

    @classmethod
    def guess(cls, protocol, api_version_hint=None):
        """Ask the server which version it has. We haven't set up an Account object yet, so we generate requests
        by hand. We only need a response header containing a ServerVersionInfo element.

        To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that
        without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this
        package supports, until we get a valid response.

        :param protocol:
        :param api_version_hint:  (Default value = None)
        """
        from .services import ResolveNames
        # The protocol doesn't have a version yet, so default to latest supported version if we don't have a hint.
        api_version = api_version_hint or API_VERSIONS[0]
        log.debug('Asking server for version info using API version %s', api_version)
        # We don't know the build version yet. Hopefully, the server will report it in the SOAP header. Lots of
        # places expect a version to have a build, so this is a bit dangerous, but passing a fake build around is also
        # dangerous. Make sure the call to ResolveNames does not require a version build.
        protocol.config.version = Version(build=None, api_version=api_version)
        # Use ResolveNames as a minimal request to the server to test if the version is correct. If not, ResolveNames
        # will try to guess the version automatically.
        name = str(protocol.credentials) if protocol.credentials and str(protocol.credentials) else 'DUMMY'
        try:
            list(ResolveNames(protocol=protocol).call(unresolved_entries=[name]))
        except ResponseMessageError as e:
            # We may have survived long enough to get a new version
            if not protocol.config.version.build:
                raise TransportError('No valid version headers found in response (%r)' % e)
        if not protocol.config.version.build:
            raise TransportError('No valid version headers found in response')
        return protocol.version

    @staticmethod
    def _is_invalid_version_string(version):
        # Check if a version string is bogus, e.g. V2_, V2015_ or V2018_
        return re.match(r'V[0-9]{1,4}_.*', version)

    @classmethod
    def from_soap_header(cls, requested_api_version, header):
        info = header.find('{%s}ServerVersionInfo' % TNS)
        if info is None:
            raise TransportError('No ServerVersionInfo in header: %r' % xml_to_str(header))
        try:
            build = Build.from_xml(elem=info)
        except ValueError:
            raise TransportError('Bad ServerVersionInfo in response: %r' % xml_to_str(header))
        # Not all Exchange servers send the Version element
        api_version_from_server = info.get('Version') or build.api_version()
        if api_version_from_server != requested_api_version:
            if cls._is_invalid_version_string(api_version_from_server):
                # For unknown reasons, Office 365 may respond with an API version strings that is invalid in a request.
                # Detect these so we can fallback to a valid version string.
                log.debug('API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version,
                          api_version_from_server, requested_api_version)
                api_version_from_server = requested_api_version
            else:
                # Trust API version from server response
                log.debug('API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version,
                          api_version_from_server, api_version_from_server)
        return cls(build=build, api_version=api_version_from_server)

    def __eq__(self, other):
        if self.api_version != other.api_version:
            return False
        if self.build and not other.build:
            return False
        if other.build and not self.build:
            return False
        return self.build == other.build

    def __repr__(self):
        return self.__class__.__name__ + repr((self.build, self.api_version))

    def __str__(self):
        return 'Build=%s, API=%s, Fullname=%s' % (self.build, self.api_version, self.fullname)

Classes

class Build (major_version, minor_version, major_build=0, minor_build=0)

Holds methods for working with build numbers.

Expand source code
class Build:
    """Holds methods for working with build numbers."""

    # List of build numbers here: https://docs.microsoft.com/en-us/exchange/new-features/build-numbers-and-release-dates
    API_VERSION_MAP = {
        8: {
            0: 'Exchange2007',
            1: 'Exchange2007_SP1',
            2: 'Exchange2007_SP1',
            3: 'Exchange2007_SP1',
        },
        14: {
            0: 'Exchange2010',
            1: 'Exchange2010_SP1',
            2: 'Exchange2010_SP2',
            3: 'Exchange2010_SP2',
        },
        15: {
            0: 'Exchange2013',  # Minor builds starting from 847 are Exchange2013_SP1, see api_version()
            1: 'Exchange2016',
            2: 'Exchange2019',
            20: 'Exchange2016',  # This is Office365. See issue #221
        },
    }

    __slots__ = 'major_version', 'minor_version', 'major_build', 'minor_build'

    def __init__(self, major_version, minor_version, major_build=0, minor_build=0):
        if not isinstance(major_version, int):
            raise ValueError("'major_version' must be an integer")
        if not isinstance(minor_version, int):
            raise ValueError("'minor_version' must be an integer")
        if not isinstance(major_build, int):
            raise ValueError("'major_build' must be an integer")
        if not isinstance(minor_build, int):
            raise ValueError("'minor_build' must be an integer")
        self.major_version = major_version
        self.minor_version = minor_version
        self.major_build = major_build
        self.minor_build = minor_build
        if major_version < 8:
            raise ValueError("Exchange major versions below 8 don't support EWS (%s)" % self)

    @classmethod
    def from_xml(cls, elem):
        xml_elems_map = {
            'major_version': 'MajorVersion',
            'minor_version': 'MinorVersion',
            'major_build': 'MajorBuildNumber',
            'minor_build': 'MinorBuildNumber',
        }
        kwargs = {}
        for k, xml_elem in xml_elems_map.items():
            v = elem.get(xml_elem)
            if v is None:
                raise ValueError()
            kwargs[k] = int(v)  # Also raises ValueError
        return cls(**kwargs)

    @classmethod
    def from_hex_string(cls, s):
        """Parse a server version string as returned in an autodiscover response. The process is described here:
        https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/serverversion-pox#example

        The string is a hex string that, converted to a 32-bit binary, encodes the server version. The rules are:
            * The first 4 bits contain the version number structure version. Can be ignored
            * The next 6 bits contain the major version number
            * The next 6 bits contain the minor version number
            * The next bit contains a flag. Can be ignored
            * The next 15 bits contain the major build number

        :param s:
        """
        bin_s = '{:032b}'.format(int(s, 16))  # Convert string to 32-bit binary string
        major_version = int(bin_s[4:10], 2)
        minor_version = int(bin_s[10:16], 2)
        build_number = int(bin_s[17:32], 2)
        return cls(major_version=major_version, minor_version=minor_version, major_build=build_number)

    def api_version(self):
        if EXCHANGE_2013_SP1 <= self < EXCHANGE_2016:
            return 'Exchange2013_SP1'
        try:
            return self.API_VERSION_MAP[self.major_version][self.minor_version]
        except KeyError:
            raise ValueError('API version for build %s is unknown' % self)

    def fullname(self):
        return VERSIONS[self.api_version()][1]

    def __cmp__(self, other):
        # __cmp__ is not a magic method in Python3. We'll just use it here to implement comparison operators
        c = (self.major_version > other.major_version) - (self.major_version < other.major_version)
        if c != 0:
            return c
        c = (self.minor_version > other.minor_version) - (self.minor_version < other.minor_version)
        if c != 0:
            return c
        c = (self.major_build > other.major_build) - (self.major_build < other.major_build)
        if c != 0:
            return c
        return (self.minor_build > other.minor_build) - (self.minor_build < other.minor_build)

    def __eq__(self, other):
        return self.__cmp__(other) == 0

    def __hash__(self):
        return hash(repr(self))

    def __ne__(self, other):
        return self.__cmp__(other) != 0

    def __lt__(self, other):
        return self.__cmp__(other) < 0

    def __le__(self, other):
        return self.__cmp__(other) <= 0

    def __gt__(self, other):
        return self.__cmp__(other) > 0

    def __ge__(self, other):
        return self.__cmp__(other) >= 0

    def __str__(self):
        return '%s.%s.%s.%s' % (self.major_version, self.minor_version, self.major_build, self.minor_build)

    def __repr__(self):
        return self.__class__.__name__ \
               + repr((self.major_version, self.minor_version, self.major_build, self.minor_build))

Class variables

var API_VERSION_MAP

Static methods

def from_hex_string(s)

Parse a server version string as returned in an autodiscover response. The process is described here: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/serverversion-pox#example

The string is a hex string that, converted to a 32-bit binary, encodes the server version. The rules are: * The first 4 bits contain the version number structure version. Can be ignored * The next 6 bits contain the major version number * The next 6 bits contain the minor version number * The next bit contains a flag. Can be ignored * The next 15 bits contain the major build number

:param s:

Expand source code
@classmethod
def from_hex_string(cls, s):
    """Parse a server version string as returned in an autodiscover response. The process is described here:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/serverversion-pox#example

    The string is a hex string that, converted to a 32-bit binary, encodes the server version. The rules are:
        * The first 4 bits contain the version number structure version. Can be ignored
        * The next 6 bits contain the major version number
        * The next 6 bits contain the minor version number
        * The next bit contains a flag. Can be ignored
        * The next 15 bits contain the major build number

    :param s:
    """
    bin_s = '{:032b}'.format(int(s, 16))  # Convert string to 32-bit binary string
    major_version = int(bin_s[4:10], 2)
    minor_version = int(bin_s[10:16], 2)
    build_number = int(bin_s[17:32], 2)
    return cls(major_version=major_version, minor_version=minor_version, major_build=build_number)
def from_xml(elem)
Expand source code
@classmethod
def from_xml(cls, elem):
    xml_elems_map = {
        'major_version': 'MajorVersion',
        'minor_version': 'MinorVersion',
        'major_build': 'MajorBuildNumber',
        'minor_build': 'MinorBuildNumber',
    }
    kwargs = {}
    for k, xml_elem in xml_elems_map.items():
        v = elem.get(xml_elem)
        if v is None:
            raise ValueError()
        kwargs[k] = int(v)  # Also raises ValueError
    return cls(**kwargs)

Instance variables

var major_build

Return an attribute of instance, which is of type owner.

var major_version

Return an attribute of instance, which is of type owner.

var minor_build

Return an attribute of instance, which is of type owner.

var minor_version

Return an attribute of instance, which is of type owner.

Methods

def api_version(self)
Expand source code
def api_version(self):
    if EXCHANGE_2013_SP1 <= self < EXCHANGE_2016:
        return 'Exchange2013_SP1'
    try:
        return self.API_VERSION_MAP[self.major_version][self.minor_version]
    except KeyError:
        raise ValueError('API version for build %s is unknown' % self)
def fullname(self)
Expand source code
def fullname(self):
    return VERSIONS[self.api_version()][1]
class Version (build, api_version=None)

Holds information about the server version.

Expand source code
class Version:
    """Holds information about the server version."""

    __slots__ = 'build', 'api_version'

    def __init__(self, build, api_version=None):
        if not isinstance(build, (Build, type(None))):
            raise ValueError("'build' must be a Build instance")
        self.build = build
        if api_version is None:
            self.api_version = build.api_version()
        else:
            if not isinstance(api_version, str):
                raise ValueError("'api_version' must be a string")
            self.api_version = api_version

    @property
    def fullname(self):
        return VERSIONS[self.api_version][1]

    @classmethod
    def guess(cls, protocol, api_version_hint=None):
        """Ask the server which version it has. We haven't set up an Account object yet, so we generate requests
        by hand. We only need a response header containing a ServerVersionInfo element.

        To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that
        without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this
        package supports, until we get a valid response.

        :param protocol:
        :param api_version_hint:  (Default value = None)
        """
        from .services import ResolveNames
        # The protocol doesn't have a version yet, so default to latest supported version if we don't have a hint.
        api_version = api_version_hint or API_VERSIONS[0]
        log.debug('Asking server for version info using API version %s', api_version)
        # We don't know the build version yet. Hopefully, the server will report it in the SOAP header. Lots of
        # places expect a version to have a build, so this is a bit dangerous, but passing a fake build around is also
        # dangerous. Make sure the call to ResolveNames does not require a version build.
        protocol.config.version = Version(build=None, api_version=api_version)
        # Use ResolveNames as a minimal request to the server to test if the version is correct. If not, ResolveNames
        # will try to guess the version automatically.
        name = str(protocol.credentials) if protocol.credentials and str(protocol.credentials) else 'DUMMY'
        try:
            list(ResolveNames(protocol=protocol).call(unresolved_entries=[name]))
        except ResponseMessageError as e:
            # We may have survived long enough to get a new version
            if not protocol.config.version.build:
                raise TransportError('No valid version headers found in response (%r)' % e)
        if not protocol.config.version.build:
            raise TransportError('No valid version headers found in response')
        return protocol.version

    @staticmethod
    def _is_invalid_version_string(version):
        # Check if a version string is bogus, e.g. V2_, V2015_ or V2018_
        return re.match(r'V[0-9]{1,4}_.*', version)

    @classmethod
    def from_soap_header(cls, requested_api_version, header):
        info = header.find('{%s}ServerVersionInfo' % TNS)
        if info is None:
            raise TransportError('No ServerVersionInfo in header: %r' % xml_to_str(header))
        try:
            build = Build.from_xml(elem=info)
        except ValueError:
            raise TransportError('Bad ServerVersionInfo in response: %r' % xml_to_str(header))
        # Not all Exchange servers send the Version element
        api_version_from_server = info.get('Version') or build.api_version()
        if api_version_from_server != requested_api_version:
            if cls._is_invalid_version_string(api_version_from_server):
                # For unknown reasons, Office 365 may respond with an API version strings that is invalid in a request.
                # Detect these so we can fallback to a valid version string.
                log.debug('API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version,
                          api_version_from_server, requested_api_version)
                api_version_from_server = requested_api_version
            else:
                # Trust API version from server response
                log.debug('API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version,
                          api_version_from_server, api_version_from_server)
        return cls(build=build, api_version=api_version_from_server)

    def __eq__(self, other):
        if self.api_version != other.api_version:
            return False
        if self.build and not other.build:
            return False
        if other.build and not self.build:
            return False
        return self.build == other.build

    def __repr__(self):
        return self.__class__.__name__ + repr((self.build, self.api_version))

    def __str__(self):
        return 'Build=%s, API=%s, Fullname=%s' % (self.build, self.api_version, self.fullname)

Static methods

def from_soap_header(requested_api_version, header)
Expand source code
@classmethod
def from_soap_header(cls, requested_api_version, header):
    info = header.find('{%s}ServerVersionInfo' % TNS)
    if info is None:
        raise TransportError('No ServerVersionInfo in header: %r' % xml_to_str(header))
    try:
        build = Build.from_xml(elem=info)
    except ValueError:
        raise TransportError('Bad ServerVersionInfo in response: %r' % xml_to_str(header))
    # Not all Exchange servers send the Version element
    api_version_from_server = info.get('Version') or build.api_version()
    if api_version_from_server != requested_api_version:
        if cls._is_invalid_version_string(api_version_from_server):
            # For unknown reasons, Office 365 may respond with an API version strings that is invalid in a request.
            # Detect these so we can fallback to a valid version string.
            log.debug('API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version,
                      api_version_from_server, requested_api_version)
            api_version_from_server = requested_api_version
        else:
            # Trust API version from server response
            log.debug('API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version,
                      api_version_from_server, api_version_from_server)
    return cls(build=build, api_version=api_version_from_server)
def guess(protocol, api_version_hint=None)

Ask the server which version it has. We haven't set up an Account object yet, so we generate requests by hand. We only need a response header containing a ServerVersionInfo element.

To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this package supports, until we get a valid response.

:param protocol: :param api_version_hint: (Default value = None)

Expand source code
@classmethod
def guess(cls, protocol, api_version_hint=None):
    """Ask the server which version it has. We haven't set up an Account object yet, so we generate requests
    by hand. We only need a response header containing a ServerVersionInfo element.

    To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that
    without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this
    package supports, until we get a valid response.

    :param protocol:
    :param api_version_hint:  (Default value = None)
    """
    from .services import ResolveNames
    # The protocol doesn't have a version yet, so default to latest supported version if we don't have a hint.
    api_version = api_version_hint or API_VERSIONS[0]
    log.debug('Asking server for version info using API version %s', api_version)
    # We don't know the build version yet. Hopefully, the server will report it in the SOAP header. Lots of
    # places expect a version to have a build, so this is a bit dangerous, but passing a fake build around is also
    # dangerous. Make sure the call to ResolveNames does not require a version build.
    protocol.config.version = Version(build=None, api_version=api_version)
    # Use ResolveNames as a minimal request to the server to test if the version is correct. If not, ResolveNames
    # will try to guess the version automatically.
    name = str(protocol.credentials) if protocol.credentials and str(protocol.credentials) else 'DUMMY'
    try:
        list(ResolveNames(protocol=protocol).call(unresolved_entries=[name]))
    except ResponseMessageError as e:
        # We may have survived long enough to get a new version
        if not protocol.config.version.build:
            raise TransportError('No valid version headers found in response (%r)' % e)
    if not protocol.config.version.build:
        raise TransportError('No valid version headers found in response')
    return protocol.version

Instance variables

var api_version

Return an attribute of instance, which is of type owner.

var build

Return an attribute of instance, which is of type owner.

var fullname
Expand source code
@property
def fullname(self):
    return VERSIONS[self.api_version][1]
exchangelib-4.6.1/docs/exchangelib/winzone.html000066400000000000000000001654231414601472700215740ustar00rootroot00000000000000 exchangelib.winzone API documentation

Module exchangelib.winzone

A dict to translate from IANA location name to Windows timezone name. Translations taken from http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml

Expand source code
"""A dict to translate from IANA location name to Windows timezone name. Translations taken from
http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml
"""
import re

import requests

from .util import to_xml

CLDR_WINZONE_URL = 'https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml'
DEFAULT_TERRITORY = '001'
CLDR_WINZONE_TYPE_VERSION = '2021a'
CLDR_WINZONE_OTHER_VERSION = '7e11800'


def generate_map(timeout=10):
    """Create a new CLDR_TO_MS_TIMEZONE_MAP map from the CLDR data. Used when the CLDR database is updated.

    :param timeout:  (Default value = 10)
    :return:
    """
    r = requests.get(CLDR_WINZONE_URL, timeout=timeout)
    if r.status_code != 200:
        raise ValueError('Unexpected response: %s' % r)
    tz_map = {}
    timezones_elem = to_xml(r.content).find('windowsZones').find('mapTimezones')
    type_version = timezones_elem.get('typeVersion')
    other_version = timezones_elem.get('otherVersion')
    for e in timezones_elem.findall('mapZone'):
        for location in re.split(r'\s+', e.get('type')):
            if e.get('territory') == DEFAULT_TERRITORY or location not in tz_map:
                # Prefer default territory. This is so MS_TIMEZONE_TO_IANA_MAP maps from MS timezone ID back to the
                # "preferred" region/location timezone name.
                if not location:
                    raise ValueError('Expected location')
                tz_map[location] = e.get('other'), e.get('territory')
    return type_version, other_version, tz_map


# This map is generated irregularly from generate_map(). Do not edit manually - make corrections to
# IANA_TO_MS_TIMEZONE_MAP instead. We provide this map to avoid hammering the CLDR_WINZONE_URL.
#
# This list was generated from CLDR_WINZONE_URL version CLDR_WINZONE_VERSION.
CLDR_TO_MS_TIMEZONE_MAP = {
    'Africa/Abidjan': ('Greenwich Standard Time', 'CI'),
    'Africa/Accra': ('Greenwich Standard Time', 'GH'),
    'Africa/Addis_Ababa': ('E. Africa Standard Time', 'ET'),
    'Africa/Algiers': ('W. Central Africa Standard Time', 'DZ'),
    'Africa/Asmera': ('E. Africa Standard Time', 'ER'),
    'Africa/Bamako': ('Greenwich Standard Time', 'ML'),
    'Africa/Bangui': ('W. Central Africa Standard Time', 'CF'),
    'Africa/Banjul': ('Greenwich Standard Time', 'GM'),
    'Africa/Bissau': ('Greenwich Standard Time', 'GW'),
    'Africa/Blantyre': ('South Africa Standard Time', 'MW'),
    'Africa/Brazzaville': ('W. Central Africa Standard Time', 'CG'),
    'Africa/Bujumbura': ('South Africa Standard Time', 'BI'),
    'Africa/Cairo': ('Egypt Standard Time', '001'),
    'Africa/Casablanca': ('Morocco Standard Time', '001'),
    'Africa/Ceuta': ('Romance Standard Time', 'ES'),
    'Africa/Conakry': ('Greenwich Standard Time', 'GN'),
    'Africa/Dakar': ('Greenwich Standard Time', 'SN'),
    'Africa/Dar_es_Salaam': ('E. Africa Standard Time', 'TZ'),
    'Africa/Djibouti': ('E. Africa Standard Time', 'DJ'),
    'Africa/Douala': ('W. Central Africa Standard Time', 'CM'),
    'Africa/El_Aaiun': ('Morocco Standard Time', 'EH'),
    'Africa/Freetown': ('Greenwich Standard Time', 'SL'),
    'Africa/Gaborone': ('South Africa Standard Time', 'BW'),
    'Africa/Harare': ('South Africa Standard Time', 'ZW'),
    'Africa/Johannesburg': ('South Africa Standard Time', '001'),
    'Africa/Juba': ('South Sudan Standard Time', '001'),
    'Africa/Kampala': ('E. Africa Standard Time', 'UG'),
    'Africa/Khartoum': ('Sudan Standard Time', '001'),
    'Africa/Kigali': ('South Africa Standard Time', 'RW'),
    'Africa/Kinshasa': ('W. Central Africa Standard Time', 'CD'),
    'Africa/Lagos': ('W. Central Africa Standard Time', '001'),
    'Africa/Libreville': ('W. Central Africa Standard Time', 'GA'),
    'Africa/Lome': ('Greenwich Standard Time', 'TG'),
    'Africa/Luanda': ('W. Central Africa Standard Time', 'AO'),
    'Africa/Lubumbashi': ('South Africa Standard Time', 'CD'),
    'Africa/Lusaka': ('South Africa Standard Time', 'ZM'),
    'Africa/Malabo': ('W. Central Africa Standard Time', 'GQ'),
    'Africa/Maputo': ('South Africa Standard Time', 'MZ'),
    'Africa/Maseru': ('South Africa Standard Time', 'LS'),
    'Africa/Mbabane': ('South Africa Standard Time', 'SZ'),
    'Africa/Mogadishu': ('E. Africa Standard Time', 'SO'),
    'Africa/Monrovia': ('Greenwich Standard Time', 'LR'),
    'Africa/Nairobi': ('E. Africa Standard Time', '001'),
    'Africa/Ndjamena': ('W. Central Africa Standard Time', 'TD'),
    'Africa/Niamey': ('W. Central Africa Standard Time', 'NE'),
    'Africa/Nouakchott': ('Greenwich Standard Time', 'MR'),
    'Africa/Ouagadougou': ('Greenwich Standard Time', 'BF'),
    'Africa/Porto-Novo': ('W. Central Africa Standard Time', 'BJ'),
    'Africa/Sao_Tome': ('Sao Tome Standard Time', '001'),
    'Africa/Tripoli': ('Libya Standard Time', '001'),
    'Africa/Tunis': ('W. Central Africa Standard Time', 'TN'),
    'Africa/Windhoek': ('Namibia Standard Time', '001'),
    'America/Adak': ('Aleutian Standard Time', '001'),
    'America/Anchorage': ('Alaskan Standard Time', '001'),
    'America/Anguilla': ('SA Western Standard Time', 'AI'),
    'America/Antigua': ('SA Western Standard Time', 'AG'),
    'America/Araguaina': ('Tocantins Standard Time', '001'),
    'America/Argentina/La_Rioja': ('Argentina Standard Time', 'AR'),
    'America/Argentina/Rio_Gallegos': ('Argentina Standard Time', 'AR'),
    'America/Argentina/Salta': ('Argentina Standard Time', 'AR'),
    'America/Argentina/San_Juan': ('Argentina Standard Time', 'AR'),
    'America/Argentina/San_Luis': ('Argentina Standard Time', 'AR'),
    'America/Argentina/Tucuman': ('Argentina Standard Time', 'AR'),
    'America/Argentina/Ushuaia': ('Argentina Standard Time', 'AR'),
    'America/Aruba': ('SA Western Standard Time', 'AW'),
    'America/Asuncion': ('Paraguay Standard Time', '001'),
    'America/Bahia': ('Bahia Standard Time', '001'),
    'America/Bahia_Banderas': ('Central Standard Time (Mexico)', 'MX'),
    'America/Barbados': ('SA Western Standard Time', 'BB'),
    'America/Belem': ('SA Eastern Standard Time', 'BR'),
    'America/Belize': ('Central America Standard Time', 'BZ'),
    'America/Blanc-Sablon': ('SA Western Standard Time', 'CA'),
    'America/Boa_Vista': ('SA Western Standard Time', 'BR'),
    'America/Bogota': ('SA Pacific Standard Time', '001'),
    'America/Boise': ('Mountain Standard Time', 'US'),
    'America/Buenos_Aires': ('Argentina Standard Time', '001'),
    'America/Cambridge_Bay': ('Mountain Standard Time', 'CA'),
    'America/Campo_Grande': ('Central Brazilian Standard Time', 'BR'),
    'America/Cancun': ('Eastern Standard Time (Mexico)', '001'),
    'America/Caracas': ('Venezuela Standard Time', '001'),
    'America/Catamarca': ('Argentina Standard Time', 'AR'),
    'America/Cayenne': ('SA Eastern Standard Time', '001'),
    'America/Cayman': ('SA Pacific Standard Time', 'KY'),
    'America/Chicago': ('Central Standard Time', '001'),
    'America/Chihuahua': ('Mountain Standard Time (Mexico)', '001'),
    'America/Coral_Harbour': ('SA Pacific Standard Time', 'CA'),
    'America/Cordoba': ('Argentina Standard Time', 'AR'),
    'America/Costa_Rica': ('Central America Standard Time', 'CR'),
    'America/Creston': ('US Mountain Standard Time', 'CA'),
    'America/Cuiaba': ('Central Brazilian Standard Time', '001'),
    'America/Curacao': ('SA Western Standard Time', 'CW'),
    'America/Danmarkshavn': ('Greenwich Standard Time', 'GL'),
    'America/Dawson': ('Yukon Standard Time', 'CA'),
    'America/Dawson_Creek': ('US Mountain Standard Time', 'CA'),
    'America/Denver': ('Mountain Standard Time', '001'),
    'America/Detroit': ('Eastern Standard Time', 'US'),
    'America/Dominica': ('SA Western Standard Time', 'DM'),
    'America/Edmonton': ('Mountain Standard Time', 'CA'),
    'America/Eirunepe': ('SA Pacific Standard Time', 'BR'),
    'America/El_Salvador': ('Central America Standard Time', 'SV'),
    'America/Fort_Nelson': ('US Mountain Standard Time', 'CA'),
    'America/Fortaleza': ('SA Eastern Standard Time', 'BR'),
    'America/Glace_Bay': ('Atlantic Standard Time', 'CA'),
    'America/Godthab': ('Greenland Standard Time', '001'),
    'America/Goose_Bay': ('Atlantic Standard Time', 'CA'),
    'America/Grand_Turk': ('Turks And Caicos Standard Time', '001'),
    'America/Grenada': ('SA Western Standard Time', 'GD'),
    'America/Guadeloupe': ('SA Western Standard Time', 'GP'),
    'America/Guatemala': ('Central America Standard Time', '001'),
    'America/Guayaquil': ('SA Pacific Standard Time', 'EC'),
    'America/Guyana': ('SA Western Standard Time', 'GY'),
    'America/Halifax': ('Atlantic Standard Time', '001'),
    'America/Havana': ('Cuba Standard Time', '001'),
    'America/Hermosillo': ('US Mountain Standard Time', 'MX'),
    'America/Indiana/Knox': ('Central Standard Time', 'US'),
    'America/Indiana/Marengo': ('US Eastern Standard Time', 'US'),
    'America/Indiana/Petersburg': ('Eastern Standard Time', 'US'),
    'America/Indiana/Tell_City': ('Central Standard Time', 'US'),
    'America/Indiana/Vevay': ('US Eastern Standard Time', 'US'),
    'America/Indiana/Vincennes': ('Eastern Standard Time', 'US'),
    'America/Indiana/Winamac': ('Eastern Standard Time', 'US'),
    'America/Indianapolis': ('US Eastern Standard Time', '001'),
    'America/Inuvik': ('Mountain Standard Time', 'CA'),
    'America/Iqaluit': ('Eastern Standard Time', 'CA'),
    'America/Jamaica': ('SA Pacific Standard Time', 'JM'),
    'America/Jujuy': ('Argentina Standard Time', 'AR'),
    'America/Juneau': ('Alaskan Standard Time', 'US'),
    'America/Kentucky/Monticello': ('Eastern Standard Time', 'US'),
    'America/Kralendijk': ('SA Western Standard Time', 'BQ'),
    'America/La_Paz': ('SA Western Standard Time', '001'),
    'America/Lima': ('SA Pacific Standard Time', 'PE'),
    'America/Los_Angeles': ('Pacific Standard Time', '001'),
    'America/Louisville': ('Eastern Standard Time', 'US'),
    'America/Lower_Princes': ('SA Western Standard Time', 'SX'),
    'America/Maceio': ('SA Eastern Standard Time', 'BR'),
    'America/Managua': ('Central America Standard Time', 'NI'),
    'America/Manaus': ('SA Western Standard Time', 'BR'),
    'America/Marigot': ('SA Western Standard Time', 'MF'),
    'America/Martinique': ('SA Western Standard Time', 'MQ'),
    'America/Matamoros': ('Central Standard Time', 'MX'),
    'America/Mazatlan': ('Mountain Standard Time (Mexico)', 'MX'),
    'America/Mendoza': ('Argentina Standard Time', 'AR'),
    'America/Menominee': ('Central Standard Time', 'US'),
    'America/Merida': ('Central Standard Time (Mexico)', 'MX'),
    'America/Metlakatla': ('Alaskan Standard Time', 'US'),
    'America/Mexico_City': ('Central Standard Time (Mexico)', '001'),
    'America/Miquelon': ('Saint Pierre Standard Time', '001'),
    'America/Moncton': ('Atlantic Standard Time', 'CA'),
    'America/Monterrey': ('Central Standard Time (Mexico)', 'MX'),
    'America/Montevideo': ('Montevideo Standard Time', '001'),
    'America/Montreal': ('Eastern Standard Time', 'CA'),
    'America/Montserrat': ('SA Western Standard Time', 'MS'),
    'America/Nassau': ('Eastern Standard Time', 'BS'),
    'America/New_York': ('Eastern Standard Time', '001'),
    'America/Nipigon': ('Eastern Standard Time', 'CA'),
    'America/Nome': ('Alaskan Standard Time', 'US'),
    'America/Noronha': ('UTC-02', 'BR'),
    'America/North_Dakota/Beulah': ('Central Standard Time', 'US'),
    'America/North_Dakota/Center': ('Central Standard Time', 'US'),
    'America/North_Dakota/New_Salem': ('Central Standard Time', 'US'),
    'America/Ojinaga': ('Mountain Standard Time', 'MX'),
    'America/Panama': ('SA Pacific Standard Time', 'PA'),
    'America/Pangnirtung': ('Eastern Standard Time', 'CA'),
    'America/Paramaribo': ('SA Eastern Standard Time', 'SR'),
    'America/Phoenix': ('US Mountain Standard Time', '001'),
    'America/Port-au-Prince': ('Haiti Standard Time', '001'),
    'America/Port_of_Spain': ('SA Western Standard Time', 'TT'),
    'America/Porto_Velho': ('SA Western Standard Time', 'BR'),
    'America/Puerto_Rico': ('SA Western Standard Time', 'PR'),
    'America/Punta_Arenas': ('Magallanes Standard Time', '001'),
    'America/Rainy_River': ('Central Standard Time', 'CA'),
    'America/Rankin_Inlet': ('Central Standard Time', 'CA'),
    'America/Recife': ('SA Eastern Standard Time', 'BR'),
    'America/Regina': ('Canada Central Standard Time', '001'),
    'America/Resolute': ('Central Standard Time', 'CA'),
    'America/Rio_Branco': ('SA Pacific Standard Time', 'BR'),
    'America/Santa_Isabel': ('Pacific Standard Time (Mexico)', 'MX'),
    'America/Santarem': ('SA Eastern Standard Time', 'BR'),
    'America/Santiago': ('Pacific SA Standard Time', '001'),
    'America/Santo_Domingo': ('SA Western Standard Time', 'DO'),
    'America/Sao_Paulo': ('E. South America Standard Time', '001'),
    'America/Scoresbysund': ('Azores Standard Time', 'GL'),
    'America/Sitka': ('Alaskan Standard Time', 'US'),
    'America/St_Barthelemy': ('SA Western Standard Time', 'BL'),
    'America/St_Johns': ('Newfoundland Standard Time', '001'),
    'America/St_Kitts': ('SA Western Standard Time', 'KN'),
    'America/St_Lucia': ('SA Western Standard Time', 'LC'),
    'America/St_Thomas': ('SA Western Standard Time', 'VI'),
    'America/St_Vincent': ('SA Western Standard Time', 'VC'),
    'America/Swift_Current': ('Canada Central Standard Time', 'CA'),
    'America/Tegucigalpa': ('Central America Standard Time', 'HN'),
    'America/Thule': ('Atlantic Standard Time', 'GL'),
    'America/Thunder_Bay': ('Eastern Standard Time', 'CA'),
    'America/Tijuana': ('Pacific Standard Time (Mexico)', '001'),
    'America/Toronto': ('Eastern Standard Time', 'CA'),
    'America/Tortola': ('SA Western Standard Time', 'VG'),
    'America/Vancouver': ('Pacific Standard Time', 'CA'),
    'America/Whitehorse': ('Yukon Standard Time', '001'),
    'America/Winnipeg': ('Central Standard Time', 'CA'),
    'America/Yakutat': ('Alaskan Standard Time', 'US'),
    'America/Yellowknife': ('Mountain Standard Time', 'CA'),
    'Antarctica/Casey': ('Central Pacific Standard Time', 'AQ'),
    'Antarctica/Davis': ('SE Asia Standard Time', 'AQ'),
    'Antarctica/DumontDUrville': ('West Pacific Standard Time', 'AQ'),
    'Antarctica/Macquarie': ('Tasmania Standard Time', 'AU'),
    'Antarctica/Mawson': ('West Asia Standard Time', 'AQ'),
    'Antarctica/McMurdo': ('New Zealand Standard Time', 'AQ'),
    'Antarctica/Palmer': ('SA Eastern Standard Time', 'AQ'),
    'Antarctica/Rothera': ('SA Eastern Standard Time', 'AQ'),
    'Antarctica/Syowa': ('E. Africa Standard Time', 'AQ'),
    'Antarctica/Vostok': ('Central Asia Standard Time', 'AQ'),
    'Arctic/Longyearbyen': ('W. Europe Standard Time', 'SJ'),
    'Asia/Aden': ('Arab Standard Time', 'YE'),
    'Asia/Almaty': ('Central Asia Standard Time', '001'),
    'Asia/Amman': ('Jordan Standard Time', '001'),
    'Asia/Anadyr': ('Russia Time Zone 11', 'RU'),
    'Asia/Aqtau': ('West Asia Standard Time', 'KZ'),
    'Asia/Aqtobe': ('West Asia Standard Time', 'KZ'),
    'Asia/Ashgabat': ('West Asia Standard Time', 'TM'),
    'Asia/Atyrau': ('West Asia Standard Time', 'KZ'),
    'Asia/Baghdad': ('Arabic Standard Time', '001'),
    'Asia/Bahrain': ('Arab Standard Time', 'BH'),
    'Asia/Baku': ('Azerbaijan Standard Time', '001'),
    'Asia/Bangkok': ('SE Asia Standard Time', '001'),
    'Asia/Barnaul': ('Altai Standard Time', '001'),
    'Asia/Beirut': ('Middle East Standard Time', '001'),
    'Asia/Bishkek': ('Central Asia Standard Time', 'KG'),
    'Asia/Brunei': ('Singapore Standard Time', 'BN'),
    'Asia/Calcutta': ('India Standard Time', '001'),
    'Asia/Chita': ('Transbaikal Standard Time', '001'),
    'Asia/Choibalsan': ('Ulaanbaatar Standard Time', 'MN'),
    'Asia/Colombo': ('Sri Lanka Standard Time', '001'),
    'Asia/Damascus': ('Syria Standard Time', '001'),
    'Asia/Dhaka': ('Bangladesh Standard Time', '001'),
    'Asia/Dili': ('Tokyo Standard Time', 'TL'),
    'Asia/Dubai': ('Arabian Standard Time', '001'),
    'Asia/Dushanbe': ('West Asia Standard Time', 'TJ'),
    'Asia/Famagusta': ('GTB Standard Time', 'CY'),
    'Asia/Gaza': ('West Bank Standard Time', 'PS'),
    'Asia/Hebron': ('West Bank Standard Time', '001'),
    'Asia/Hong_Kong': ('China Standard Time', 'HK'),
    'Asia/Hovd': ('W. Mongolia Standard Time', '001'),
    'Asia/Irkutsk': ('North Asia East Standard Time', '001'),
    'Asia/Jakarta': ('SE Asia Standard Time', 'ID'),
    'Asia/Jayapura': ('Tokyo Standard Time', 'ID'),
    'Asia/Jerusalem': ('Israel Standard Time', '001'),
    'Asia/Kabul': ('Afghanistan Standard Time', '001'),
    'Asia/Kamchatka': ('Russia Time Zone 11', '001'),
    'Asia/Karachi': ('Pakistan Standard Time', '001'),
    'Asia/Katmandu': ('Nepal Standard Time', '001'),
    'Asia/Khandyga': ('Yakutsk Standard Time', 'RU'),
    'Asia/Krasnoyarsk': ('North Asia Standard Time', '001'),
    'Asia/Kuala_Lumpur': ('Singapore Standard Time', 'MY'),
    'Asia/Kuching': ('Singapore Standard Time', 'MY'),
    'Asia/Kuwait': ('Arab Standard Time', 'KW'),
    'Asia/Macau': ('China Standard Time', 'MO'),
    'Asia/Magadan': ('Magadan Standard Time', '001'),
    'Asia/Makassar': ('Singapore Standard Time', 'ID'),
    'Asia/Manila': ('Singapore Standard Time', 'PH'),
    'Asia/Muscat': ('Arabian Standard Time', 'OM'),
    'Asia/Nicosia': ('GTB Standard Time', 'CY'),
    'Asia/Novokuznetsk': ('North Asia Standard Time', 'RU'),
    'Asia/Novosibirsk': ('N. Central Asia Standard Time', '001'),
    'Asia/Omsk': ('Omsk Standard Time', '001'),
    'Asia/Oral': ('West Asia Standard Time', 'KZ'),
    'Asia/Phnom_Penh': ('SE Asia Standard Time', 'KH'),
    'Asia/Pontianak': ('SE Asia Standard Time', 'ID'),
    'Asia/Pyongyang': ('North Korea Standard Time', '001'),
    'Asia/Qatar': ('Arab Standard Time', 'QA'),
    'Asia/Qostanay': ('Central Asia Standard Time', 'KZ'),
    'Asia/Qyzylorda': ('Qyzylorda Standard Time', '001'),
    'Asia/Rangoon': ('Myanmar Standard Time', '001'),
    'Asia/Riyadh': ('Arab Standard Time', '001'),
    'Asia/Saigon': ('SE Asia Standard Time', 'VN'),
    'Asia/Sakhalin': ('Sakhalin Standard Time', '001'),
    'Asia/Samarkand': ('West Asia Standard Time', 'UZ'),
    'Asia/Seoul': ('Korea Standard Time', '001'),
    'Asia/Shanghai': ('China Standard Time', '001'),
    'Asia/Singapore': ('Singapore Standard Time', '001'),
    'Asia/Srednekolymsk': ('Russia Time Zone 10', '001'),
    'Asia/Taipei': ('Taipei Standard Time', '001'),
    'Asia/Tashkent': ('West Asia Standard Time', '001'),
    'Asia/Tbilisi': ('Georgian Standard Time', '001'),
    'Asia/Tehran': ('Iran Standard Time', '001'),
    'Asia/Thimphu': ('Bangladesh Standard Time', 'BT'),
    'Asia/Tokyo': ('Tokyo Standard Time', '001'),
    'Asia/Tomsk': ('Tomsk Standard Time', '001'),
    'Asia/Ulaanbaatar': ('Ulaanbaatar Standard Time', '001'),
    'Asia/Urumqi': ('Central Asia Standard Time', 'CN'),
    'Asia/Ust-Nera': ('Vladivostok Standard Time', 'RU'),
    'Asia/Vientiane': ('SE Asia Standard Time', 'LA'),
    'Asia/Vladivostok': ('Vladivostok Standard Time', '001'),
    'Asia/Yakutsk': ('Yakutsk Standard Time', '001'),
    'Asia/Yekaterinburg': ('Ekaterinburg Standard Time', '001'),
    'Asia/Yerevan': ('Caucasus Standard Time', '001'),
    'Atlantic/Azores': ('Azores Standard Time', '001'),
    'Atlantic/Bermuda': ('Atlantic Standard Time', 'BM'),
    'Atlantic/Canary': ('GMT Standard Time', 'ES'),
    'Atlantic/Cape_Verde': ('Cape Verde Standard Time', '001'),
    'Atlantic/Faeroe': ('GMT Standard Time', 'FO'),
    'Atlantic/Madeira': ('GMT Standard Time', 'PT'),
    'Atlantic/Reykjavik': ('Greenwich Standard Time', '001'),
    'Atlantic/South_Georgia': ('UTC-02', 'GS'),
    'Atlantic/St_Helena': ('Greenwich Standard Time', 'SH'),
    'Atlantic/Stanley': ('SA Eastern Standard Time', 'FK'),
    'Australia/Adelaide': ('Cen. Australia Standard Time', '001'),
    'Australia/Brisbane': ('E. Australia Standard Time', '001'),
    'Australia/Broken_Hill': ('Cen. Australia Standard Time', 'AU'),
    'Australia/Currie': ('Tasmania Standard Time', 'AU'),
    'Australia/Darwin': ('AUS Central Standard Time', '001'),
    'Australia/Eucla': ('Aus Central W. Standard Time', '001'),
    'Australia/Hobart': ('Tasmania Standard Time', '001'),
    'Australia/Lindeman': ('E. Australia Standard Time', 'AU'),
    'Australia/Lord_Howe': ('Lord Howe Standard Time', '001'),
    'Australia/Melbourne': ('AUS Eastern Standard Time', 'AU'),
    'Australia/Perth': ('W. Australia Standard Time', '001'),
    'Australia/Sydney': ('AUS Eastern Standard Time', '001'),
    'CST6CDT': ('Central Standard Time', 'ZZ'),
    'EST5EDT': ('Eastern Standard Time', 'ZZ'),
    'Etc/GMT': ('UTC', 'ZZ'),
    'Etc/GMT+1': ('Cape Verde Standard Time', 'ZZ'),
    'Etc/GMT+10': ('Hawaiian Standard Time', 'ZZ'),
    'Etc/GMT+11': ('UTC-11', '001'),
    'Etc/GMT+12': ('Dateline Standard Time', '001'),
    'Etc/GMT+2': ('UTC-02', '001'),
    'Etc/GMT+3': ('SA Eastern Standard Time', 'ZZ'),
    'Etc/GMT+4': ('SA Western Standard Time', 'ZZ'),
    'Etc/GMT+5': ('SA Pacific Standard Time', 'ZZ'),
    'Etc/GMT+6': ('Central America Standard Time', 'ZZ'),
    'Etc/GMT+7': ('US Mountain Standard Time', 'ZZ'),
    'Etc/GMT+8': ('UTC-08', '001'),
    'Etc/GMT+9': ('UTC-09', '001'),
    'Etc/GMT-1': ('W. Central Africa Standard Time', 'ZZ'),
    'Etc/GMT-10': ('West Pacific Standard Time', 'ZZ'),
    'Etc/GMT-11': ('Central Pacific Standard Time', 'ZZ'),
    'Etc/GMT-12': ('UTC+12', '001'),
    'Etc/GMT-13': ('UTC+13', '001'),
    'Etc/GMT-14': ('Line Islands Standard Time', 'ZZ'),
    'Etc/GMT-2': ('South Africa Standard Time', 'ZZ'),
    'Etc/GMT-3': ('E. Africa Standard Time', 'ZZ'),
    'Etc/GMT-4': ('Arabian Standard Time', 'ZZ'),
    'Etc/GMT-5': ('West Asia Standard Time', 'ZZ'),
    'Etc/GMT-6': ('Central Asia Standard Time', 'ZZ'),
    'Etc/GMT-7': ('SE Asia Standard Time', 'ZZ'),
    'Etc/GMT-8': ('Singapore Standard Time', 'ZZ'),
    'Etc/GMT-9': ('Tokyo Standard Time', 'ZZ'),
    'Etc/UTC': ('UTC', '001'),
    'Europe/Amsterdam': ('W. Europe Standard Time', 'NL'),
    'Europe/Andorra': ('W. Europe Standard Time', 'AD'),
    'Europe/Astrakhan': ('Astrakhan Standard Time', '001'),
    'Europe/Athens': ('GTB Standard Time', 'GR'),
    'Europe/Belgrade': ('Central Europe Standard Time', 'RS'),
    'Europe/Berlin': ('W. Europe Standard Time', '001'),
    'Europe/Bratislava': ('Central Europe Standard Time', 'SK'),
    'Europe/Brussels': ('Romance Standard Time', 'BE'),
    'Europe/Bucharest': ('GTB Standard Time', '001'),
    'Europe/Budapest': ('Central Europe Standard Time', '001'),
    'Europe/Busingen': ('W. Europe Standard Time', 'DE'),
    'Europe/Chisinau': ('E. Europe Standard Time', '001'),
    'Europe/Copenhagen': ('Romance Standard Time', 'DK'),
    'Europe/Dublin': ('GMT Standard Time', 'IE'),
    'Europe/Gibraltar': ('W. Europe Standard Time', 'GI'),
    'Europe/Guernsey': ('GMT Standard Time', 'GG'),
    'Europe/Helsinki': ('FLE Standard Time', 'FI'),
    'Europe/Isle_of_Man': ('GMT Standard Time', 'IM'),
    'Europe/Istanbul': ('Turkey Standard Time', '001'),
    'Europe/Jersey': ('GMT Standard Time', 'JE'),
    'Europe/Kaliningrad': ('Kaliningrad Standard Time', '001'),
    'Europe/Kiev': ('FLE Standard Time', '001'),
    'Europe/Kirov': ('Russian Standard Time', 'RU'),
    'Europe/Lisbon': ('GMT Standard Time', 'PT'),
    'Europe/Ljubljana': ('Central Europe Standard Time', 'SI'),
    'Europe/London': ('GMT Standard Time', '001'),
    'Europe/Luxembourg': ('W. Europe Standard Time', 'LU'),
    'Europe/Madrid': ('Romance Standard Time', 'ES'),
    'Europe/Malta': ('W. Europe Standard Time', 'MT'),
    'Europe/Mariehamn': ('FLE Standard Time', 'AX'),
    'Europe/Minsk': ('Belarus Standard Time', '001'),
    'Europe/Monaco': ('W. Europe Standard Time', 'MC'),
    'Europe/Moscow': ('Russian Standard Time', '001'),
    'Europe/Oslo': ('W. Europe Standard Time', 'NO'),
    'Europe/Paris': ('Romance Standard Time', '001'),
    'Europe/Podgorica': ('Central Europe Standard Time', 'ME'),
    'Europe/Prague': ('Central Europe Standard Time', 'CZ'),
    'Europe/Riga': ('FLE Standard Time', 'LV'),
    'Europe/Rome': ('W. Europe Standard Time', 'IT'),
    'Europe/Samara': ('Russia Time Zone 3', '001'),
    'Europe/San_Marino': ('W. Europe Standard Time', 'SM'),
    'Europe/Sarajevo': ('Central European Standard Time', 'BA'),
    'Europe/Saratov': ('Saratov Standard Time', '001'),
    'Europe/Simferopol': ('Russian Standard Time', 'UA'),
    'Europe/Skopje': ('Central European Standard Time', 'MK'),
    'Europe/Sofia': ('FLE Standard Time', 'BG'),
    'Europe/Stockholm': ('W. Europe Standard Time', 'SE'),
    'Europe/Tallinn': ('FLE Standard Time', 'EE'),
    'Europe/Tirane': ('Central Europe Standard Time', 'AL'),
    'Europe/Ulyanovsk': ('Astrakhan Standard Time', 'RU'),
    'Europe/Uzhgorod': ('FLE Standard Time', 'UA'),
    'Europe/Vaduz': ('W. Europe Standard Time', 'LI'),
    'Europe/Vatican': ('W. Europe Standard Time', 'VA'),
    'Europe/Vienna': ('W. Europe Standard Time', 'AT'),
    'Europe/Vilnius': ('FLE Standard Time', 'LT'),
    'Europe/Volgograd': ('Volgograd Standard Time', '001'),
    'Europe/Warsaw': ('Central European Standard Time', '001'),
    'Europe/Zagreb': ('Central European Standard Time', 'HR'),
    'Europe/Zaporozhye': ('FLE Standard Time', 'UA'),
    'Europe/Zurich': ('W. Europe Standard Time', 'CH'),
    'Indian/Antananarivo': ('E. Africa Standard Time', 'MG'),
    'Indian/Chagos': ('Central Asia Standard Time', 'IO'),
    'Indian/Christmas': ('SE Asia Standard Time', 'CX'),
    'Indian/Cocos': ('Myanmar Standard Time', 'CC'),
    'Indian/Comoro': ('E. Africa Standard Time', 'KM'),
    'Indian/Kerguelen': ('West Asia Standard Time', 'TF'),
    'Indian/Mahe': ('Mauritius Standard Time', 'SC'),
    'Indian/Maldives': ('West Asia Standard Time', 'MV'),
    'Indian/Mauritius': ('Mauritius Standard Time', '001'),
    'Indian/Mayotte': ('E. Africa Standard Time', 'YT'),
    'Indian/Reunion': ('Mauritius Standard Time', 'RE'),
    'MST7MDT': ('Mountain Standard Time', 'ZZ'),
    'PST8PDT': ('Pacific Standard Time', 'ZZ'),
    'Pacific/Apia': ('Samoa Standard Time', '001'),
    'Pacific/Auckland': ('New Zealand Standard Time', '001'),
    'Pacific/Bougainville': ('Bougainville Standard Time', '001'),
    'Pacific/Chatham': ('Chatham Islands Standard Time', '001'),
    'Pacific/Easter': ('Easter Island Standard Time', '001'),
    'Pacific/Efate': ('Central Pacific Standard Time', 'VU'),
    'Pacific/Enderbury': ('UTC+13', 'KI'),
    'Pacific/Fakaofo': ('UTC+13', 'TK'),
    'Pacific/Fiji': ('Fiji Standard Time', '001'),
    'Pacific/Funafuti': ('UTC+12', 'TV'),
    'Pacific/Galapagos': ('Central America Standard Time', 'EC'),
    'Pacific/Gambier': ('UTC-09', 'PF'),
    'Pacific/Guadalcanal': ('Central Pacific Standard Time', '001'),
    'Pacific/Guam': ('West Pacific Standard Time', 'GU'),
    'Pacific/Honolulu': ('Hawaiian Standard Time', '001'),
    'Pacific/Johnston': ('Hawaiian Standard Time', 'UM'),
    'Pacific/Kiritimati': ('Line Islands Standard Time', '001'),
    'Pacific/Kosrae': ('Central Pacific Standard Time', 'FM'),
    'Pacific/Kwajalein': ('UTC+12', 'MH'),
    'Pacific/Majuro': ('UTC+12', 'MH'),
    'Pacific/Marquesas': ('Marquesas Standard Time', '001'),
    'Pacific/Midway': ('UTC-11', 'UM'),
    'Pacific/Nauru': ('UTC+12', 'NR'),
    'Pacific/Niue': ('UTC-11', 'NU'),
    'Pacific/Norfolk': ('Norfolk Standard Time', '001'),
    'Pacific/Noumea': ('Central Pacific Standard Time', 'NC'),
    'Pacific/Pago_Pago': ('UTC-11', 'AS'),
    'Pacific/Palau': ('Tokyo Standard Time', 'PW'),
    'Pacific/Pitcairn': ('UTC-08', 'PN'),
    'Pacific/Ponape': ('Central Pacific Standard Time', 'FM'),
    'Pacific/Port_Moresby': ('West Pacific Standard Time', '001'),
    'Pacific/Rarotonga': ('Hawaiian Standard Time', 'CK'),
    'Pacific/Saipan': ('West Pacific Standard Time', 'MP'),
    'Pacific/Tahiti': ('Hawaiian Standard Time', 'PF'),
    'Pacific/Tarawa': ('UTC+12', 'KI'),
    'Pacific/Tongatapu': ('Tonga Standard Time', '001'),
    'Pacific/Truk': ('West Pacific Standard Time', 'FM'),
    'Pacific/Wake': ('UTC+12', 'UM'),
    'Pacific/Wallis': ('UTC+12', 'WF'),
}

# Timezone names used by IANA but not mentioned in the CLDR. All of them have an alias in CLDR. This is essentially
# all timezone names that zoneinfo.available_timezones() contains but CLDR doesn't. Aliases were found
# at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
IANA_TO_MS_TIMEZONE_MAP = dict(
    CLDR_TO_MS_TIMEZONE_MAP,
    **{
        'Africa/Asmara': CLDR_TO_MS_TIMEZONE_MAP['Africa/Nairobi'],
        'Africa/Timbuktu': CLDR_TO_MS_TIMEZONE_MAP['Africa/Abidjan'],
        'America/Argentina/Buenos_Aires': CLDR_TO_MS_TIMEZONE_MAP['America/Buenos_Aires'],
        'America/Argentina/Catamarca': CLDR_TO_MS_TIMEZONE_MAP['America/Catamarca'],
        'America/Argentina/ComodRivadavia': CLDR_TO_MS_TIMEZONE_MAP['America/Catamarca'],
        'America/Argentina/Cordoba': CLDR_TO_MS_TIMEZONE_MAP['America/Cordoba'],
        'America/Argentina/Jujuy': CLDR_TO_MS_TIMEZONE_MAP['America/Jujuy'],
        'America/Argentina/Mendoza': CLDR_TO_MS_TIMEZONE_MAP['America/Mendoza'],
        'America/Atikokan': CLDR_TO_MS_TIMEZONE_MAP['America/Coral_Harbour'],
        'America/Atka': CLDR_TO_MS_TIMEZONE_MAP['America/Adak'],
        'America/Ensenada': CLDR_TO_MS_TIMEZONE_MAP['America/Tijuana'],
        'America/Fort_Wayne': CLDR_TO_MS_TIMEZONE_MAP['America/Indianapolis'],
        'America/Indiana/Indianapolis': CLDR_TO_MS_TIMEZONE_MAP['America/Indianapolis'],
        'America/Kentucky/Louisville': CLDR_TO_MS_TIMEZONE_MAP['America/Louisville'],
        'America/Knox_IN': CLDR_TO_MS_TIMEZONE_MAP['America/Indiana/Knox'],
        'America/Nuuk': CLDR_TO_MS_TIMEZONE_MAP['America/Godthab'],
        'America/Porto_Acre': CLDR_TO_MS_TIMEZONE_MAP['America/Rio_Branco'],
        'America/Rosario': CLDR_TO_MS_TIMEZONE_MAP['America/Cordoba'],
        'America/Shiprock': CLDR_TO_MS_TIMEZONE_MAP['America/Denver'],
        'America/Virgin': CLDR_TO_MS_TIMEZONE_MAP['America/Port_of_Spain'],
        'Antarctica/South_Pole': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Auckland'],
        'Antarctica/Troll': CLDR_TO_MS_TIMEZONE_MAP['Europe/Oslo'],
        'Asia/Ashkhabad': CLDR_TO_MS_TIMEZONE_MAP['Asia/Ashgabat'],
        'Asia/Chongqing': CLDR_TO_MS_TIMEZONE_MAP['Asia/Shanghai'],
        'Asia/Chungking': CLDR_TO_MS_TIMEZONE_MAP['Asia/Shanghai'],
        'Asia/Dacca': CLDR_TO_MS_TIMEZONE_MAP['Asia/Dhaka'],
        'Asia/Harbin': CLDR_TO_MS_TIMEZONE_MAP['Asia/Shanghai'],
        'Asia/Ho_Chi_Minh': CLDR_TO_MS_TIMEZONE_MAP['Asia/Saigon'],
        'Asia/Istanbul': CLDR_TO_MS_TIMEZONE_MAP['Europe/Istanbul'],
        'Asia/Kashgar': CLDR_TO_MS_TIMEZONE_MAP['Asia/Urumqi'],
        'Asia/Kathmandu': CLDR_TO_MS_TIMEZONE_MAP['Asia/Katmandu'],
        'Asia/Kolkata': CLDR_TO_MS_TIMEZONE_MAP['Asia/Calcutta'],
        'Asia/Macao': CLDR_TO_MS_TIMEZONE_MAP['Asia/Macau'],
        'Asia/Tel_Aviv': CLDR_TO_MS_TIMEZONE_MAP['Asia/Jerusalem'],
        'Asia/Thimbu': CLDR_TO_MS_TIMEZONE_MAP['Asia/Thimphu'],
        'Asia/Ujung_Pandang': CLDR_TO_MS_TIMEZONE_MAP['Asia/Makassar'],
        'Asia/Ulan_Bator': CLDR_TO_MS_TIMEZONE_MAP['Asia/Ulaanbaatar'],
        'Asia/Yangon': CLDR_TO_MS_TIMEZONE_MAP['Asia/Rangoon'],
        'Atlantic/Faroe': CLDR_TO_MS_TIMEZONE_MAP['Atlantic/Faeroe'],
        'Atlantic/Jan_Mayen': CLDR_TO_MS_TIMEZONE_MAP['Europe/Oslo'],
        'Australia/ACT': CLDR_TO_MS_TIMEZONE_MAP['Australia/Sydney'],
        'Australia/Canberra': CLDR_TO_MS_TIMEZONE_MAP['Australia/Sydney'],
        'Australia/LHI': CLDR_TO_MS_TIMEZONE_MAP['Australia/Lord_Howe'],
        'Australia/NSW': CLDR_TO_MS_TIMEZONE_MAP['Australia/Sydney'],
        'Australia/North': CLDR_TO_MS_TIMEZONE_MAP['Australia/Darwin'],
        'Australia/Queensland': CLDR_TO_MS_TIMEZONE_MAP['Australia/Brisbane'],
        'Australia/South': CLDR_TO_MS_TIMEZONE_MAP['Australia/Adelaide'],
        'Australia/Tasmania': CLDR_TO_MS_TIMEZONE_MAP['Australia/Hobart'],
        'Australia/Victoria': CLDR_TO_MS_TIMEZONE_MAP['Australia/Melbourne'],
        'Australia/West': CLDR_TO_MS_TIMEZONE_MAP['Australia/Perth'],
        'Australia/Yancowinna': CLDR_TO_MS_TIMEZONE_MAP['Australia/Broken_Hill'],
        'Brazil/Acre': CLDR_TO_MS_TIMEZONE_MAP['America/Rio_Branco'],
        'Brazil/DeNoronha': CLDR_TO_MS_TIMEZONE_MAP['America/Noronha'],
        'Brazil/East': CLDR_TO_MS_TIMEZONE_MAP['America/Sao_Paulo'],
        'Brazil/West': CLDR_TO_MS_TIMEZONE_MAP['America/Manaus'],
        'CET': CLDR_TO_MS_TIMEZONE_MAP['Europe/Paris'],
        'Canada/Atlantic': CLDR_TO_MS_TIMEZONE_MAP['America/Halifax'],
        'Canada/Central': CLDR_TO_MS_TIMEZONE_MAP['America/Winnipeg'],
        'Canada/Eastern': CLDR_TO_MS_TIMEZONE_MAP['America/Toronto'],
        'Canada/Mountain': CLDR_TO_MS_TIMEZONE_MAP['America/Edmonton'],
        'Canada/Newfoundland': CLDR_TO_MS_TIMEZONE_MAP['America/St_Johns'],
        'Canada/Pacific': CLDR_TO_MS_TIMEZONE_MAP['America/Vancouver'],
        'Canada/Saskatchewan': CLDR_TO_MS_TIMEZONE_MAP['America/Regina'],
        'Canada/Yukon': CLDR_TO_MS_TIMEZONE_MAP['America/Whitehorse'],
        'Chile/Continental': CLDR_TO_MS_TIMEZONE_MAP['America/Santiago'],
        'Chile/EasterIsland': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Easter'],
        'Cuba': CLDR_TO_MS_TIMEZONE_MAP['America/Havana'],
        'EET': CLDR_TO_MS_TIMEZONE_MAP['Europe/Sofia'],
        'EST': CLDR_TO_MS_TIMEZONE_MAP['America/Cancun'],
        'Egypt': CLDR_TO_MS_TIMEZONE_MAP['Africa/Cairo'],
        'Eire': CLDR_TO_MS_TIMEZONE_MAP['Europe/Dublin'],
        'Etc/GMT+0': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'],
        'Etc/GMT-0': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'],
        'Etc/GMT0': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'],
        'Etc/Greenwich': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'],
        'Etc/UCT': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'],
        'Etc/Universal': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'],
        'Etc/Zulu': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'],
        'Europe/Belfast': CLDR_TO_MS_TIMEZONE_MAP['Europe/London'],
        'Europe/Nicosia': CLDR_TO_MS_TIMEZONE_MAP['Asia/Nicosia'],
        'Europe/Tiraspol': CLDR_TO_MS_TIMEZONE_MAP['Europe/Chisinau'],
        'Factory': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'],
        'GB': CLDR_TO_MS_TIMEZONE_MAP['Europe/London'],
        'GB-Eire': CLDR_TO_MS_TIMEZONE_MAP['Europe/London'],
        'GMT': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'],
        'GMT+0': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'],
        'GMT-0': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'],
        'GMT0': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'],
        'Greenwich': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'],
        'HST': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Honolulu'],
        'Hongkong': CLDR_TO_MS_TIMEZONE_MAP['Asia/Hong_Kong'],
        'Iceland': CLDR_TO_MS_TIMEZONE_MAP['Atlantic/Reykjavik'],
        'Iran': CLDR_TO_MS_TIMEZONE_MAP['Asia/Tehran'],
        'Israel': CLDR_TO_MS_TIMEZONE_MAP['Asia/Jerusalem'],
        'Jamaica': CLDR_TO_MS_TIMEZONE_MAP['America/Jamaica'],
        'Japan': CLDR_TO_MS_TIMEZONE_MAP['Asia/Tokyo'],
        'Kwajalein': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Kwajalein'],
        'Libya': CLDR_TO_MS_TIMEZONE_MAP['Africa/Tripoli'],
        'MET': CLDR_TO_MS_TIMEZONE_MAP['Europe/Paris'],
        'MST': CLDR_TO_MS_TIMEZONE_MAP['America/Phoenix'],
        'Mexico/BajaNorte': CLDR_TO_MS_TIMEZONE_MAP['America/Tijuana'],
        'Mexico/BajaSur': CLDR_TO_MS_TIMEZONE_MAP['America/Mazatlan'],
        'Mexico/General': CLDR_TO_MS_TIMEZONE_MAP['America/Mexico_City'],
        'NZ': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Auckland'],
        'NZ-CHAT': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Chatham'],
        'Navajo': CLDR_TO_MS_TIMEZONE_MAP['America/Denver'],
        'PRC': CLDR_TO_MS_TIMEZONE_MAP['Asia/Shanghai'],
        'Pacific/Chuuk': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Truk'],
        'Pacific/Kanton': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Enderbury'],
        'Pacific/Pohnpei': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Ponape'],
        'Pacific/Samoa': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Pago_Pago'],
        'Pacific/Yap': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Truk'],
        'Poland': CLDR_TO_MS_TIMEZONE_MAP['Europe/Warsaw'],
        'Portugal': CLDR_TO_MS_TIMEZONE_MAP['Europe/Lisbon'],
        'ROC': CLDR_TO_MS_TIMEZONE_MAP['Asia/Taipei'],
        'ROK': CLDR_TO_MS_TIMEZONE_MAP['Asia/Seoul'],
        'Singapore': CLDR_TO_MS_TIMEZONE_MAP['Asia/Singapore'],
        'Turkey': CLDR_TO_MS_TIMEZONE_MAP['Europe/Istanbul'],
        'UCT': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'],
        'US/Alaska': CLDR_TO_MS_TIMEZONE_MAP['America/Anchorage'],
        'US/Aleutian': CLDR_TO_MS_TIMEZONE_MAP['America/Adak'],
        'US/Arizona': CLDR_TO_MS_TIMEZONE_MAP['America/Phoenix'],
        'US/Central': CLDR_TO_MS_TIMEZONE_MAP['America/Chicago'],
        'US/East-Indiana': CLDR_TO_MS_TIMEZONE_MAP['America/Indianapolis'],
        'US/Eastern': CLDR_TO_MS_TIMEZONE_MAP['America/New_York'],
        'US/Hawaii': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Honolulu'],
        'US/Indiana-Starke': CLDR_TO_MS_TIMEZONE_MAP['America/Indiana/Knox'],
        'US/Michigan': CLDR_TO_MS_TIMEZONE_MAP['America/Detroit'],
        'US/Mountain': CLDR_TO_MS_TIMEZONE_MAP['America/Denver'],
        'US/Pacific': CLDR_TO_MS_TIMEZONE_MAP['America/Los_Angeles'],
        'US/Samoa': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Pago_Pago'],
        'UTC': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'],
        'Universal': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'],
        'W-SU': CLDR_TO_MS_TIMEZONE_MAP['Europe/Moscow'],
        'WET': CLDR_TO_MS_TIMEZONE_MAP['Europe/Lisbon'],
        'Zulu': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'],
    }
)

# Reverse map from Microsoft timezone ID to IANA timezone name. Non-IANA timezone ID's can be added here.
MS_TIMEZONE_TO_IANA_MAP = dict(
    # Use the CLDR map because the IANA map contains deprecated aliases that not all systems support
    {v[0]: k for k, v in CLDR_TO_MS_TIMEZONE_MAP.items() if v[1] == DEFAULT_TERRITORY},
    **{
        'tzone://Microsoft/Utc': 'UTC',
    }
)

Functions

def generate_map(timeout=10)

Create a new CLDR_TO_MS_TIMEZONE_MAP map from the CLDR data. Used when the CLDR database is updated.

:param timeout: (Default value = 10) :return:

Expand source code
def generate_map(timeout=10):
    """Create a new CLDR_TO_MS_TIMEZONE_MAP map from the CLDR data. Used when the CLDR database is updated.

    :param timeout:  (Default value = 10)
    :return:
    """
    r = requests.get(CLDR_WINZONE_URL, timeout=timeout)
    if r.status_code != 200:
        raise ValueError('Unexpected response: %s' % r)
    tz_map = {}
    timezones_elem = to_xml(r.content).find('windowsZones').find('mapTimezones')
    type_version = timezones_elem.get('typeVersion')
    other_version = timezones_elem.get('otherVersion')
    for e in timezones_elem.findall('mapZone'):
        for location in re.split(r'\s+', e.get('type')):
            if e.get('territory') == DEFAULT_TERRITORY or location not in tz_map:
                # Prefer default territory. This is so MS_TIMEZONE_TO_IANA_MAP maps from MS timezone ID back to the
                # "preferred" region/location timezone name.
                if not location:
                    raise ValueError('Expected location')
                tz_map[location] = e.get('other'), e.get('territory')
    return type_version, other_version, tz_map
exchangelib-4.6.1/docs/index.md000066400000000000000000002030451414601472700163660ustar00rootroot00000000000000--- layout: default title: exchangelib --- Exchange Web Services client library ==================================== This module is an ORM for your Exchange mailbox, providing Django-style access to all your data. It is a platform-independent, well-performing, well-behaving, well-documented, well-tested and simple interface for communicating with an on-premise Microsoft Exchange 2007-2016 server or Office365 using Exchange Web Services (EWS). Among other things, it implements autodiscover, and functions for searching, creating, updating, deleting, exporting and uploading calendar, mailbox, task, contact and distribution list items. Apart from this documentation, we also provide online [source code documentation](https://ecederstrand.github.io/exchangelib/exchangelib/). ## Table of Contents * [Installation](#installation) * [Setup and connecting](#setup-and-connecting) * [Optimizing connections](#optimizing-connections) * [Fault tolerance](#fault-tolerance) * [Kerberos and SSPI authentication](#kerberos-and-sspi-authentication) * [Certificate Based Authentication (CBA)](#certificate-based-authentication-cba) * [OAuth authentication](#oauth-authentication) * [Caching autodiscover results](#caching-autodiscover-results) * [Proxies and custom TLS validation](#proxies-and-custom-tls-validation) * [User-Agent](#user-agent) * [Folders](#folders) * [Dates, datetimes and timezones](#dates-datetimes-and-timezones) * [Creating, updating, deleting, sending, moving, archiving, marking as junk](#creating-updating-deleting-sending-moving-archiving-marking-as-junk) * [Bulk operations](#bulk-operations) * [Searching](#searching) * [Paging](#paging) * [Meetings](#meetings) * [Contacts](#contacts) * [Extended properties](#extended-properties) * [Attachments](#attachments) * [Recurring calendar items](#recurring-calendar-items) * [Message timestamp fields](#message-timestamp-fields) * [Out of Facility (OOF)](#out-of-facility-oof) * [Mail tips](#mail-tips) * [Delegate information](#delegate-information) * [Export and upload](#export-and-upload) * [Synchronization, subscriptions and notifications](#synchronization-subscriptions-and-notifications) * [Non-account services](#non-account-services) * [Troubleshooting](#troubleshooting) * [Tests](#tests) ## Installation You can install this package from PyPI: ```bash pip install exchangelib ``` The default installation does not support Kerberos or SSPI. For additional Kerberos or SSPI support, install with the extra `kerberos` or `sspi` dependencies (please note that SSPI is only supported on Windows): ```bash pip install exchangelib[kerberos] pip install exchangelib[sspi] ``` To get both, install as: ```bash pip install exchangelib[complete] ``` To install the very latest code, install directly from GitHub instead: ```bash pip install git+https://github.com/ecederstrand/exchangelib.git ``` `exchangelib` uses the `lxml` package, and `pykerberos` to support Kerberos authentication. To be able to install these, you may need to install some additional operating system packages. On Ubuntu: ```bash apt-get install libxml2-dev libxslt1-dev # For Kerberos support, also install these: apt-get install libkrb5-dev build-essential libssl-dev libffi-dev python-dev ``` On CentOS: ```bash # For Kerberos support, install these: yum install gcc python-devel krb5-devel krb5-workstation python-devel ``` On FreeBSD, `pip` needs a little help: ```bash pkg install libxml2 libxslt CFLAGS=-I/usr/local/include pip install lxml # For Kerberos support, also install these: pkg install krb5 CFLAGS=-I/usr/local/include pip install kerberos pykerberos ``` For other operating systems, please consult the documentation for the Python package that fails to install. ## Setup and connecting First, specify your credentials. Username is usually in `WINDOMAIN\username` format, where `WINDOMAIN is` the name of the Windows Domain your username is connected to, but some servers also accept usernames in PrimarySMTPAddress ('myusername@example.com') format (Office365 requires it). UPN format is also supported, if your server expects that. ```python from exchangelib import Credentials credentials = Credentials(username='MYWINDOMAIN\\myuser', password='topsecret') ``` If you're running long-running jobs, you may want to enable fault-tolerance. Fault-tolerance means that requests to the server do an exponential backoff and sleep for up to a certain threshold before giving up, if the server is unavailable or responding with error messages. This prevents automated scripts from overwhelming a failing or overloaded server, and hides intermittent service outages that often happen in large Exchange installations. An Account is the account on the Exchange server that you want to connect to. This can be the account associated with the credentials you connect with, or any other account on the server that you have been granted access to. If, for example, you want to access a shared folder, create an Account instance using the email address of the account that the shared folder belongs to, and access the shared folder through this account. `primary_smtp_address` is the primary SMTP address assigned the account. If you enable autodiscover, an alias address will work, too. In this case, `Account.primary_smtp_address` will be set to the primary SMTP address. ```python from exchangelib import DELEGATE, IMPERSONATION, Account my_account = Account( primary_smtp_address='myusername@example.com', credentials=credentials, autodiscover=True, access_type=DELEGATE ) johns_account = Account( primary_smtp_address='john@example.com', credentials=credentials, autodiscover=True, access_type=DELEGATE ) marys_account = Account( primary_smtp_address='mary@example.com', credentials=credentials, autodiscover=True, access_type=DELEGATE ) still_marys_account = Account( primary_smtp_address='alias_for_mary@example.com', credentials=credentials, autodiscover=True, access_type=DELEGATE ) # Full autodiscover data is available on the Account object: my_account.ad_response # Set up a target account and do an autodiscover lookup to find the EWS endpoint account = Account( primary_smtp_address='john@example.com', credentials=credentials, autodiscover=True, access_type=DELEGATE ) # If your credentials have been given impersonation access to the target # account, set a different 'access_type': johns_account = Account( primary_smtp_address='john@example.com', credentials=credentials, autodiscover=True, access_type=IMPERSONATION ) ``` If you want to impersonate an account and access a shared folder that this account has access to, you need to specify the email adress of the shared folder to access the folder: ```python from exchangelib.folders import Calendar, SingleFolderQuerySet from exchangelib.properties import DistinguishedFolderId, Mailbox shared_calendar = SingleFolderQuerySet(account=johns_account, folder=DistinguishedFolderId( id=Calendar.DISTINGUISHED_FOLDER_ID, mailbox=Mailbox(email_address='mary@example.com') )).resolve() ``` Autodiscover needs to make some DNS queries. We use the dnspython package for that. Here's an example of customizing the way the dns.resolver.Resolver object is created: ```python from exchangelib.autodiscover import Autodiscovery Autodiscovery.DNS_RESOLVER_ATTRS['edns'] = False # Disable EDNS queries ``` ### Optimizing connections According to MSDN docs, you can avoid a per-request AD lookup if you specify the UPN or SID of the account when you are using impersonation. To do this, set one of these values. EWS cannot provide you with these values - you have to fetch them by some other means, e.g. via AD lookup: ```python account = Account(...) account.identity.sid = 'S-my-sid' account.identity.upn = 'john@subdomain.example.com' ``` If the server doesn't support autodiscover, or you want to avoid the overhead of autodiscover, use a Configuration object to set the hostname instead: ```python from exchangelib import Configuration, Credentials credentials = Credentials(...) config = Configuration(server='mail.example.com', credentials=credentials) ``` For accounts that are known to be hosted on Office365, there's no need to use autodiscover. Here's the server to use for Office365: ```python config = Configuration(server='outlook.office365.com', credentials=credentials) account = Account(primary_smtp_address='john@example.com', config=config, autodiscover=False, access_type=DELEGATE) ``` exchangelib will attempt to guess the server version and authentication method. If you have a really bizarre or locked-down installation and the guessing fails, or you want to avoid the extra network traffic, you can set the auth method and version explicitly instead: ```python from exchangelib import Build, NTLM version = Version(build=Build(15, 0, 12, 34)) config = Configuration( server='example.com', credentials=credentials, version=version, auth_type=NTLM ) ``` By default, 'exchangelib' will only create 1 connection to the server. If you are using threads to send multiple requests concurrently, you may want to increase this limit. The Exchange server may have rate-limiting policies in place for the connecting credentials, so make sure to agree with your Exchange admins before increasing this value. ```python config = Configuration(server='mail.example.com', max_connections=10) ``` ### Fault tolerance By default, we fail on all exceptions from the server. If you want to enable fault tolerance, add a retry policy to your configuration. We will then retry on certain transient errors. By default, we back off exponentially and retry for up to an hour. This is configurable: ```python from exchangelib import Account, FaultTolerance, Configuration, Credentials credentials = Credentials(...) config = Configuration( retry_policy=FaultTolerance(max_wait=3600), credentials=credentials ) account = Account(primary_smtp_address='john@example.com', config=config) ``` Autodiscovery will also use this policy, but only for the final autodiscover endpoint. Here's how to change the policy for connecting to autodiscover candidate servers. ```python from exchangelib.autodiscover import Autodiscovery Autodiscovery.INITIAL_RETRY_POLICY = FaultTolerance(max_wait=30) ``` ### Kerberos and SSPI authentication Kerberos and SSPI authentication are supported via the GSSAPI and SSPI auth types. ```python from exchangelib import Configuration, GSSAPI, SSPI config = Configuration(auth_type=GSSAPI) config = Configuration(auth_type=SSPI) ``` ### Certificate Based Authentication (CBA) ```python from exchangelib import Configuration, BaseProtocol, CBA, TLSClientAuth TLSClientAuth.cert_file = '/path/to/client.pem' BaseProtocol.HTTP_ADAPTER_CLS = TLSClientAuth config = Configuration(auth_type=CBA) ``` ### OAuth authentication OAuth is supported via the OAUTH2 auth type and the OAuth2Credentials class. Use OAuth2AuthorizationCodeCredentials for the authorization code flow (useful for applications that access multiple accounts). ```python from exchangelib import OAuth2Credentials credentials = OAuth2Credentials( client_id='MY_ID', client_secret='MY_SECRET', tenant_id='TENANT_ID' ) ``` The OAuth2 flow may need to have impersonation headers set. If you get impersonation errors, add information about the account that the OAuth2 credentials was created for: ```python from exchangelib import Configuration, OAuth2Credentials, \ OAuth2AuthorizationCodeCredentials, Identity, OAUTH2 from oauthlib.oauth2 import OAuth2Token credentials = OAuth2Credentials( ..., identity=Identity(primary_smtp_address='svc_acct@example.com') ) credentials = OAuth2AuthorizationCodeCredentials( ..., identity=Identity(upn='svc_acct@subdomain.example.com') ) credentials = OAuth2AuthorizationCodeCredentials( client_id='MY_ID', client_secret='MY_SECRET', authorization_code='AUTH_CODE' ) credentials = OAuth2AuthorizationCodeCredentials( client_id='MY_ID', client_secret='MY_SECRET', access_token=OAuth2Token(access_token='EXISTING_TOKEN') ) config = Configuration(credentials=credentials, auth_type=OAUTH2) ``` Applications using the authorization code flow that let exchangelib refresh access tokens for them probably want to store the refreshed tokens so users don't have to re-authorize. Subclass OAuth2AuthorizationCodeCredentials and override on_token_auto_refreshed(): ```python class MyCredentials(OAuth2AuthorizationCodeCredentials): def on_token_auto_refreshed(self, access_token): store_it_somewhere(access_token) # Let the object update its internal state! super().on_token_auto_refreshed(access_token) ``` For applications that use the authorization code flow and rely on an external provider to refresh access tokens (and thus are unable to provide a client ID and secret to exchangelib), subclass OAuth2AuthorizationCodeCredentials and override refresh(). ```python class MyCredentials(OAuth2AuthorizationCodeCredentials): def refresh(self): self.access_token = ... ``` ### Caching autodiscover results If you're connecting to the same account very often, you can cache the autodiscover result for later so you can skip the autodiscover lookup: ```python from exchangelib import Configuration, Credentials, Account, DELEGATE account = Account(...) ews_url = account.protocol.service_endpoint ews_auth_type = account.protocol.auth_type primary_smtp_address = account.primary_smtp_address # This one is optional. It is used as a hint to the initial connection and # avoids one or more roundtrips to guess the correct Exchange server version. version = account.version # You can now create the Account without autodiscovery, using the cached values: credentials = Credentials(...) config = Configuration( service_endpoint=ews_url, credentials=credentials, auth_type=ews_auth_type, version=version ) account = Account( primary_smtp_address=primary_smtp_address, config=config, autodiscover=False, access_type=DELEGATE, ) ``` Autodiscover can take a lot of time, specially the part that figures out the autodiscover server to contact for a specific email domain. For this reason, we will create a persistent, per-user, on-disk cache containing a map of previous, successful domain -> autodiscover server lookups. This cache is shared between processes and is not deleted when your program exits. A cache entry for a domain is removed automatically if autodiscovery fails for an email in that domain. It's possible to clear the entire cache completely if you want: ```python from exchangelib.autodiscover import clear_cache clear_cache() ``` ### Proxies and custom TLS validation If you need proxy support or custom TLS validation, you can supply a custom 'requests' transport adapter class, as described in [http://docs.python-requests.org/en/master/user/advanced/#transport-adapters](http://docs.python-requests.org/en/master/user/advanced/#transport-adapters). Here's an example using different custom root certificates depending on the server to connect to: ```python from urllib.parse import urlparse import requests.adapters from exchangelib.protocol import BaseProtocol class RootCAAdapter(requests.adapters.HTTPAdapter): """An HTTP adapter that uses a custom root CA certificate at a hard coded location. """ def cert_verify(self, conn, url, verify, cert): cert_file = { 'example.com': '/path/to/example.com.crt', 'mail.internal': '/path/to/mail.internal.crt', }[urlparse(url).hostname] super().cert_verify(conn=conn, url=url, verify=cert_file, cert=cert) # Tell exchangelib to use this adapter class instead of the default BaseProtocol.HTTP_ADAPTER_CLS = RootCAAdapter ``` Here's an example of adding proxy support: ```python import requests.adapters from exchangelib.protocol import BaseProtocol class ProxyAdapter(requests.adapters.HTTPAdapter): def send(self, *args, **kwargs): kwargs['proxies'] = { 'http': 'http://10.0.0.1:1243', 'https': 'http://10.0.0.1:4321', } return super().send(*args, **kwargs) # Tell exchangelib to use this adapter class instead of the default BaseProtocol.HTTP_ADAPTER_CLS = ProxyAdapter ``` `exchangelib` provides a sample adapter which ignores TLS validation errors. Use at own risk. ```python from exchangelib.protocol import BaseProtocol, NoVerifyHTTPAdapter # Tell exchangelib to use this adapter class instead of the default BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter ``` ### User-Agent You can supply a custom 'User-Agent' for your application. By default, `exchangelib` will use: `exchangelib/ (python-requests/)` Here's an example using different User-Agent: ```python from exchangelib.protocol import BaseProtocol # Tell exchangelib to use this user-agent instead of the default BaseProtocol.USERAGENT = "Auto-Reply/0.1.0" ``` ## Folders All wellknown folders are available as properties on the account, e.g. as `account.root`, `account.calendar`, `account.trash`, `account.inbox`, `account.outbox`, `account.sent`, `account.junk`, `account.tasks` and `account.contacts`. There are multiple ways of navigating the folder tree and searching for folders. Globbing and absolute path may create unexpected results if your folder names contain slashes. The folder structure is cached after first access to a folder hierarchy. This means that external changes to the folder structure will not show up until you clear the cache. Here's how to clear the cache of each of the currently supported folder hierarchies: ```python from exchangelib import Account a = Account(...) a.root.refresh() a.public_folders_root.refresh() a.archive_root.refresh() some_folder = a.root / 'Some Folder' some_folder.parent some_folder.parent.parent.parent # Returns the root of the folder structure, at any level. Same as Account.root some_folder.root some_folder.children # A generator of child folders some_folder.absolute # Returns the absolute path, as a string # A generator returning all subfolders at arbitrary depth this level some_folder.walk() # Globbing uses the normal UNIX globbing syntax, but case-insensitive some_folder.glob('foo*') # Return child folders matching the pattern some_folder.glob('*/foo') # Return subfolders named 'foo' in any child folder some_folder.glob('**/foo') # Return subfolders named 'foo' at any depth some_folder / 'sub_folder' / 'even_deeper' / 'leaf' # Works like pathlib.Path ``` You can also drill down into the folder structure without using the cache. This works like the single slash syntax, but does not start by creating a cache the folder hierarchy. This is useful if your account contains a huge number of folders, and you already know where to go. ```python some_folder // 'sub_folder' // 'even_deeper' // 'leaf' some_folder.parts # returns some_folder and all parents, as Folder instances some_folder.absolute # Returns the full path as a string ``` tree() returns a string representation of the tree structure at a given level ```python print(a.root.tree()) ''' root ├── inbox │ └── todos └── archive ├── Last Job ├── exchangelib issues └── Mom ''' ``` Folders have some useful counters: ```python a.inbox.total_count a.inbox.child_folder_count a.inbox.unread_count # Update the counters a.inbox.refresh() ``` Folders can be created, updated and deleted: ```python from exchangelib import Folder f = Folder(parent=a.inbox, name='My New Folder') f.save() f.name = 'My New Subfolder' f.save() f.delete() # Delete all items in a folder f.empty() # Also delete all subfolders in the folder f.empty(delete_sub_folders=True) # Recursively delete all items in a folder, and all subfolders and their # content. This is like `empty(delete_sub_folders=True)` but attempts to protect # distinguished folders from being deleted. Use with caution! f.wipe() ``` Folders support getting, creating, updating and deleting Master Category Lists, also known as User Configuration objects. Supported key and value types for the 'dictionary' attribute are: bool, int, bytes, str, tuples of str, datetime, EWSDateTime, and the 'Byte' type which we emulate in Python as a 1-length bytes. ```python f.create_user_configuration( name='SomeName', dictionary={'foo': 'bar', 123: 'a', 'b': False}, xml_data=b'bar', binary_data=b'XXX', ) config = f.get_user_configuration(name='SomeName') config.dictionary # {'foo': 'bar', 123: 'a', 'b': False} config.xml_data # b'bar' config.binary_data # b'XXX' f.update_user_configuration( name='SomeName', dictionary={'bar': 'foo', 456: 'a', 'b': True}, xml_data=b'baz', binary_data=b'YYY', ) f.delete_user_configuration(name='SomeName') ``` ## Dates, datetimes and timezones EWS has some special requirements on datetimes and timezones. You may use regular `datetime.*` and `zoneinfo` objects as input, but all methods return date values as the special `EWSDate`, `EWSDateTime` and `EWSTimeZone` classes. Thes classes are all subclasses of `datetime.*` or `zoneinfo.ZoneInfo` so you should be able to use them as regular date objects. ```python from datetime import datetime, timedelta import dateutil.tz import pytz try: import zoneinfo except ImportError: from backports import zoneinfo from exchangelib import EWSTimeZone, EWSDateTime, EWSDate, UTC, UTC_NOW # EWSTimeZone works just like zoneinfo.ZoneInfo() tz = EWSTimeZone('Europe/Copenhagen') # You can also get the local timezone defined in your operating system tz = EWSTimeZone.localzone() # EWSDate and EWSDateTime work just like datetime.datetime and datetime.date. # Always create timezone-aware datetimes: localized_dt = EWSDateTime(2017, 9, 5, 8, 30, tzinfo=tz) right_now = EWSDateTime.now(tz) # Datetime math works transparently two_hours_later = localized_dt + timedelta(hours=2) two_hours = two_hours_later - localized_dt two_hours_later += timedelta(hours=2) # Dates my_date = EWSDate(2017, 9, 5) today = EWSDate.today() also_today = right_now.date() also_today += timedelta(days=10) # UTC helpers. 'UTC' is the UTC timezone as an EWSTimeZone instance. # 'UTC_NOW' returns a timezone-aware UTC timestamp of current time. right_now_in_utc = EWSDateTime.now(tz=UTC) right_now_in_utc = UTC_NOW() # 'pytz', 'dateutil' and `zoneinfo` timezones can be converted to EWSTimeZone pytz_tz = pytz.timezone('Europe/Copenhagen') tz = EWSTimeZone.from_timezone(pytz_tz) dateutil_tz = dateutil.tz.gettz('Europe/Copenhagen') tz = EWSTimeZone.from_timezone(dateutil_tz) zoneinfo_tz = zoneinfo.ZoneInfo('Europe/Copenhagen') tz = EWSTimeZone.from_timezone(zoneinfo_tz) # Python datetime objects can be converted using from_datetime(). Make sure # values are timezone-aware and tzinfo is an EWSTimeZone or ZoneInfo instance. py_dt = datetime(2017, 12, 11, 10, 9, 8, tzinfo=tz) ews_now = EWSDateTime.from_datetime(py_dt) ``` ## Creating, updating, deleting, sending, moving, archiving, marking as junk Here's an example of creating a calendar item in the user's standard calendar. ```python from exchangelib import Account, CalendarItem from exchangelib.items import SEND_ONLY_TO_ALL, SEND_ONLY_TO_CHANGED from exchangelib.properties import DistinguishedFolderId a = Account(...) item = CalendarItem(folder=a.calendar, subject='foo') item.save() # This gives the item an 'id' and a 'changekey' value # Send a meeting invitation to attendees item.save(send_meeting_invitations=SEND_ONLY_TO_ALL) # Update a field. All fields have a corresponding Python type that must be used. item.subject = 'bar' item.save() # When the items has an item_id, this will update the item # Only updates certain fields. Accepts a list of field names. item.save(update_fields=['subject']) # Send invites only to attendee changes item.save(send_meeting_invitations=SEND_ONLY_TO_CHANGED) item.delete() # Hard deletion # Send cancellations to all attendees item.delete(send_meeting_cancellations=SEND_ONLY_TO_ALL) item.soft_delete() # Delete, but keep a copy in the recoverable items folder item.move_to_trash() # Move to the trash folder item.move(a.trash) # Also moves the item to the trash folder item.copy(a.trash) # Creates a copy of the item to the trash folder # Archives the item to inbox of the archive mailbox item.archive(DistinguishedFolderId('inbox')) # Block sender and move item to junk folder item.mark_as_junk(is_junk=True, move_item=True) ``` Here's how to print all available fields on the `CalendarItem` class. Beware that some fields are read-only, or read-only after the item has been saved or sent, and some fields are not supported on old versions of Exchange. ```python print(CalendarItem.FIELDS) ``` You can also send emails. If you don't want a local copy: ```python from exchangelib import Message, Mailbox m = Message( account=a, subject='Daily motivation', body='All bodies are beautiful', to_recipients=[ Mailbox(email_address='anne@example.com'), Mailbox(email_address='bob@example.com'), ], # Simple strings work, too cc_recipients=['carl@example.com', 'denice@example.com'], bcc_recipients=[ Mailbox(email_address='erik@example.com'), 'felicity@example.com', ], # Or a mix of both ) m.send() ``` Or, if you want a copy in e.g. the 'Sent' folder ```python m = Message( account=a, folder=a.sent, subject='Daily motivation', body='All bodies are beautiful', to_recipients=[Mailbox(email_address='anne@example.com')] ) m.send_and_save() ``` Likewise, you can reply to and forward messages that are stored in your mailbox (i.e. they have an item ID). ```python m = a.sent.get(subject='Daily motivation') m.reply( subject='Re: Daily motivation', body='I agree', to_recipients=['carl@example.com', 'denice@example.com'] ) m.reply_all(subject='Re: Daily motivation', body='I agree') m.forward( subject='Fwd: Daily motivation', body='Hey, look at this!', to_recipients=['carl@example.com', 'denice@example.com'] ) ``` You can also edit a draft of a reply or forward ```python from exchangelib import FileAttachment forward_draft = m.create_forward( subject='Fwd: Daily motivation', body='Hey, look at this!', to_recipients=['carl@example.com', 'denice@example.com'] ).save(a.drafts) # gives you back the item forward_draft.reply_to = ['erik@example.com'] forward_draft.attach(FileAttachment( name='my_file.txt', content='hello world'.encode('utf-8')) ) # Now our forward has an extra reply_to field and an extra attachment. forward_draft.send() ``` EWS distinguishes between plain text and HTML body contents. If you want to send HTML body content, use the HTMLBody helper. Clients will see this as HTML and display the body correctly: ```python from exchangelib import HTMLBody item.body = HTMLBody( 'Hello happy OWA user!' ) ``` ## Bulk operations Bulk methods are available to run operations on multiple items at a time. Here's an example of building a list of calendar items: ```python import datetime try: import zoneinfo except ImportError: from backports import zoneinfo from exchangelib import Account, CalendarItem, Attendee, Mailbox from exchangelib.properties import DistinguishedFolderId a = Account(...) tz = zoneinfo.ZoneInfo('Europe/Copenhagen') year, month, day = 2016, 3, 20 calendar_items = [] for hour in range(7, 17): calendar_items.append(CalendarItem( start=datetime.datetime(year, month, day, hour, 30, tzinfo=tz), end=datetime.datetime(year, month, day, hour + 1, 15, tzinfo=tz), subject='Test item', body='Hello from Python', location='devnull', categories=['foo', 'bar'], required_attendees = [Attendee( mailbox=Mailbox(email_address='user1@example.com'), response_type='Accept' )] )) # Create all items at once return_ids = a.bulk_create(folder=a.calendar, items=calendar_items) # Bulk fetch, when you have a list of item IDs and want the full objects. # Returns a generator. calendar_ids = [(i.id, i.changekey) for i in calendar_items] items_iter = a.fetch(ids=calendar_ids) # If you only want some fields, use the 'only_fields' attribute items_iter = a.fetch(ids=calendar_ids, only_fields=['start', 'subject']) # Bulk update items. Each item must be accompanied by a list of attributes to # update. updated_ids = a.bulk_update( items=[(i, ('start', 'subject')) for i in calendar_items] ) # Move many items to a new folder new_ids = a.bulk_move(ids=calendar_ids, to_folder=a.other_calendar) # Send draft messages in bulk message_ids = a.drafts.all().only('id', 'changekey') new_ids = a.bulk_send(ids=message_ids, save_copy=False) # Delete in bulk delete_results = a.bulk_delete(ids=calendar_ids) # Archive in bulk delete_results = a.bulk_archive( ids=calendar_ids, to_folder=DistinguishedFolderId('inbox') ) ``` Bulk operations also work on QuerySet objects. Here's how to bulk delete messages in the inbox: ```python a.inbox.filter(subject__startswith='Invoice').delete() # Likewise, you can bulk send, copy, move or archive items found in a QuerySet a.drafts.filter(subject__startswith='Invoice').send() # All kwargs are passed on to the equivalent bulk methods on the Account a.drafts.filter(subject__startswith='Invoice').send(save_copy=False) a.inbox.filter(subject__startswith='Invoice').copy(to_folder=a.trash) a.inbox.filter(subject__startswith='Invoice').move(to_folder=a.trash) a.inbox.filter(subject__startswith='Invoice').archive( to_folder=DistinguishedFolderId('inbox') ) a.inbox.filter(subject__startswith='Invoice').mark_as_junk( is_junk=True, move_item=True ) ``` You can change the default page size of bulk operations if you have a slow or busy server. ```python a.inbox.filter(subject__startswith='Invoice').delete(page_size=25) ``` ## Searching Searching is modeled after the Django QuerySet API, and a large part of the API is supported. Like in Django, the QuerySet is lazy and doesn't fetch anything before the QuerySet is iterated. QuerySets support chaining, so you can build the final query in multiple steps, and you can re-use a base QuerySet for multiple sub-searches. The QuerySet is an iterator. Here are some examples of using the API: ```python import datetime from exchangelib import Account, Message a = Account(...) # Not all fields on an item support searching. Here's the list of options for # Message items. print([f.name for f in Message.FIELDS if f.is_searchable]) all_items = a.inbox.all() # Get everything all_items_without_caching = a.inbox.all() # Chain multiple modifiers to refine the query filtered_items = a.inbox.filter(subject__contains='foo')\ .exclude(categories__icontains='bar') # Delete all items returned by the QuerySet status_report = a.inbox.all().delete() start = datetime.datetime(2017, 1, 1, tzinfo=a.default_timezone) end = datetime.datetime(2018, 1, 1, tzinfo=a.default_timezone) # Filter by a date range items_for_2017 = a.calendar.filter(start__range=(start, end)) ``` Same as filter() but throws an error if exactly one item isn't returned ```python item = a.inbox.get(subject='unique_string') # If you only have the ID and possibly the changekey of an item, you can get the # full item: a.inbox.get(id='AAMkADQy=') a.inbox.get(id='AAMkADQy=', changekey='FwAAABYA') ``` You can sort by a single or multiple fields. Prefix a field with '-' to reverse the sorting. Sorting is efficient since it is done server-side, except when a calendar view sorting on multiple fields. ```python ordered_items = a.inbox.all().order_by('subject') reverse_ordered_items = a.inbox.all().order_by('-subject') # Indexed properties can be ordered on their individual components sorted_by_home_street = a.contacts.all().order_by( 'physical_addresses__Home__street' ) # Beware that sorting is done client-side here a.calendar.view(start=start, end=end).order_by('subject', 'categories') ``` Counting and exists ```python n = a.inbox.all().count() # Efficient counting folder_is_empty = not a.inbox.all().exists() # Efficient tasting ``` Restricting returned attributes: ```python sparse_items = a.inbox.all().only('subject', 'start') # Dig deeper on indexed properties sparse_items = a.contacts.all().only('phone_numbers') sparse_items = a.contacts.all().only('phone_numbers__CarPhone') sparse_items = a.contacts.all().only('physical_addresses__Home__street') ``` Return values as dicts, nested or flat lists instead of objects: ```python ids_as_dict = a.inbox.all().values('id', 'changekey') values_as_list = a.inbox.all().values_list('subject', 'body') all_subjects = a.inbox.all().values_list( 'physical_addresses__Home__street', flat=True ) ``` A QuerySet can be indexed and sliced like a normal Python list. Slicing and indexing of the QuerySet is efficient because it only fetches the necessary items to perform the slicing. Slicing from the end is also efficient, but then you might as well reverse the sorting. ```python # Efficient. We only fetch 10 items first_ten = a.inbox.all().order_by('-subject')[:10] # Efficient, but convoluted last_ten = a.inbox.all().order_by('-subject')[:-10] # Efficient. We only fetch 10 items next_ten = a.inbox.all().order_by('-subject')[10:20] # Efficient. We only fetch 1 item single_item = a.inbox.all().order_by('-subject')[34298] # Efficient. We only fetch 10 items ten_items = a.inbox.all().order_by('-subject')[3420:3430] # This is just stupid, but works random_emails = a.inbox.all().order_by('-subject')[::3] ``` The syntax for filter() is modeled after Django QuerySet filters. The following filter lookup types are supported. Some lookups only work with string attributes. Range and less/greater operators only work for date or numerical attributes. This is determined by the field type. Some attributes are not searchable at all via EWS. This is determined by the "is_searchable" attribute on the field. ```python # List the field name and field type of searchable fields for a given item type for f in Message.FIELDS: if f.is_searchable: print(f.name, f) # No restrictions. Return all items. qs = a.calendar.all() # Returns items where subject is exactly 'foo'. Case-sensitive qs.filter(subject='foo') # Returns items within range qs.filter(start__range=(start, end)) # Return items where subject is either 'foo' or 'bar' qs.filter(subject__in=('foo', 'bar')) # Returns items where subject is not 'foo' qs.filter(subject__not='foo') # Returns items starting after 'dt' qs.filter(start__gt=start) # Returns items starting on or after 'dt' qs.filter(start__gte=start) # Returns items starting before 'dt' qs.filter(start__lt=start) # Returns items starting on or before 'dt' qs.filter(start__lte=start) # Same as filter(subject='foo') qs.filter(subject__exact='foo') # Returns items where subject is 'foo', 'FOO' or 'Foo' qs.filter(subject__iexact='foo') # Returns items where subject contains 'foo' qs.filter(subject__contains='foo') # Returns items where subject contains 'foo', 'FOO' or 'Foo' qs.filter(subject__icontains='foo') # Returns items where subject starts with 'foo' qs.filter(subject__startswith='foo') # Returns items where subject starts with 'foo', 'FOO' or 'Foo' qs.filter(subject__istartswith='foo') # Returns items that have at least one category assigned, i.e. the field exists # on the item on the server. qs.filter(categories__exists=True) # Returns items that have no categories set, i.e. the field does not exist on # the item on the server. qs.filter(categories__exists=False) ``` WARNING: Filtering on the 'body' field is not fully supported by EWS. There seems to be a window before some internal search index is populated where case-sensitive or case-insensitive filtering for substrings in the body element incorrectly returns an empty result, and sometimes the result stays empty. `filter()` also supports EWS QueryStrings. Just pass the string to filter(). QueryStrings cannot be combined with other filters. We make no attempt at validating the syntax of the QueryString - we just pass the string verbatim to EWS. Read more about the QueryString syntax here: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/querystring-querystringtype ```python a.inbox.filter('subject:XXX') ``` `filter()` also supports `Q` objects that are modeled after Django Q objects, for building complex boolean logic search expressions. ```python from exchangelib import Q q = ( Q(subject__iexact='foo') | Q(subject__contains='bar') ) & ~Q(subject__startswith='baz') a.inbox.filter(q) ``` In this example, we filter by categories so we only get items created by us. ```python a.calendar.filter( start__lt=datetime.datetime(2019, 1, 1, tzinfo=a.default_timezone), end__gt=datetime.datetime(2019, 1, 31, tzinfo=a.default_timezone), categories__contains=['foo', 'bar'], ) ``` By default, EWS returns only the master recurring item. If you want recurring calendar items to be expanded, use calendar.view(start=..., end=...) instead. ```python start = datetime.datetime(2019, 1, 31, tzinfo=a.default_timezone) items = a.calendar.view( start=start, end=start + datetime.timedelta(days=1), ) for item in items: print(item.start, item.end, item.subject, item.body, item.location) ``` You can combine view() with other modifiers. For example, to check for conflicts before adding a meeting from 8:00 to 10:00: ```python has_conflicts = a.calendar.view( start=datetime.datetime(2019, 1, 31, 8, tzinfo=a.default_timezone), end=datetime.datetime(2019, 1, 31, 10, tzinfo=a.default_timezone), max_items=1 ).exists() ``` The filtering syntax also works on collections of folders, so you can search multiple folders in a single request. ```python from exchangelib import FolderCollection a.inbox.children.filter(subject='foo') a.inbox.walk().filter(subject='foo') a.inbox.glob('foo*').filter(subject='foo') # Or select the folders individually FolderCollection(account=a, folders=[a.inbox, a.calendar]).filter(subject='foo') ``` ## Paging Paging EWS services, e.g. `FindItem` and `FindFolder`, have a default page size of 100. You can change this value globally if you want: ```python import exchangelib.services exchangelib.services.CHUNK_SIZE = 25 ``` If you are working with very small or very large items, this may not be a reasonable value. For example, if you want to retrieve and save emails with large attachments, you can change this value on a per-queryset basis: ```python from exchangelib import Account a = Account(...) qs = a.inbox.all().only('mime_content') qs.page_size = 5 for msg in qs: with open('%s.eml' % msg.item_id, 'w') as f: f.write(msg.mime_content) ``` Finally, the bulk methods defined on the `Account` class have an optional `chunk_size` argument that you can use to set a non-default page size when fetching, creating, updating or deleting items. ```python from exchangelib import Account, Message a = Account(...) huge_list_of_items = [Message(...) for i in range(10000)] return_ids = a.bulk_create( folder=a.inbox, items=huge_list_of_items, chunk_size=5 ) ``` ## Meetings The `CalendarItem` class allows you send out requests for meetings that you initiate or to cancel meetings that you already set out before. It is also possible to process `MeetingRequest` messages that are received. You can reply to these messages using the `AcceptItem`, `TentativelyAcceptItem` and `DeclineItem` classes. If you receive a cancellation for a meeting (class `MeetingCancellation`) that you already accepted then you can also process these by removing the entry from the calendar. ```python import datetime from exchangelib import Account, CalendarItem from exchangelib.items import MeetingRequest, MeetingCancellation, \ SEND_TO_ALL_AND_SAVE_COPY a = Account(...) # create a meeting request and send it out item = CalendarItem( account=a, folder=a.calendar, start=datetime.datetime(2019, 1, 31, 8, 15, tzinfo=a.default_timezone), end=datetime.datetime(2019, 1, 31, 8, 45, tzinfo=a.default_timezone), subject="Subject of Meeting", body="Please come to my meeting", required_attendees=['anne@example.com', 'bob@example.com'] ) item.save(send_meeting_invitations=SEND_TO_ALL_AND_SAVE_COPY) # cancel a meeting that was sent out using the CalendarItem class for calendar_item in a.calendar.all().order_by('-datetime_received')[:5]: # only the organizer of a meeting can cancel it if calendar_item.organizer.email_address == a.primary_smtp_address: calendar_item.cancel() # processing an incoming MeetingRequest for item in a.inbox.all().order_by('-datetime_received')[:5]: if isinstance(item, MeetingRequest): item.accept(body="Sure, I'll come") # Or: item.decline(body="No way!") # Or: item.tentatively_accept(body="Maybe...") # meeting requests can also be handled from the calendar - e.g. decline the # meeting that was received last. for calendar_item in a.calendar.all().order_by('-datetime_received')[:1]: calendar_item.decline() # processing an incoming MeetingCancellation (also delete from calendar) for item in a.inbox.all().order_by('-datetime_received')[:5]: if isinstance(item, MeetingCancellation): if item.associated_calendar_item_id: calendar_item = a.inbox.get( id=item.associated_calendar_item_id.id, changekey=item.associated_calendar_item_id.changekey ) calendar_item.delete() item.move_to_trash() ``` ## Contacts Fetching personas from a contact folder is supported using the same syntax as folders. Just start your query with `.people()`: ```python # Navigate to a contact folder and start the search from exchangelib import Account, DistributionList from exchangelib.indexed_properties import EmailAddress a = Account(...) folder = a.root / 'AllContacts' for p in folder.people(): print(p) for p in folder.people().only('display_name').filter(display_name='john')\ .order_by('display_name'): print(p) # Getting a single contact in the GAL contact list gal = a.contacts / 'GAL Contacts' contact = gal.get(email_addresses=EmailAddress(email='lucas@example.com')) # All contacts with a gmail address gmail_contacts = list(gal.filter( email_addresses__contains=EmailAddress(email='gmail.com') )) # All Gmail email addresses gmail_addresses = [ e.email for c in gal.filter( email_addresses__contains=EmailAddress(email='gmail.com') ) for e in c.email_addresses ] # All email addresses all_addresses = [ e.email for c in gal.all() for e in c.email_addresses if not isinstance(c, DistributionList) ] ``` Contact items have `photo` and `notes` fields, but they are apparently unused. Instead, you can add a contact photo and notes like this: ```python from exchangelib import Account, FileAttachment a = Account(...) contact = a.contacts.get(given_name='John') contact.body = 'This is a note' contact.save(update_fields=['body']) att = FileAttachment( name='ContactPicture.jpg', content_type='image/png', is_inline=False, is_contact_photo=True, content=open('john_profile_picture.png', 'rb').read(), ) contact.attach(att) ``` ## Extended properties Extended properties makes it possible to attach custom key-value pairs to items and folders on the Exchange server. There are multiple online resources that describe working with extended properties, and list many of the magic values that are used by existing Exchange clients to store common and custom properties. The following is not a comprehensive description of the possibilities, but we do intend to support all the possibilities provided by EWS. If folder items have extended properties, you need to register them before you can access them. Create a subclass of `ExtendedProperty` and define a set of matching setup values: ```python from exchangelib import Account, ExtendedProperty, CalendarItem, Folder, Message a = Account(...) class LunchMenu(ExtendedProperty): property_set_id = '12345678-1234-1234-1234-123456781234' property_name = 'Catering from the cafeteria' property_type = 'String' # Register the property on the item type of your choice CalendarItem.register('lunch_menu', LunchMenu) # Now your property is available as the attribute 'lunch_menu', just like any # other attribute. item = CalendarItem(..., lunch_menu='Foie gras et consommé de légumes') item.save() for i in a.calendar.all(): print(i.lunch_menu) # If you change your mind, jsut remove the property again CalendarItem.deregister('lunch_menu') # You can also create named properties (e.g. created from User Defined Fields in # Outlook, see issue #137): class LunchMenu(ExtendedProperty): distinguished_property_set_id = 'PublicStrings' property_name = 'Catering from the cafeteria' property_type = 'String' # We support extended properties with tags. This is the definition for the # 'completed' and 'followup' flag you can add to items in Outlook (see also # issue #85): class Flag(ExtendedProperty): property_tag = 0x1090 property_type = 'Integer' # Or with property ID: class MyMeetingArray(ExtendedProperty): property_set_id = '00062004-0000-0000-C000-000000000046' property_type = 'BinaryArray' property_id = 32852 # Or using distinguished property sets combined with property ID (here as a hex # value to align with the format usually mentioned in Microsoft docs). This is # the definition for a response to an Outlook Vote request (see issue #198): class VoteResponse(ExtendedProperty): distinguished_property_set_id = 'Common' property_id = 0x00008524 property_type = 'String' ``` Extended properties also work with folders. For folders, it's only possible to register custom fields on all folder types at once. This is because it's difficult to provide a consistent API when some folders have custom fields and others don't. Custom fields must be registered on the generic Folder or RootOfHierarchy folder classes. Here's an example of getting the size (in bytes) of a folder: ```python class FolderSize(ExtendedProperty): property_tag = 0x0e08 property_type = 'Integer' Folder.register('size', FolderSize) print(a.inbox.size) ``` In general, here's how to work with any MAPI property as listed in e.g. https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/mapi-properties. Let's take `PidLidTaskDueDate` as an example. This is the due date for a message maked with the follow-up flag in Microsoft Outlook. `PidLidTaskDueDate` is documented at https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/pidlidtaskduedate-canonical-property. The property ID is `0x00008105` and the property set is `PSETID_Task`. But EWS wants the UUID for `PSETID_Task`, so we look that up in the MS-OXPROPS pdf: https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxprops/f6ab1613-aefe-447d-a49c-18217230b148 The UUID is `00062003-0000-0000-C000-000000000046`. The property type is `PT_SYSTIME` which is also called `SystemTime` (see https://docs.microsoft.com/en-us/dotnet/api/microsoft.exchange.webservices.data.mapipropertytype ) In conclusion, the definition for the due date becomes: ```python class FlagDue(ExtendedProperty): property_set_id = '00062003-0000-0000-C000-000000000046' property_id = 0x8105 property_type = 'SystemTime' Message.register('flag_due', FlagDue) ``` ## Attachments It's possible to create, delete and get attachments connected to any item type. Here's an example showing how to process attachments on existing items. FileAttachments have a 'content' attribute containing the binary content of the file, and ItemAttachments have an 'item' attribute containing the item. The item can be a Message, CalendarItem, Task etc. ```python import os.path from exchangelib import Account, FileAttachment, ItemAttachment, Message a = Account for item in a.inbox.all(): for attachment in item.attachments: if isinstance(attachment, FileAttachment): 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) elif isinstance(attachment, ItemAttachment): if isinstance(attachment.item, Message): print(attachment.item.subject, attachment.item.body) ``` Streaming downloads of file attachment is supported. This reduces memory consumption since we never store the full content of the file in-memory: ```python for item in a.inbox.all(): for attachment in item.attachments: if isinstance(attachment, FileAttachment): local_path = os.path.join('/tmp', attachment.name) with open(local_path, 'wb') as f, attachment.fp as fp: buffer = fp.read(1024) while buffer: f.write(buffer) buffer = fp.read(1024) print('Saved attachment to', local_path) ``` Some more examples of working with attachments: ```python # Create a new item with an attachment item = Message(...) # State the bytes directly, or read from file, BytesIO etc. binary_file_content = 'Hello from unicode æøå'.encode('utf-8') my_file = FileAttachment(name='my_file.txt', content=binary_file_content) item.attach(my_file) my_calendar_item = CalendarItem(...) # If you got the item to attach from the server, you probably want to ignore the # 'mime_content' field that contains copies of other field values on the item. # This avoids duplicate attachments etc. my_calendar_item.mime_content = None 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 ) item.attach(my_other_file) # Remove the attachment again item.detach(my_file) ``` If you want to embed an image in the item body, you can link to the file in the HTML. ```python from exchangelib import HTMLBody message = Message(...) logo_filename = 'logo.png' with open(logo_filename, 'rb') as f: my_logo = FileAttachment( name=logo_filename, content=f.read(), is_inline=True, content_id=logo_filename ) message.attach(my_logo) # Most email systems message.body = HTMLBody( 'Hello logo: ' % logo_filename ) # Gmail needs this additional img attribute message.body = HTMLBody('''\ Hello logo: ''' % logo_filename ) ``` Attachments cannot be updated via EWS. If you want to change an attachment, you must detach the attachment, update the relevant fields, and attach as a new attachment. 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. ## Recurring calendar items There is full read-write support for creating recurring calendar items. You can create daily, weekly, monthly and yearly recurrences (the latter two in relative and absolute versions). Here's an example of creating 7 occurrences on Mondays and Wednesdays of every third week, starting September 1, 2017: ```python import datetime from exchangelib import Account, CalendarItem from exchangelib.fields import MONDAY, WEDNESDAY from exchangelib.recurrence import Recurrence, WeeklyPattern a = Account(...) start = datetime.datetime(2017, 9, 1, 11, tzinfo=a.default_timezone) end = start + datetime.timedelta(hours=2) master_recurrence = CalendarItem( folder=a.calendar, start=start, end=end, subject='Hello Recurrence', recurrence=Recurrence( pattern=WeeklyPattern(interval=3, weekdays=[MONDAY, WEDNESDAY]), start=start.date(), number=7 ), ).save() # Occurrence data for the master item i = a.calendar.get(id=master_recurrence.id) print(i.subject, i.start, i.end) print(i.recurrence) print(i.first_occurrence) print(i.last_occurrence) for o in i.modified_occurrences: print(o) for o in i.deleted_occurrences: print(o) # All occurrences expanded. The recurrence will span over 4 iterations of a # 3-week period. for i in a.calendar.view( start=start, end=start + datetime.timedelta(days=4*3*7) ): print(i.subject, i.start, i.end) # 'modified_occurrences' and 'deleted_occurrences' of master items are read-only # fields. To delete or modify an occurrence, you must use 'view()' to fetch the # occurrence and modify or delete it: for occurrence in a.calendar.view( start=start, end=start + datetime.timedelta(days=4*3*7) ): # Delete or update random occurrences. This will affect # 'modified_occurrences' and 'deleted_occurrences' of the master item. if occurrence.start.milliseconds % 2: # We receive timestamps as UTC but want to write back as local timezone occurrence.start = occurrence.start.astimezone(a.default_timezone) occurrence.start += datetime.timedelta(minutes=30) occurrence.end = occurrence.end.astimezone(a.default_timezone) occurrence.end += datetime.timedelta(minutes=30) occurrence.subject = 'My new subject' occurrence.save() else: occurrence.delete() # If you want to access a specific occurrence any you only have the master: third_occurrence = master_recurrence.occurrence(index=3) # Get all fields on this occurrence third_occurrence.refresh() # Change a field on the occurrence third_occurrence.start += datetime.timedelta(hours=3) # Delete occurrence third_occurrence.save(update_fields=['start']) # Similarly, you can reach the master recurrence from the occurrence master = third_occurrence.master_recurrence() master.subject = 'An update' master.save(update_fields=['subject']) ``` ## Message timestamp fields Each `Message` item has four timestamp fields: - `datetime_created` - `datetime_sent` - `datetime_received` - `last_modified_time` The values for these fields are set by the Exchange server and are not modifiable via EWS. All values are timezone-aware `EWSDateTime` instances. The `datetime_sent` value may be earlier than `datetime_created`. ## Out of Facility (OOF) You can get and set OOF messages using the `Account.oof_settings` property: ```python import datetime from exchangelib import Account, OofSettings a = Account(...) # Get the current OOF settings a.oof_settings # Change the OOF settings to something else a.oof_settings = OofSettings( state=OofSettings.SCHEDULED, external_audience='Known', internal_reply="I'm in the pub. See ya guys!", external_reply="I'm having a business dinner in town", start=datetime.datetime(2017, 11, 1, 11, tzinfo=a.default_timezone), end=datetime.datetime(2017, 12, 1, 11, tzinfo=a.default_timezone), ) # Disable OOF messages a.oof_settings = OofSettings( state=OofSettings.DISABLED, internal_reply='', external_reply='', ) ``` ## Mail tips Mail tips for an account contain some extra information about the account, e.g. OOF information, max message size, whether the mailbox is full, messages are moderated etc. Here's how to get mail tips for a single account: ```python from exchangelib import Account a = Account(...) print(a.mail_tips) ``` ## Delegate information An account can have delegates, which are other users that are allowed to access the account. Here's how to fetch information about those delegates, including which level of access they have to the account. ```python from exchangelib import Account a = Account(...) print(a.delegates) ``` ## Export and upload Exchange supports backup and restore of folder contents using special export and upload services. They are available on the `Account` model: ```python from exchangelib import Account a = Account(...) items = a.inbox.all().only('id', 'changekey') data = a.export(items) # Pass a list of Items or (item_id, changekey) tuples a.upload((a.inbox, d) for d in data) # Expects a list of (folder, data) tuples ``` ## Synchronization, subscriptions and notifications Methods for synchronization of folders and items, and for subscribing to, and receiving notifications, are available on the `Folder` model. An in-depth description of how to synchronize folders and items using EWS is available at [https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/mailbox-synchronization-and-ews-in-exchange](https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/mailbox-synchronization-and-ews-in-exchange). A description of how to subscribe to notifications and receive notifications using EWS is available at [https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/notification-subscriptions-mailbox-events-and-ews-in-exchange](https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/notification-subscriptions-mailbox-events-and-ews-in-exchange): The following shows how to synchronize folders and items: ```python from exchangelib import Account a = Account(...) # Synchronize your local folder hierarchy underneath a.inbox for change_type, item in a.inbox.sync_hierarchy(): # Do something depending on the change types defined in # SyncFolderHierarchy.CHANGE_TYPES pass # The next time you call a.inbox.sync_hierarchy(), you will only get folder # changes since the last .sync_hierarchy() call. The sync status is stored in # a.inbox.folder_sync_state. # Synchronize your local items within a.inbox for change_type, item in a.inbox.sync_items(): # Do something depending on the change types defined in # SyncFolderItems.CHANGE_TYPES pass # The next time you call a.inbox.sync_items(), you will only get item changes # since the last .sync_items() call. The sync status is stored in # a.inbox.item_sync_state. ``` Here's how to create a pull subscription that can be used to pull events from the server: ```python subscription_id, watermark = a.inbox.subscribe_to_pull() ``` Here's how to create a push subscription. The server will regularly send an HTTP POST request to the callback URL to deliver changes or a status message. There is also support for parsing the POST data that the Exchange server sends to the callback URL. ```python subscription_id, watermark = a.inbox.subscribe_to_push( callback_url='https://my_app.example.com/callback_url' ) ``` When the server sends a push notification, the POST data contains a 'SendNotification' XML document. You can use exchangelib in the callback URL implementation to parse this data. Here's a short example of a Flask app that handles these documents: ```python from exchangelib.services import SendNotification from flask import Flask, request app = Flask(__name__) @app.route('/callback_url', methods=['POST']) def upload_file(): ws = SendNotification(protocol=None) for notification in ws.parse(request.data): # ws.parse() returns Notification objects pass ``` Here's how to create a streaming subscription that can be used to stream events from the server. ```python subscription_id = a.inbox.subscribe_to_streaming() ``` Cancel the subscription when you're done synchronizing. This is not supported for push subscriptions. They cancel automatically after a certain amount of failed attempts. ```python a.inbox.unsubscribe(subscription_id) ``` When creating subscriptions, you can also use one of the three context managers that handle unsubscription automatically: ```python with a.inbox.pull_subscription() as (subscription_id, watermark): pass with a.inbox.push_subscription( callback_url='https://my_app.example.com/callback_url' ) as (subscription_id, watermark): pass with a.inbox.streaming_subscription() as subscription_id: pass ``` Pull events from the server. This method returns Notification objects that contain events in the `events` attribute and a new watermark in the `watermark` attribute. ```python from exchangelib.properties import CopiedEvent, CreatedEvent, DeletedEvent, \ ModifiedEvent for notification in a.inbox.get_events(subscription_id, watermark): for event in notification.events: if isinstance(event, (CreatedEvent, ModifiedEvent)): # Do something pass elif isinstance(event, (CopiedEvent, DeletedEvent)): # Do something else pass ``` Stream events from the server. This method returns Notification objects that contain events in the `events` attribute. Takes an optional `connection_timeout` argument that defines how long, in minutes, to keep the connection open. `.get_streaming_events()` will block while the connection is open, yielding notifications as they become available on the request stream. `.get_streaming_events()` occupies a connection to the server while it's streaming events. If you need to do additional calls to the server while streaming, e.g. fetching extra item fields, moving items to a folder, sending emails etc, you need to make sure that you have enough connections to do so. The default configuration is to only have 1 connection. See the documentation on `Configuration.max_connections` on how to increase the connection count. ```python from exchangelib.properties import MovedEvent, NewMailEvent, StatusEvent, \ FreeBusyChangedEvent for notification in a.inbox.get_streaming_events( subscription_id, connection_timeout=1 ): for event in notification.events: if isinstance(event, (MovedEvent, NewMailEvent)): # Do something pass elif isinstance(event, (StatusEvent, FreeBusyChangedEvent)): # Do something else pass ``` ## Non-account services ```python from exchangelib import Account, DLMailbox from exchangelib.properties import AlternateId, EWS_ID, OWA_ID a = Account(...) # Get timezone information from the server a.protocol.get_timezones() # Get room lists defined on the server a.protocol.get_roomlists() # Get rooms belonging to a specific room list for rl in a.protocol.get_roomlists(): a.protocol.get_rooms(rl) # Get account information for a list of names or email addresses for mailbox in a.protocol.resolve_names(['ann@example.com', 'ben@example.com']): print(mailbox.email_address) for mailbox, contact in a.protocol.resolve_names( ['anne', 'bart'], return_full_contact_data=True ): print(mailbox.email_address, contact.display_name) # Get all mailboxes on a distribution list for mailbox in a.protocol.expand_dl( DLMailbox(email_address='distro@example.com', mailbox_type='PublicDL') ): print(mailbox.email_address) # Or just pass a string containing the SMTP address for mailbox in a.protocol.expand_dl('distro@example.com'): print(mailbox.email_address) # Convert item IDs from one format to another for converted_id in a.protocol.convert_ids([ AlternateId(id='AAA=', format=EWS_ID, mailbox=a.primary_smtp_address), ], destination_format=OWA_ID): print(converted_id) # Get searchable mailboxes. This method is only available to users who have been # assigned the Discovery Management RBAC role. (This feature works on Exchange # 2013 onwards) for mailbox in a.protocol.get_searchable_mailboxes(): print(mailbox) ``` EWS supports getting availability information for a set of users in a certain timeframe. The server returns an object for each account containing free/busy information, including a list of calendar events in the user's calendar, and the working hours and timezone of the user. ```python import datetime from exchangelib import Account a = Account(...) start = datetime.datetime.now(a.default_timezone) end = start + datetime.timedelta(hours=6) accounts = [(a, 'Organizer', False)] for busy_info in a.protocol.get_free_busy_info( accounts=accounts, start=start, end=end ): print(busy_info) ``` The calendar events and working hours are returned as naive datetimes. To convert to timezone-aware datetimes, a bit of extra work is needed if the users are not known to be in the same timezone. ```python # Get all server timezones. We need that to convert 'working_hours_timezone' import datetime from exchangelib import Account, EWSTimeZone a = Account(...) timezones = list(a.protocol.get_timezones(return_full_timezone_data=True)) # Get availability information for a list of accounts start = datetime.datetime.now(a.default_timezone) end = start + datetime.timedelta(hours=6) # get_free_busy_info expects (account, attendee_type, exclude_conflicts) tuples accounts = [(a, 'Organizer', False)] for busy_info in a.protocol.get_free_busy_info( accounts=accounts, start=start, end=end ): # Convert the TimeZone object to a Microsoft timezone ID ms_id = busy_info.working_hours_timezone.to_server_timezone( timezones, start.year ) account_tz = EWSTimeZone.from_ms_id(ms_id) print(account_tz, busy_info.working_hours) for event in busy_info.calendar_events: print(account_tz.localize(event.start), account_tz.localize(event.end)) ``` ## Troubleshooting If you are having trouble using this library, the first thing to try is to enable debug logging. This will output a huge amount of information about what is going on, most notable the actual XML documents that are going over the wire. This can be really handy to see which fields are being sent and received. ```python import logging # This handler will pretty-print and syntax highlight the request and response # XML documents from exchangelib.util import PrettyXmlHandler logging.basicConfig(level=logging.DEBUG, handlers=[PrettyXmlHandler()]) # Your code using exchangelib goes here ``` Most class definitions have a docstring containing at least a URL to the MSDN page for the corresponding XML element. ```python from exchangelib import CalendarItem print(CalendarItem.__doc__) ``` ## Tests The test suite is split into unit tests, and integration tests that require a real Exchange server. If you want to run the full test suite, you must provide setup parameters for a test account. Copy `settings.yml.sample` to `settings.yml` and change the default parameters. If a `settings.yml` is available, we will run the entire test suite. Otherwise, just the unit tests are run. *WARNING*: The test account should not contain valuable data. The tests try hard to no touch existing data in the account, but accidents happen. You can run either the entire test suite or individual tests. ```bash # Full test suite python -m unittest # Single test class or test case python -m unittest -k test_folder.FolderTest.test_refresh # If you want extreme levels of debug output: DEBUG=1 python -m unittest -k test_folder.FolderTest.test_refresh # Tests can be run in parallel if you install the 'unittest-parallel' package unittest-parallel -j 4 --class-fixtures ``` exchangelib-4.6.1/exchangelib/000077500000000000000000000000001414601472700162525ustar00rootroot00000000000000exchangelib-4.6.1/exchangelib/__init__.py000066400000000000000000000051221414601472700203630ustar00rootroot00000000000000import sys from .account import Account, Identity from .attachments import FileAttachment, ItemAttachment from .autodiscover import discover from .configuration import Configuration from .credentials import DELEGATE, IMPERSONATION, Credentials, OAuth2Credentials, \ OAuth2AuthorizationCodeCredentials from .ewsdatetime import EWSDate, EWSDateTime, EWSTimeZone, UTC, UTC_NOW from .extended_properties import ExtendedProperty from .folders import Folder, RootOfHierarchy, FolderCollection, SHALLOW, DEEP from .items import AcceptItem, TentativelyAcceptItem, DeclineItem, CalendarItem, CancelCalendarItem, Contact, \ DistributionList, Message, PostItem, Task, ForwardItem, ReplyToItem, ReplyAllToItem from .properties import Body, HTMLBody, ItemId, Mailbox, Attendee, Room, RoomList, UID, DLMailbox from .protocol import FaultTolerance, FailFast, BaseProtocol, NoVerifyHTTPAdapter, TLSClientAuth from .restriction import Q from .settings import OofSettings from .transport import BASIC, DIGEST, NTLM, GSSAPI, SSPI, OAUTH2, CBA from .version import Build, Version __version__ = '4.6.1' __all__ = [ '__version__', 'Account', 'Identity', 'FileAttachment', 'ItemAttachment', 'discover', 'Configuration', 'DELEGATE', 'IMPERSONATION', 'Credentials', 'OAuth2AuthorizationCodeCredentials', 'OAuth2Credentials', 'EWSDate', 'EWSDateTime', 'EWSTimeZone', 'UTC', 'UTC_NOW', 'ExtendedProperty', 'Folder', 'RootOfHierarchy', 'FolderCollection', 'SHALLOW', 'DEEP', 'AcceptItem', 'TentativelyAcceptItem', 'DeclineItem', 'CalendarItem', 'CancelCalendarItem', 'Contact', 'DistributionList', 'Message', 'PostItem', 'Task', 'ForwardItem', 'ReplyToItem', 'ReplyAllToItem', 'ItemId', 'Mailbox', 'DLMailbox', 'Attendee', 'Room', 'RoomList', 'Body', 'HTMLBody', 'UID', 'FailFast', 'FaultTolerance', 'BaseProtocol', 'NoVerifyHTTPAdapter', 'TLSClientAuth', 'OofSettings', 'Q', 'BASIC', 'DIGEST', 'NTLM', 'GSSAPI', 'SSPI', 'OAUTH2', 'CBA', 'Build', 'Version', ] # Set a default user agent, e.g. "exchangelib/3.1.1 (python-requests/2.22.0)" import requests.utils BaseProtocol.USERAGENT = "%s/%s (%s)" % (__name__, __version__, requests.utils.default_user_agent()) # Support fromisoformat() in Python < 3.7 if sys.version_info[:2] < (3, 7): from backports.datetime_fromisoformat import MonkeyPatch MonkeyPatch.patch_fromisoformat() def close_connections(): from .autodiscover import close_connections as close_autodiscover_connections from .protocol import close_connections as close_protocol_connections close_autodiscover_connections() close_protocol_connections() exchangelib-4.6.1/exchangelib/account.py000066400000000000000000000725641414601472700202760ustar00rootroot00000000000000from 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: """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): """ :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)) 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) # 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) @property def primary_smtp_address(self): return self.identity.primary_smtp_address @threaded_cached_property def admin_audit_logs(self): return self.root.get_default_folder(AdminAuditLogs) @threaded_cached_property def archive_deleted_items(self): return self.archive_root.get_default_folder(ArchiveDeletedItems) @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 exchangelib-4.6.1/exchangelib/attachments.py000066400000000000000000000232761414601472700211510ustar00rootroot00000000000000import io import logging import mimetypes from .fields import BooleanField, TextField, IntegerField, URIField, DateTimeField, EWSElementField, Base64Field, \ ItemField, IdField, FieldPath from .properties import EWSElement, EWSMeta from .services import GetAttachment, CreateAttachment, DeleteAttachment log = logging.getLogger(__name__) class AttachmentId(EWSElement): """'id' and 'changekey' are UUIDs generated by Exchange. MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/attachmentid """ ELEMENT_NAME = 'AttachmentId' ID_ATTR = 'Id' ROOT_ID_ATTR = 'RootItemId' ROOT_CHANGEKEY_ATTR = 'RootItemChangeKey' id = IdField(field_uri=ID_ATTR, is_required=True) root_id = IdField(field_uri=ROOT_ID_ATTR) root_changekey = IdField(field_uri=ROOT_CHANGEKEY_ATTR) class Attachment(EWSElement, metaclass=EWSMeta): """Base class for FileAttachment and ItemAttachment.""" attachment_id = EWSElementField(value_cls=AttachmentId) name = TextField(field_uri='Name') content_type = TextField(field_uri='ContentType') content_id = TextField(field_uri='ContentId') content_location = URIField(field_uri='ContentLocation') size = IntegerField(field_uri='Size', is_read_only=True) # Attachment size in bytes last_modified_time = DateTimeField(field_uri='LastModifiedTime') is_inline = BooleanField(field_uri='IsInline') __slots__ = 'parent_item', def __init__(self, **kwargs): self.parent_item = kwargs.pop('parent_item', None) super().__init__(**kwargs) def clean(self, version=None): from .items import Item if self.parent_item is not None and not isinstance(self.parent_item, Item): raise ValueError("'parent_item' value %r must be an Item instance" % self.parent_item) if self.content_type is None and self.name is not None: self.content_type = mimetypes.guess_type(self.name)[0] or 'application/octet-stream' super().clean(version=version) def attach(self): # Adds this attachment to an item and updates the changekey of the parent item if self.attachment_id: raise ValueError('This attachment has already been created') if not self.parent_item or not self.parent_item.account: raise ValueError('Parent item %s must have an account' % self.parent_item) item = CreateAttachment(account=self.parent_item.account).get(parent_item=self.parent_item, items=[self]) attachment_id = item.attachment_id if attachment_id.root_id != self.parent_item.id: raise ValueError("'root_id' vs. 'id' mismatch") if attachment_id.root_changekey == self.parent_item.changekey: raise ValueError('root_id changekey match') self.parent_item.changekey = attachment_id.root_changekey # EWS does not like receiving root_id and root_changekey on subsequent requests attachment_id.root_id = None attachment_id.root_changekey = None self.attachment_id = attachment_id def detach(self): # Deletes an attachment remotely and updates the changekey of the parent item if not self.attachment_id: raise ValueError('This attachment has not been created') if not self.parent_item or not self.parent_item.account: raise ValueError('Parent item %s must have an account' % self.parent_item) root_item_id = DeleteAttachment(account=self.parent_item.account).get(items=[self.attachment_id]) if root_item_id.id != self.parent_item.id: raise ValueError("'root_item_id.id' mismatch") if root_item_id.changekey == self.parent_item.changekey: raise ValueError("'root_item_id.changekey' match") self.parent_item.changekey = root_item_id.changekey self.parent_item = None self.attachment_id = None def __hash__(self): if self.attachment_id: return hash(self.attachment_id) # Be careful to avoid recursion on the back-reference to the parent item return hash(tuple(getattr(self, f) for f in self._slots_keys if f != 'parent_item')) def __repr__(self): return self.__class__.__name__ + '(%s)' % ', '.join( '%s=%r' % (f.name, getattr(self, f.name)) for f in self.FIELDS if f.name not in ('_item', '_content') ) class FileAttachment(Attachment): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/fileattachment""" ELEMENT_NAME = 'FileAttachment' is_contact_photo = BooleanField(field_uri='IsContactPhoto') _content = Base64Field(field_uri='Content') __slots__ = '_fp', def __init__(self, **kwargs): kwargs['_content'] = kwargs.pop('content', None) super().__init__(**kwargs) self._fp = None @property def fp(self): # Return a file-like object for the content. This avoids creating multiple in-memory copies of the content. if self._fp is None: self._init_fp() return self._fp def _init_fp(self): # Create a file-like object for the attachment content. We try hard to reduce memory consumption so we never # store the full attachment content in-memory. if not self.parent_item or not self.parent_item.account: raise ValueError('%s must have an account' % self.__class__.__name__) self._fp = FileAttachmentIO(attachment=self) @property def content(self): """Return the attachment content. Stores a local copy of the content in case you want to upload the attachment again later. """ if self.attachment_id is None: return self._content if self._content is not None: return self._content # We have an ID to the data but still haven't called GetAttachment to get the actual data. Do that now. with self.fp as fp: self._content = fp.read() return self._content @content.setter def content(self, value): """Replace the attachment content.""" if not isinstance(value, bytes): raise ValueError("'value' %r must be a bytes object" % value) self._content = value @classmethod def from_xml(cls, elem, account): kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS} kwargs['content'] = kwargs.pop('_content') cls._clear(elem) return cls(**kwargs) def to_xml(self, version): self._content = self.content # Make sure content is available, to avoid ErrorRequiredPropertyMissing return super().to_xml(version=version) def __getstate__(self): # The fp does not need to be pickled state = {k: getattr(self, k) for k in self._slots_keys} del state['_fp'] return state def __setstate__(self, state): # Restore the fp for k in self._slots_keys: setattr(self, k, state.get(k)) self._fp = None class ItemAttachment(Attachment): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/itemattachment""" ELEMENT_NAME = 'ItemAttachment' _item = ItemField(field_uri='Item') def __init__(self, **kwargs): kwargs['_item'] = kwargs.pop('item', None) super().__init__(**kwargs) @property def item(self): from .folders import BaseFolder if self.attachment_id is None: return self._item if self._item is not None: return self._item # We have an ID to the data but still haven't called GetAttachment to get the actual data. Do that now. if not self.parent_item or not self.parent_item.account: raise ValueError('%s must have an account' % self.__class__.__name__) additional_fields = { FieldPath(field=f) for f in BaseFolder.allowed_item_fields(version=self.parent_item.account.version) } attachment = GetAttachment(account=self.parent_item.account).get( items=[self.attachment_id], include_mime_content=True, body_type=None, filter_html_content=None, additional_fields=additional_fields, ) self._item = attachment.item return self._item @item.setter def item(self, value): from .items import Item if not isinstance(value, Item): raise ValueError("'value' %r must be an Item object" % value) self._item = value @classmethod def from_xml(cls, elem, account): kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS} kwargs['item'] = kwargs.pop('_item') cls._clear(elem) return cls(**kwargs) class FileAttachmentIO(io.RawIOBase): """A BytesIO where the stream of data comes from the GetAttachment service.""" def __init__(self, attachment): self._attachment = attachment self._overflow = None def readable(self): return True @property def closed(self): return self._stream is None def readinto(self, b): buf_size = len(b) # We can't return more than l bytes try: chunk = self._overflow or next(self._stream) except StopIteration: return 0 else: output, self._overflow = chunk[:buf_size], chunk[buf_size:] b[:len(output)] = output return len(output) def __enter__(self): self._stream = GetAttachment(account=self._attachment.parent_item.account).stream_file_content( attachment_id=self._attachment.attachment_id ) self._overflow = None return io.BufferedReader(self, buffer_size=io.DEFAULT_BUFFER_SIZE) def __exit__(self, *args, **kwargs): self._stream = None self._overflow = None exchangelib-4.6.1/exchangelib/autodiscover/000077500000000000000000000000001414601472700207615ustar00rootroot00000000000000exchangelib-4.6.1/exchangelib/autodiscover/__init__.py000066400000000000000000000007361414601472700231000ustar00rootroot00000000000000from .cache import AutodiscoverCache, autodiscover_cache from .discovery import Autodiscovery, discover from .protocol import AutodiscoverProtocol def close_connections(): with autodiscover_cache: autodiscover_cache.close() def clear_cache(): with autodiscover_cache: autodiscover_cache.clear() __all__ = [ 'AutodiscoverCache', 'AutodiscoverProtocol', 'Autodiscovery', 'discover', 'autodiscover_cache', 'close_connections', 'clear_cache' ] exchangelib-4.6.1/exchangelib/autodiscover/cache.py000066400000000000000000000140121414601472700223740ustar00rootroot00000000000000import getpass import glob import logging import os import shelve import sys import tempfile from contextlib import contextmanager from threading import RLock from .protocol import AutodiscoverProtocol from ..configuration import Configuration log = logging.getLogger(__name__) def shelve_filename(): # Add the version of the cache format to the filename. If we change the format of the cached data, this version # must be bumped. Otherwise, new versions of this package cannot open cache files generated by older versions. version = 2 # 'shelve' may pickle objects using different pickle protocol versions. Append the python major+minor version # numbers to the filename. Also append the username, to avoid permission errors. major, minor = sys.version_info[:2] try: user = getpass.getuser() except KeyError: # getuser() fails on some systems. Provide a sane default. See issue #448 user = 'exchangelib' return 'exchangelib.{version}.cache.{user}.py{major}{minor}'.format( version=version, user=user, major=major, minor=minor ) AUTODISCOVER_PERSISTENT_STORAGE = os.path.join(tempfile.gettempdir(), shelve_filename()) @contextmanager def shelve_open_with_failover(filename): # We can expect empty or corrupt files. Whatever happens, just delete the cache file and try again. # 'shelve' may add a backend-specific suffix to the file, so also delete all files with a suffix. # We don't know which file caused the error, so just delete them all. try: shelve_handle = shelve.open(filename) # Try to actually use the shelve. Some implementations may allow opening the file but then throw # errors on access. try: _ = shelve_handle[''] except KeyError: # The entry doesn't exist. This is expected. pass except Exception as e: for f in glob.glob(filename + '*'): log.warning('Deleting invalid cache file %s (%r)', f, e) os.unlink(f) shelve_handle = shelve.open(filename) yield shelve_handle class AutodiscoverCache: """Stores the translation from (email domain, credentials) -> AutodiscoverProtocol object so we can re-use TCP connections to an autodiscover server within the same process. Also persists the email domain -> (autodiscover endpoint URL, auth_type) translation to the filesystem so the cache can be shared between multiple processes. According to Microsoft, we may forever cache the (email domain -> autodiscover endpoint URL) mapping, or until it stops responding. My previous experience with Exchange products in mind, I'm not sure if I should trust that advice. But it could save some valuable seconds every time we start a new connection to a known server. In any case, the persistent storage must not contain any sensitive information since the cache could be readable by unprivileged users. Domain, endpoint and auth_type are OK to cache since this info is make publicly available on HTTP and DNS servers via the autodiscover protocol. Just don't persist any credentials info. If an autodiscover lookup fails for any reason, the corresponding cache entry must be purged. 'shelve' is supposedly thread-safe and process-safe, which suits our needs. """ def __init__(self): self._protocols = {} # Mapping from (domain, credentials) to AutodiscoverProtocol self._lock = RLock() @property def _storage_file(self): return AUTODISCOVER_PERSISTENT_STORAGE def clear(self): # Wipe the entire cache with shelve_open_with_failover(self._storage_file) as db: db.clear() self._protocols.clear() def __len__(self): return len(self._protocols) def __contains__(self, key): domain = key[0] with shelve_open_with_failover(self._storage_file) as db: return str(domain) in db def __getitem__(self, key): protocol = self._protocols.get(key) if protocol: return protocol domain, credentials = key with shelve_open_with_failover(self._storage_file) as db: endpoint, auth_type, retry_policy = db[str(domain)] # It's OK to fail with KeyError here protocol = AutodiscoverProtocol(config=Configuration( service_endpoint=endpoint, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy )) self._protocols[key] = protocol return protocol def __setitem__(self, key, protocol): # Populate both local and persistent cache domain = key[0] with shelve_open_with_failover(self._storage_file) as db: # Don't change this payload without bumping the cache file version in shelve_filename() db[str(domain)] = (protocol.service_endpoint, protocol.auth_type, protocol.retry_policy) self._protocols[key] = protocol def __delitem__(self, key): # Empty both local and persistent cache. Don't fail on non-existing entries because we could end here # multiple times due to race conditions. domain = key[0] with shelve_open_with_failover(self._storage_file) as db: try: del db[str(domain)] except KeyError: pass try: del self._protocols[key] except KeyError: pass def close(self): # Close all open connections for (domain, _), protocol in self._protocols.items(): log.debug('Domain %s: Closing sessions', domain) protocol.close() del protocol self._protocols.clear() def __enter__(self): self._lock.__enter__() def __exit__(self, *args, **kwargs): self._lock.__exit__(*args, **kwargs) def __del__(self): # pylint: disable=bare-except try: self.close() except Exception: # nosec # __del__ should never fail pass def __str__(self): return str(self._protocols) autodiscover_cache = AutodiscoverCache() exchangelib-4.6.1/exchangelib/autodiscover/discovery.py000066400000000000000000000672261414601472700233570ustar00rootroot00000000000000import logging import time from urllib.parse import urlparse import dns.resolver from cached_property import threaded_cached_property from .cache import autodiscover_cache from .properties import Autodiscover from .protocol import AutodiscoverProtocol from ..configuration import Configuration from ..credentials import OAuth2Credentials from ..errors import AutoDiscoverFailed, AutoDiscoverCircularRedirect, TransportError, RedirectError, UnauthorizedError from ..protocol import Protocol, FailFast from ..transport import get_auth_method_from_response, DEFAULT_HEADERS, NOAUTH, OAUTH2, GSSAPI, AUTH_TYPE_MAP, \ CREDENTIALS_REQUIRED from ..util import post_ratelimited, get_domain, get_redirect_url, _back_off_if_needed, \ DummyResponse, CONNECTION_ERRORS, TLS_ERRORS from ..version import Version log = logging.getLogger(__name__) def discover(email, credentials=None, auth_type=None, retry_policy=None): return Autodiscovery( email=email, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy ).discover() class SrvRecord: """A container for autodiscover-related SRV records in DNS.""" def __init__(self, priority, weight, port, srv): self.priority = priority self.weight = weight self.port = port self.srv = srv def __eq__(self, other): for k in self.__dict__: if getattr(self, k) != getattr(other, k): return False return True class Autodiscovery: """Autodiscover is a Microsoft protocol for automatically getting the endpoint of the Exchange server and other connection-related settings holding the email address using only the email address, and username and password of the user. For a description of the protocol implemented, see "Autodiscover for Exchange ActiveSync developers": https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-interoperability-guidance/hh352638%28v%3dexchg.140%29 Descriptions of the steps from the article are provided in their respective methods in this class. For a description of how to handle autodiscover error messages, see: https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/handling-autodiscover-error-messages A tip from the article: The client can perform steps 1 through 4 in any order or in parallel to expedite the process, but it must wait for responses to finish at each step before proceeding. Given that many organizations prefer to use the URL in step 2 to set up the Autodiscover service, the client might try this step first. Another possibly newer resource which has not yet been attempted is "Outlook 2016 Implementation of Autodiscover": https://support.microsoft.com/en-us/help/3211279/outlook-2016-implementation-of-autodiscover WARNING: The autodiscover protocol is very complicated. If you have problems autodiscovering using this implementation, start by doing an official test at https://testconnectivity.microsoft.com """ # When connecting to servers that may not be serving the correct endpoint, we should use a retry policy that does # not leave us hanging for a long time on each step in the protocol. INITIAL_RETRY_POLICY = FailFast() RETRY_WAIT = 10 # Seconds to wait before retry on connection errors MAX_REDIRECTS = 10 # Maximum number of URL redirects before we give up DNS_RESOLVER_KWARGS = {} DNS_RESOLVER_ATTRS = { 'timeout': AutodiscoverProtocol.TIMEOUT, } def __init__(self, email, credentials=None, auth_type=None, retry_policy=None): """ :param email: The email address to autodiscover :param credentials: Credentials with authorization to make autodiscover lookups for this Account (Default value = None) :param auth_type: (Default value = None) :param retry_policy: (Default value = None) """ self.email = email self.credentials = credentials self.auth_type = auth_type # The auth type that the resulting protocol instance should have self.retry_policy = retry_policy # The retry policy that the resulting protocol instance should have self._urls_visited = [] # Collects HTTP and Autodiscover redirects self._redirect_count = 0 self._emails_visited = [] # Collects Autodiscover email redirects def discover(self): self._emails_visited.append(self.email.lower()) # Check the autodiscover cache to see if we already know the autodiscover service endpoint for this email # domain. Use a lock to guard against multiple threads competing to cache information. log.debug('Waiting for autodiscover_cache lock') with autodiscover_cache: log.debug('autodiscover_cache lock acquired') cache_key = self._cache_key domain = get_domain(self.email) if cache_key in autodiscover_cache: ad_protocol = autodiscover_cache[cache_key] log.debug('Cache hit for key %s: %s', cache_key, ad_protocol.service_endpoint) try: ad_response = self._quick(protocol=ad_protocol) except AutoDiscoverFailed: # Autodiscover no longer works with this domain. Clear cache and try again after releasing the lock log.debug('AD request failure. Removing cache for key %s', cache_key) del autodiscover_cache[cache_key] ad_response = self._step_1(hostname=domain) else: # This will cache the result ad_response = self._step_1(hostname=domain) log.debug('Released autodiscover_cache_lock') if ad_response.redirect_address: log.debug('Got a redirect address: %s', ad_response.redirect_address) if ad_response.redirect_address.lower() in self._emails_visited: raise AutoDiscoverCircularRedirect('We were redirected to an email address we have already seen') # Start over, but with the new email address self.email = ad_response.redirect_address return self.discover() # We successfully received a response. Clear the cache of seen emails etc. self.clear() return self._build_response(ad_response=ad_response) def clear(self): # This resets cached variables self._urls_visited = [] self._redirect_count = 0 self._emails_visited = [] @property def _cache_key(self): # We may be using multiple different credentials and changing our minds on TLS verification. This key # combination should be safe for caching. domain = get_domain(self.email) return domain, self.credentials @threaded_cached_property def resolver(self): resolver = dns.resolver.Resolver(**self.DNS_RESOLVER_KWARGS) for k, v in self.DNS_RESOLVER_ATTRS.items(): setattr(resolver, k, v) return resolver def _build_response(self, ad_response): ews_url = ad_response.ews_url if not ews_url: raise AutoDiscoverFailed("Response is missing an 'ews_url' value") if not ad_response.autodiscover_smtp_address: # Autodiscover does not always return an email address. In that case, the requesting email should be used ad_response.user.autodiscover_smtp_address = self.email # Get the server version. Not all protocol entries have a server version so we cheat a bit and also look at the # other ones that point to the same endpoint. for protocol in ad_response.account.protocols: if not protocol.ews_url or not protocol.server_version: continue if protocol.ews_url.lower() == ews_url.lower(): version = Version(build=protocol.server_version) break else: version = None # We may not want to use the auth_package hints in the AD response. It could be incorrect and we can just guess. protocol = Protocol( config=Configuration( service_endpoint=ews_url, credentials=self.credentials, version=version, auth_type=self.auth_type, retry_policy=self.retry_policy, ) ) return ad_response, protocol def _quick(self, protocol): # Reset auth type and retry policy if we requested non-default values if self.auth_type: protocol.config.auth_type = self.auth_type if self.retry_policy: protocol.config.retry_policy = self.retry_policy try: r = self._get_authenticated_response(protocol=protocol) except TransportError as e: raise AutoDiscoverFailed('Response error: %s' % e) if r.status_code == 200: try: ad = Autodiscover.from_bytes(bytes_content=r.content) return self._step_5(ad=ad) except ValueError as e: raise AutoDiscoverFailed('Invalid response: %s' % e) raise AutoDiscoverFailed('Invalid response code: %s' % r.status_code) def _redirect_url_is_valid(self, url): """Three separate responses can be “Redirect responses”: * An HTTP status code (301, 302) with a new URL * An HTTP status code of 200, but with a payload XML containing a redirect to a different URL * An HTTP status code of 200, but with a payload XML containing a different SMTP address as the target address We only handle the HTTP 302 redirects here. We validate the URL received in the redirect response to ensure that it does not redirect to non-SSL endpoints or SSL endpoints with invalid certificates, and that the redirect is not circular. Finally, we should fail after 10 redirects. :param url: :return: """ if url.lower() in self._urls_visited: log.warning('We have already tried this URL: %s', url) return False if self._redirect_count >= self.MAX_REDIRECTS: log.warning('We reached max redirects at URL: %s', url) return False # We require TLS endpoints if not url.startswith('https://'): log.debug('Invalid scheme for URL: %s', url) return False # Quick test that the endpoint responds and that TLS handshake is OK try: self._get_unauthenticated_response(url, method='head') except TransportError as e: log.debug('Response error on redirect URL %s: %s', url, e) return False self._redirect_count += 1 return True def _get_unauthenticated_response(self, url, method='post'): """Get auth type by tasting headers from the server. Do POST requests be default. HEAD is too error prone, and some servers are set up to redirect to OWA on all requests except POST to the autodiscover endpoint. :param url: :param method: (Default value = 'post') :return: """ # We are connecting to untrusted servers here, so take necessary precautions. hostname = urlparse(url).netloc if not self._is_valid_hostname(hostname): # 'requests' is really bad at reporting that a hostname cannot be resolved. Let's check this separately. # Don't retry on DNS errors. They will most likely be persistent. raise TransportError('%r has no DNS entry' % hostname) kwargs = dict( url=url, headers=DEFAULT_HEADERS.copy(), allow_redirects=False, timeout=AutodiscoverProtocol.TIMEOUT ) if method == 'post': kwargs['data'] = Autodiscover.payload(email=self.email) retry = 0 t_start = time.monotonic() while True: _back_off_if_needed(self.INITIAL_RETRY_POLICY.back_off_until) log.debug('Trying to get response from %s', url) with AutodiscoverProtocol.raw_session(url) as s: try: r = getattr(s, method)(**kwargs) r.close() # Release memory break except TLS_ERRORS as e: # Don't retry on TLS errors. They will most likely be persistent. raise TransportError(str(e)) except CONNECTION_ERRORS as e: r = DummyResponse(url=url, headers={}, request_headers=kwargs['headers']) total_wait = time.monotonic() - t_start if self.INITIAL_RETRY_POLICY.may_retry_on_error(response=r, wait=total_wait): log.debug("Connection error on URL %s (retry %s, error: %s). Cool down", url, retry, e) # Don't respect the 'Retry-After' header. We don't know if this is a useful endpoint, and we # want autodiscover to be reasonably fast. self.INITIAL_RETRY_POLICY.back_off(self.RETRY_WAIT) retry += 1 continue log.debug("Connection error on URL %s: %s", url, e) raise TransportError(str(e)) try: auth_type = get_auth_method_from_response(response=r) except UnauthorizedError: # Failed to guess the auth type auth_type = NOAUTH if r.status_code in (301, 302) and 'location' in r.headers: # Make the redirect URL absolute try: r.headers['location'] = get_redirect_url(r) except TransportError: del r.headers['location'] return auth_type, r def _get_authenticated_response(self, protocol): """Get a response by using the credentials provided. We guess the auth type along the way. :param protocol: :return: """ # Redo the request with the correct auth data = Autodiscover.payload(email=self.email) headers = DEFAULT_HEADERS.copy() session = protocol.get_session() if GSSAPI in AUTH_TYPE_MAP and isinstance(session.auth, AUTH_TYPE_MAP[GSSAPI]): # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/pox-autodiscover-request-for-exchange headers['X-ClientCanHandle'] = 'Negotiate' try: r, session = post_ratelimited(protocol=protocol, session=session, url=protocol.service_endpoint, headers=headers, data=data, allow_redirects=False, stream=False) protocol.release_session(session) except UnauthorizedError as e: # It's entirely possible for the endpoint to ask for login. We should continue if login fails because this # isn't necessarily the right endpoint to use. raise TransportError(str(e)) except RedirectError as e: r = DummyResponse(url=protocol.service_endpoint, headers={'location': e.url}, request_headers=None, status_code=302) return r def _attempt_response(self, url): """Return an (is_valid_response, response) tuple. :param url: :return: """ self._urls_visited.append(url.lower()) log.debug('Attempting to get a valid response from %s', url) try: auth_type, r = self._get_unauthenticated_response(url=url) if isinstance(self.credentials, OAuth2Credentials): # This type of credentials *must* use the OAuth auth type auth_type = OAUTH2 elif self.credentials is None and auth_type in CREDENTIALS_REQUIRED: raise ValueError('Auth type %r was detected but no credentials were provided' % auth_type) ad_protocol = AutodiscoverProtocol( config=Configuration( service_endpoint=url, credentials=self.credentials, auth_type=auth_type, retry_policy=self.INITIAL_RETRY_POLICY, ) ) if auth_type != NOAUTH: r = self._get_authenticated_response(protocol=ad_protocol) except TransportError as e: log.debug('Failed to get a response: %s', e) return False, None if r.status_code in (301, 302) and 'location' in r.headers: redirect_url = get_redirect_url(r) if self._redirect_url_is_valid(url=redirect_url): # The protocol does not specify this explicitly, but by looking at how testconnectivity.microsoft.com # works, it seems that we should follow this URL now and try to get a valid response. return self._attempt_response(url=redirect_url) if r.status_code == 200: try: ad = Autodiscover.from_bytes(bytes_content=r.content) # We got a valid response. Unless this is a URL redirect response, we cache the result if ad.response is None or not ad.response.redirect_url: cache_key = self._cache_key log.debug('Adding cache entry for key %s: %s', cache_key, ad_protocol.service_endpoint) autodiscover_cache[cache_key] = ad_protocol return True, ad except ValueError as e: log.debug('Invalid response: %s', e) return False, None def _is_valid_hostname(self, hostname): log.debug('Checking if %s can be looked up in DNS', hostname) try: self.resolver.resolve(hostname) except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): return False return True def _get_srv_records(self, hostname): """Send a DNS query for SRV entries for the hostname. An SRV entry that has been formatted for autodiscovery will have the following format: canonical name = mail.example.com. service = 8 100 443 webmail.example.com. The first three numbers in the service line are: priority, weight, port :param hostname: :return: """ log.debug('Attempting to get SRV records for %s', hostname) records = [] try: answers = self.resolver.resolve('%s.' % hostname, 'SRV') except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) as e: log.debug('DNS lookup failure: %s', e) return records for rdata in answers: try: vals = rdata.to_text().strip().rstrip('.').split(' ') # Raise ValueError if the first three are not ints, and IndexError if there are less than 4 values priority, weight, port, srv = int(vals[0]), int(vals[1]), int(vals[2]), vals[3] record = SrvRecord(priority=priority, weight=weight, port=port, srv=srv) log.debug('Found SRV record %s ', record) records.append(record) except (ValueError, IndexError): log.debug('Incompatible SRV record for %s (%s)', hostname, rdata.to_text()) return records def _step_1(self, hostname): """Perform step 1, where the client sends an Autodiscover request to https://example.com/autodiscover/autodiscover.xml and then does one of the following: * If the Autodiscover attempt succeeds, the client proceeds to step 5. * If the Autodiscover attempt fails, the client proceeds to step 2. :param hostname: :return: """ url = 'https://%s/Autodiscover/Autodiscover.xml' % hostname log.info('Step 1: Trying autodiscover on %r with email %r', url, self.email) is_valid_response, ad = self._attempt_response(url=url) if is_valid_response: return self._step_5(ad=ad) return self._step_2(hostname=hostname) def _step_2(self, hostname): """Perform step 2, where the client sends an Autodiscover request to https://autodiscover.example.com/autodiscover/autodiscover.xml and then does one of the following: * If the Autodiscover attempt succeeds, the client proceeds to step 5. * If the Autodiscover attempt fails, the client proceeds to step 3. :param hostname: :return: """ url = 'https://autodiscover.%s/Autodiscover/Autodiscover.xml' % hostname log.info('Step 2: Trying autodiscover on %r with email %r', url, self.email) is_valid_response, ad = self._attempt_response(url=url) if is_valid_response: return self._step_5(ad=ad) return self._step_3(hostname=hostname) def _step_3(self, hostname): """Perform step 3, where the client sends an unauth'ed GET method request to http://autodiscover.example.com/autodiscover/autodiscover.xml (Note that this is a non-HTTPS endpoint). The client then does one of the following: * If the GET request returns a 302 redirect response, it gets the redirection URL from the 'Location' HTTP header and validates it as described in the "Redirect responses" section. The client then does one of the following: * If the redirection URL is valid, the client tries the URL and then does one of the following: * If the attempt succeeds, the client proceeds to step 5. * If the attempt fails, the client proceeds to step 4. * If the redirection URL is not valid, the client proceeds to step 4. * If the GET request does not return a 302 redirect response, the client proceeds to step 4. :param hostname: :return: """ url = 'http://autodiscover.%s/Autodiscover/Autodiscover.xml' % hostname log.info('Step 3: Trying autodiscover on %r with email %r', url, self.email) try: _, r = self._get_unauthenticated_response(url=url, method='get') except TransportError: r = DummyResponse(url=url, headers={}, request_headers={}) if r.status_code in (301, 302) and 'location' in r.headers: redirect_url = get_redirect_url(r) if self._redirect_url_is_valid(url=redirect_url): is_valid_response, ad = self._attempt_response(url=redirect_url) if is_valid_response: return self._step_5(ad=ad) return self._step_4(hostname=hostname) return self._step_4(hostname=hostname) return self._step_4(hostname=hostname) def _step_4(self, hostname): """Perform step 4, where the client performs a Domain Name System (DNS) query for an SRV record for _autodiscover._tcp.example.com. The query might return multiple records. The client selects only records that point to an SSL endpoint and that have the highest priority and weight. One of the following actions then occurs: * If no such records are returned, the client proceeds to step 6. * If records are returned, the application randomly chooses a record in the list and validates the endpoint that it points to by following the process described in the "Redirect Response" section. The client then does one of the following: * If the redirection URL is valid, the client tries the URL and then does one of the following: * If the attempt succeeds, the client proceeds to step 5. * If the attempt fails, the client proceeds to step 6. * If the redirection URL is not valid, the client proceeds to step 6. :param hostname: :return: """ dns_hostname = '_autodiscover._tcp.%s' % hostname log.info('Step 4: Trying autodiscover on %r with email %r', dns_hostname, self.email) srv_records = self._get_srv_records(dns_hostname) try: srv_host = _select_srv_host(srv_records) except ValueError: srv_host = None if not srv_host: return self._step_6() redirect_url = 'https://%s/Autodiscover/Autodiscover.xml' % srv_host if self._redirect_url_is_valid(url=redirect_url): is_valid_response, ad = self._attempt_response(url=redirect_url) if is_valid_response: return self._step_5(ad=ad) return self._step_6() return self._step_6() def _step_5(self, ad): """Perform step 5. When a valid Autodiscover request succeeds, the following sequence occurs: * If the server responds with an HTTP 302 redirect, the client validates the redirection URL according to the process defined in the "Redirect responses" and then does one of the following: * If the redirection URL is valid, the client tries the URL and then does one of the following: * If the attempt succeeds, the client repeats step 5 from the beginning. * If the attempt fails, the client proceeds to step 6. * If the redirection URL is not valid, the client proceeds to step 6. * If the server responds with a valid Autodiscover response, the client does one of the following: * If the value of the Action element is "Redirect", the client gets the redirection email address from the Redirect element and then returns to step 1, using this new email address. * If the value of the Action element is "Settings", the client has successfully received the requested configuration settings for the specified user. The client does not need to proceed to step 6. :param ad: :return: """ log.info('Step 5: Checking response') if ad.response is None: # This is not explicit in the protocol, but let's raise errors here ad.raise_errors() ad_response = ad.response if ad_response.redirect_url: log.debug('Got a redirect URL: %s', ad_response.redirect_url) # We are diverging a bit from the protocol here. We will never get an HTTP 302 since earlier steps already # followed the redirects where possible. Instead, we handle retirect responses here. if self._redirect_url_is_valid(url=ad_response.redirect_url): is_valid_response, ad = self._attempt_response(url=ad_response.redirect_url) if is_valid_response: return self._step_5(ad=ad) return self._step_6() log.debug('Invalid redirect URL: %s', ad_response.redirect_url) return self._step_6() # This could be an email redirect. Let outer layer handle this return ad_response def _step_6(self): """Perform step 6. If the client cannot contact the Autodiscover service, the client should ask the user for the Exchange server name and use it to construct an Exchange EWS URL. The client should try to use this URL for future requests. """ raise AutoDiscoverFailed( 'All steps in the autodiscover protocol failed for email %r. If you think this is an error, consider doing ' 'an official test at https://testconnectivity.microsoft.com' % self.email) def _select_srv_host(srv_records): """Select the record with the highest priority, that also supports TLS. :param srv_records: :return: """ best_record = None for srv_record in srv_records: if srv_record.port != 443: log.debug('Skipping SRV record %r (no TLS)', srv_record) continue # Assume port 443 will serve TLS. If not, autodiscover will probably also be broken for others. if best_record is None or best_record.priority < srv_record.priority: best_record = srv_record if not best_record: raise ValueError('No suitable records') return best_record.srv exchangelib-4.6.1/exchangelib/autodiscover/properties.py000066400000000000000000000343561414601472700235420ustar00rootroot00000000000000from ..errors import ErrorNonExistentMailbox, AutoDiscoverFailed from ..fields import TextField, EmailAddressField, ChoiceField, Choice, EWSElementField, OnOffField, BooleanField, \ IntegerField, BuildField, ProtocolListField from ..properties import EWSElement from ..transport import DEFAULT_ENCODING, NOAUTH, NTLM, BASIC, GSSAPI, SSPI, CBA from ..util import create_element, add_xml_child, to_xml, is_xml, xml_to_str, AUTODISCOVER_REQUEST_NS, \ AUTODISCOVER_BASE_NS, AUTODISCOVER_RESPONSE_NS as RNS, ParseError class AutodiscoverBase(EWSElement): NAMESPACE = RNS class User(AutodiscoverBase): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/user-pox""" ELEMENT_NAME = 'User' display_name = TextField(field_uri='DisplayName', namespace=RNS) legacy_dn = TextField(field_uri='LegacyDN', namespace=RNS) deployment_id = TextField(field_uri='DeploymentId', namespace=RNS) # GUID format autodiscover_smtp_address = EmailAddressField(field_uri='AutoDiscoverSMTPAddress', namespace=RNS) class IntExtUrlBase(AutodiscoverBase): external_url = TextField(field_uri='ExternalUrl', namespace=RNS) internal_url = TextField(field_uri='InternalUrl', namespace=RNS) class AddressBook(IntExtUrlBase): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/addressbook-pox""" ELEMENT_NAME = 'AddressBook' class MailStore(IntExtUrlBase): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailstore-pox""" ELEMENT_NAME = 'MailStore' class NetworkRequirements(AutodiscoverBase): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/networkrequirements-pox""" ELEMENT_NAME = 'NetworkRequirements' ipv4_start = TextField(field_uri='IPv4Start', namespace=RNS) ipv4_end = TextField(field_uri='IPv4End', namespace=RNS) ipv6_start = TextField(field_uri='IPv6Start', namespace=RNS) ipv6_end = TextField(field_uri='IPv6End', namespace=RNS) class SimpleProtocol(AutodiscoverBase): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox Used for the 'Internal' and 'External' elements that may contain a stripped-down version of the Protocol element. """ ELEMENT_NAME = 'Protocol' WEB = 'WEB' EXCH = 'EXCH' EXPR = 'EXPR' EXHTTP = 'EXHTTP' TYPES = (WEB, EXCH, EXPR, EXHTTP) type = ChoiceField(field_uri='Type', choices={Choice(c) for c in TYPES}, namespace=RNS) as_url = TextField(field_uri='ASUrl', namespace=RNS) class IntExtBase(AutodiscoverBase): # TODO: 'OWAUrl' also has an AuthenticationMethod enum-style XML attribute with values: # WindowsIntegrated, FBA, NTLM, Digest, Basic owa_url = TextField(field_uri='OWAUrl', namespace=RNS) protocol = EWSElementField(value_cls=SimpleProtocol) class Internal(IntExtBase): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/internal-pox""" ELEMENT_NAME = 'Internal' class External(IntExtBase): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/external-pox""" ELEMENT_NAME = 'External' class Protocol(SimpleProtocol): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox""" # Attribute 'Type' is ignored here. Has a name conflict with the child element and does not seem useful. version = TextField(field_uri='Version', is_attribute=True, namespace=RNS) internal = EWSElementField(value_cls=Internal) external = EWSElementField(value_cls=External) ttl = IntegerField(field_uri='TTL', namespace=RNS, default=1) # TTL for this autodiscover response, in hours server = TextField(field_uri='Server', namespace=RNS) server_dn = TextField(field_uri='ServerDN', namespace=RNS) server_version = BuildField(field_uri='ServerVersion', namespace=RNS) mdb_dn = TextField(field_uri='MdbDN', namespace=RNS) public_folder_server = TextField(field_uri='PublicFolderServer', namespace=RNS) port = IntegerField(field_uri='Port', namespace=RNS, min=1, max=65535) directory_port = IntegerField(field_uri='DirectoryPort', namespace=RNS, min=1, max=65535) referral_port = IntegerField(field_uri='ReferralPort', namespace=RNS, min=1, max=65535) ews_url = TextField(field_uri='EwsUrl', namespace=RNS) emws_url = TextField(field_uri='EmwsUrl', namespace=RNS) sharing_url = TextField(field_uri='SharingUrl', namespace=RNS) ecp_url = TextField(field_uri='EcpUrl', namespace=RNS) ecp_url_um = TextField(field_uri='EcpUrl-um', namespace=RNS) ecp_url_aggr = TextField(field_uri='EcpUrl-aggr', namespace=RNS) ecp_url_mt = TextField(field_uri='EcpUrl-mt', namespace=RNS) ecp_url_ret = TextField(field_uri='EcpUrl-ret', namespace=RNS) ecp_url_sms = TextField(field_uri='EcpUrl-sms', namespace=RNS) ecp_url_publish = TextField(field_uri='EcpUrl-publish', namespace=RNS) ecp_url_photo = TextField(field_uri='EcpUrl-photo', namespace=RNS) ecp_url_tm = TextField(field_uri='EcpUrl-tm', namespace=RNS) ecp_url_tm_creating = TextField(field_uri='EcpUrl-tmCreating', namespace=RNS) ecp_url_tm_hiding = TextField(field_uri='EcpUrl-tmHiding', namespace=RNS) ecp_url_tm_editing = TextField(field_uri='EcpUrl-tmEditing', namespace=RNS) ecp_url_extinstall = TextField(field_uri='EcpUrl-extinstall', namespace=RNS) oof_url = TextField(field_uri='OOFUrl', namespace=RNS) oab_url = TextField(field_uri='OABUrl', namespace=RNS) um_url = TextField(field_uri='UMUrl', namespace=RNS) ews_partner_url = TextField(field_uri='EwsPartnerUrl', namespace=RNS) login_name = TextField(field_uri='LoginName', namespace=RNS) domain_required = OnOffField(field_uri='DomainRequired', namespace=RNS) domain_name = TextField(field_uri='DomainName', namespace=RNS) spa = OnOffField(field_uri='SPA', namespace=RNS, default=True) auth_package = ChoiceField(field_uri='AuthPackage', namespace=RNS, choices={ Choice(c) for c in ('basic', 'kerb', 'kerbntlm', 'ntlm', 'certificate', 'negotiate', 'nego2') }) cert_principal_name = TextField(field_uri='CertPrincipalName', namespace=RNS) ssl = OnOffField(field_uri='SSL', namespace=RNS, default=True) auth_required = OnOffField(field_uri='AuthRequired', namespace=RNS, default=True) use_pop_path = OnOffField(field_uri='UsePOPAuth', namespace=RNS) smtp_last = OnOffField(field_uri='SMTPLast', namespace=RNS, default=False) network_requirements = EWSElementField(value_cls=NetworkRequirements) address_book = EWSElementField(value_cls=AddressBook) mail_store = EWSElementField(value_cls=MailStore) @property def auth_type(self): # Translates 'auth_package' value to our own 'auth_type' enum vals if not self.auth_required: return NOAUTH return { # Missing in list are DIGEST and OAUTH2 'basic': BASIC, 'kerb': GSSAPI, 'kerbntlm': NTLM, # Means client can chose between NTLM and GSSAPI 'ntlm': NTLM, 'certificate': CBA, 'negotiate': SSPI, # Unsure about this one 'nego2': GSSAPI, 'anonymous': NOAUTH, # Seen in some docs even though it's not mentioned in MSDN }.get(self.auth_package.lower(), NTLM) # Default to NTLM class Error(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/error-pox""" ELEMENT_NAME = 'Error' NAMESPACE = AUTODISCOVER_BASE_NS id = TextField(field_uri='Id', namespace=AUTODISCOVER_BASE_NS, is_attribute=True) time = TextField(field_uri='Time', namespace=AUTODISCOVER_BASE_NS, is_attribute=True) code = TextField(field_uri='ErrorCode', namespace=AUTODISCOVER_BASE_NS) message = TextField(field_uri='Message', namespace=AUTODISCOVER_BASE_NS) debug_data = TextField(field_uri='DebugData', namespace=AUTODISCOVER_BASE_NS) class Account(AutodiscoverBase): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/account-pox""" ELEMENT_NAME = 'Account' REDIRECT_URL = 'redirectUrl' REDIRECT_ADDR = 'redirectAddr' SETTINGS = 'settings' ACTIONS = (REDIRECT_URL, REDIRECT_ADDR, SETTINGS) type = ChoiceField(field_uri='AccountType', namespace=RNS, choices={Choice('email')}) action = ChoiceField(field_uri='Action', namespace=RNS, choices={Choice(p) for p in ACTIONS}) microsoft_online = BooleanField(field_uri='MicrosoftOnline', namespace=RNS) redirect_url = TextField(field_uri='RedirectURL', namespace=RNS) redirect_address = EmailAddressField(field_uri='RedirectAddr', namespace=RNS) image = TextField(field_uri='Image', namespace=RNS) # Path to image used for branding service_home = TextField(field_uri='ServiceHome', namespace=RNS) # URL to website of ISP protocols = ProtocolListField() # 'SmtpAddress' is inside the 'PublicFolderInformation' element public_folder_smtp_address = TextField(field_uri='SmtpAddress', namespace=RNS) @classmethod def from_xml(cls, elem, account): kwargs = {} public_folder_information = elem.find('{%s}PublicFolderInformation' % cls.NAMESPACE) for f in cls.FIELDS: if f.name == 'public_folder_smtp_address': if public_folder_information is None: continue kwargs[f.name] = f.from_xml(elem=public_folder_information, account=account) continue kwargs[f.name] = f.from_xml(elem=elem, account=account) cls._clear(elem) return cls(**kwargs) class Response(AutodiscoverBase): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/response-pox""" ELEMENT_NAME = 'Response' user = EWSElementField(value_cls=User) account = EWSElementField(value_cls=Account) @property def redirect_address(self): try: if self.account.action != Account.REDIRECT_ADDR: return None return self.account.redirect_address except AttributeError: return None @property def redirect_url(self): try: if self.account.action != Account.REDIRECT_URL: return None return self.account.redirect_url except AttributeError: return None @property def autodiscover_smtp_address(self): # AutoDiscoverSMTPAddress might not be present in the XML. In this case, use the original email address. try: if self.account.action != Account.SETTINGS: return None return self.user.autodiscover_smtp_address except AttributeError: return None @property def ews_url(self): """Return the EWS URL contained in the response. A response may contain a number of possible protocol types. EXPR is meant for EWS. See https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16 We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available. Additionally, some responses may contain and EXPR with no EWS URL. In that case, return the URL from EXCH, if available. """ protocols = {p.type: p for p in self.account.protocols if p.ews_url} if Protocol.EXPR in protocols: return protocols[Protocol.EXPR].ews_url if Protocol.EXCH in protocols: return protocols[Protocol.EXCH].ews_url raise ValueError( 'No EWS URL found in any of the available protocols: %s' % [str(p) for p in self.account.protocols] ) class ErrorResponse(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/response-pox Like 'Response', but with a different namespace. """ ELEMENT_NAME = 'Response' NAMESPACE = AUTODISCOVER_BASE_NS error = EWSElementField(value_cls=Error) class Autodiscover(EWSElement): ELEMENT_NAME = 'Autodiscover' NAMESPACE = AUTODISCOVER_BASE_NS response = EWSElementField(value_cls=Response) error_response = EWSElementField(value_cls=ErrorResponse) @staticmethod def _clear(elem): # Parent implementation also clears the parent, but this element doesn't have one. elem.clear() @classmethod def from_bytes(cls, bytes_content): """Create an instance from response bytes. An Autodiscover request and response example is available at: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/pox-autodiscover-response-for-exchange :param bytes_content: :return: """ if not is_xml(bytes_content) and not is_xml(bytes_content, expected_prefix=b' dst_after: dt = dt_before if is_dst else dt_after elif dst_before < dst_after: dt = dt_after if is_dst else dt_before return dt def fromutc(self, dt): t = super().fromutc(dt) if isinstance(t, EWSDateTime): return t return EWSDateTime.from_datetime(t) # We want to return EWSDateTime objects UTC = EWSTimeZone('UTC') UTC_NOW = lambda: EWSDateTime.now(tz=UTC) # noqa: E731 exchangelib-4.6.1/exchangelib/extended_properties.py000066400000000000000000000313231414601472700227020ustar00rootroot00000000000000import logging from decimal import Decimal from .ewsdatetime import EWSDateTime from .properties import EWSElement, ExtendedFieldURI from .util import create_element, add_xml_child, get_xml_attrs, get_xml_attr, set_xml_value, value_to_xml_text, \ xml_text_to_value, is_iterable, TNS log = logging.getLogger(__name__) class ExtendedProperty(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedproperty""" ELEMENT_NAME = 'ExtendedProperty' # Enum values: https://docs.microsoft.com/en-us/dotnet/api/exchangewebservices.distinguishedpropertysettype DISTINGUISHED_SETS = { 'Address', 'Appointment', 'CalendarAssistant', 'Common', 'InternetHeaders', 'Meeting', 'PublicStrings', 'Sharing', 'Task', 'UnifiedMessaging', } # Enum values: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedfielduri PROPERTY_TYPES = { 'ApplicationTime', 'Binary', 'BinaryArray', 'Boolean', 'CLSID', 'CLSIDArray', 'Currency', 'CurrencyArray', 'Double', 'DoubleArray', # 'Error', 'Float', 'FloatArray', 'Integer', 'IntegerArray', 'Long', 'LongArray', # 'Null', # 'Object', # 'ObjectArray', 'Short', 'ShortArray', 'SystemTime', 'SystemTimeArray', 'String', 'StringArray', } # The commented-out types cannot be used for setting or getting (see docs) and are thus not very useful here # Translation table between common distinguished_property_set_id and property_set_id values. See # https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/commonly-used-property-sets # ID values must be lowercase. DISTINGUISHED_SET_NAME_TO_ID_MAP = { 'Address': '00062004-0000-0000-c000-000000000046', 'AirSync': '71035549-0739-4dcb-9163-00f0580dbbdf', 'Appointment': '00062002-0000-0000-c000-000000000046', 'Common': '00062008-0000-0000-c000-000000000046', 'InternetHeaders': '00020386-0000-0000-c000-000000000046', 'Log': '0006200a-0000-0000-c000-000000000046', 'Mapi': '00020328-0000-0000-c000-000000000046', 'Meeting': '6ed8da90-450b-101b-98da-00aa003f1305', 'Messaging': '41f28f13-83f4-4114-a584-eedb5a6b0bff', 'Note': '0006200e-0000-0000-c000-000000000046', 'PostRss': '00062041-0000-0000-c000-000000000046', 'PublicStrings': '00020329-0000-0000-c000-000000000046', 'Remote': '00062014-0000-0000-c000-000000000046', 'Report': '00062013-0000-0000-c000-000000000046', 'Sharing': '00062040-0000-0000-c000-000000000046', 'Task': '00062003-0000-0000-c000-000000000046', 'UnifiedMessaging': '4442858e-a9e3-4e80-b900-317a210cc15b', } DISTINGUISHED_SET_ID_TO_NAME_MAP = {v: k for k, v in DISTINGUISHED_SET_NAME_TO_ID_MAP.items()} distinguished_property_set_id = None property_set_id = None property_tag = None # hex integer (e.g. 0x8000) or string ('0x8000') property_name = None property_id = None # integer as hex-formatted int (e.g. 0x8000) or normal int (32768) property_type = '' __slots__ = 'value', def __init__(self, *args, **kwargs): if not kwargs: # Allow to set attributes without keyword kwargs = dict(zip(self._slots_keys, args)) self.value = kwargs.pop('value') super().__init__(**kwargs) @classmethod def validate_cls(cls): # Validate values of class attributes and their inter-dependencies cls._validate_distinguished_property_set_id() cls._validate_property_set_id() cls._validate_property_tag() cls._validate_property_name() cls._validate_property_id() cls._validate_property_type() @classmethod def _validate_distinguished_property_set_id(cls): if cls.distinguished_property_set_id: if any([cls.property_set_id, cls.property_tag]): raise ValueError( "When 'distinguished_property_set_id' is set, 'property_set_id' and 'property_tag' must be None" ) if not any([cls.property_id, cls.property_name]): raise ValueError( "When 'distinguished_property_set_id' is set, 'property_id' or 'property_name' must also be set" ) if cls.distinguished_property_set_id not in cls.DISTINGUISHED_SETS: raise ValueError( "'distinguished_property_set_id' %r must be one of %s" % (cls.distinguished_property_set_id, sorted(cls.DISTINGUISHED_SETS)) ) @classmethod def _validate_property_set_id(cls): if cls.property_set_id: if any([cls.distinguished_property_set_id, cls.property_tag]): raise ValueError( "When 'property_set_id' is set, 'distinguished_property_set_id' and 'property_tag' must be None" ) if not any([cls.property_id, cls.property_name]): raise ValueError( "When 'property_set_id' is set, 'property_id' or 'property_name' must also be set" ) @classmethod def _validate_property_tag(cls): if cls.property_tag: if any([ cls.distinguished_property_set_id, cls.property_set_id, cls.property_name, cls.property_id ]): raise ValueError("When 'property_tag' is set, only 'property_type' must be set") if 0x8000 <= cls.property_tag_as_int() <= 0xFFFE: raise ValueError( "'property_tag' value '%s' is reserved for custom properties" % cls.property_tag_as_hex() ) @classmethod def _validate_property_name(cls): if cls.property_name: if any([cls.property_id, cls.property_tag]): raise ValueError("When 'property_name' is set, 'property_id' and 'property_tag' must be None") if not any([cls.distinguished_property_set_id, cls.property_set_id]): raise ValueError( "When 'property_name' is set, 'distinguished_property_set_id' or 'property_set_id' must also be set" ) @classmethod def _validate_property_id(cls): if cls.property_id: if any([cls.property_name, cls.property_tag]): raise ValueError("When 'property_id' is set, 'property_name' and 'property_tag' must be None") if not any([cls.distinguished_property_set_id, cls.property_set_id]): raise ValueError( "When 'property_id' is set, 'distinguished_property_set_id' or 'property_set_id' must also be set" ) @classmethod def _validate_property_type(cls): if cls.property_type not in cls.PROPERTY_TYPES: raise ValueError( "'property_type' %r must be one of %s" % (cls.property_type, sorted(cls.PROPERTY_TYPES)) ) def clean(self, version=None): self.validate_cls() python_type = self.python_type() if self.is_array_type(): if not is_iterable(self.value): raise ValueError("'%s' value %r must be a list" % (self.__class__.__name__, self.value)) for v in self.value: if not isinstance(v, python_type): raise TypeError( "'%s' value element %r must be an instance of %s" % (self.__class__.__name__, v, python_type)) else: if not isinstance(self.value, python_type): raise TypeError( "'%s' value %r must be an instance of %s" % (self.__class__.__name__, self.value, python_type)) @classmethod def _normalize_obj(cls, obj): # Sometimes, EWS will helpfully translate a 'distinguished_property_set_id' value to a 'property_set_id' value # and vice versa. Align these values on an ExtendedFieldURI instance. try: obj.property_set_id = cls.DISTINGUISHED_SET_NAME_TO_ID_MAP[obj.distinguished_property_set_id] except KeyError: try: obj.distinguished_property_set_id = cls.DISTINGUISHED_SET_ID_TO_NAME_MAP[obj.property_set_id] except KeyError: pass return obj @classmethod def is_property_instance(cls, elem): """Return whether an 'ExtendedProperty' element matches the definition for this class. Extended property fields do not have a name, so we must match on the cls.property_* attributes to match a field in the request with a field in the response. """ # We can't use ExtendedFieldURI.from_xml(). It clears the XML element but we may not want to consume it here. kwargs = { f.name: f.from_xml(elem=elem.find(ExtendedFieldURI.response_tag()), account=None) for f in ExtendedFieldURI.FIELDS } xml_obj = ExtendedFieldURI(**kwargs) cls_obj = cls.as_object() return cls._normalize_obj(cls_obj) == cls._normalize_obj(xml_obj) @classmethod def from_xml(cls, elem, account): # Gets value of this specific ExtendedProperty from a list of 'ExtendedProperty' XML elements python_type = cls.python_type() if cls.is_array_type(): values = elem.find('{%s}Values' % TNS) return [ xml_text_to_value(value=val, value_type=python_type) for val in get_xml_attrs(values, '{%s}Value' % TNS) ] extended_field_value = xml_text_to_value(value=get_xml_attr(elem, '{%s}Value' % TNS), value_type=python_type) if python_type == str and not extended_field_value: # For string types, we want to return the empty string instead of None if the element was # actually found, but there was no XML value. For other types, it would be more problematic # to make that distinction, e.g. return False for bool, 0 for int, etc. return '' return extended_field_value def to_xml(self, version): if self.is_array_type(): values = create_element('t:Values') for v in self.value: add_xml_child(values, 't:Value', v) return values return set_xml_value(create_element('t:Value'), self.value, version=version) @classmethod def is_array_type(cls): return cls.property_type.endswith('Array') @classmethod def property_tag_as_int(cls): if isinstance(cls.property_tag, str): return int(cls.property_tag, base=16) return cls.property_tag @classmethod def property_tag_as_hex(cls): return hex(cls.property_tag) if isinstance(cls.property_tag, int) else cls.property_tag @classmethod def python_type(cls): # Return the best equivalent for a Python type for the property type of this class base_type = cls.property_type[:-5] if cls.is_array_type() else cls.property_type return { 'ApplicationTime': Decimal, 'Binary': bytes, 'Boolean': bool, 'CLSID': str, 'Currency': int, 'Double': Decimal, 'Float': Decimal, 'Integer': int, 'Long': int, 'Short': int, 'SystemTime': EWSDateTime, 'String': str, }[base_type] @classmethod def as_object(cls): # Return an object we can use to match with the incoming object from XML return ExtendedFieldURI( distinguished_property_set_id=cls.distinguished_property_set_id, property_set_id=cls.property_set_id.lower() if cls.property_set_id else None, property_tag=cls.property_tag_as_hex(), property_name=cls.property_name, property_id=value_to_xml_text(cls.property_id) if cls.property_id else None, property_type=cls.property_type, ) class ExternId(ExtendedProperty): """This is a custom extended property defined by us. It's useful for synchronization purposes, to attach a unique ID from an external system. """ property_set_id = 'c11ff724-aa03-4555-9952-8fa248a11c3e' # This is arbitrary. We just want a unique UUID. property_name = 'External ID' property_type = 'String' class Flag(ExtendedProperty): """This property returns None for Not Flagged messages, 1 for Completed messages and 2 for Flagged messages. For a description of each status, see: https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxoflag/eda9fd25-6407-4cec-9e62-26e4f9d6a098 """ property_tag = 0x1090 property_type = 'Integer' exchangelib-4.6.1/exchangelib/fields.py000066400000000000000000001713241414601472700201020ustar00rootroot00000000000000import abc import datetime import logging from collections import OrderedDict from decimal import Decimal, InvalidOperation from importlib import import_module from .ewsdatetime import EWSDateTime, EWSDate, EWSTimeZone, NaiveDateTimeNotAllowed, UnknownTimeZone, UTC from .util import create_element, get_xml_attr, get_xml_attrs, set_xml_value, value_to_xml_text, is_iterable, \ xml_text_to_value, TNS from .version import Build, Version, EXCHANGE_2013 log = logging.getLogger(__name__) # DayOfWeekIndex enum. See # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/dayofweekindex FIRST = 'First' SECOND = 'Second' THIRD = 'Third' FOURTH = 'Fourth' LAST = 'Last' WEEK_NUMBERS = (FIRST, SECOND, THIRD, FOURTH, LAST) # Month enum JANUARY = 'January' FEBRUARY = 'February' MARCH = 'March' APRIL = 'April' MAY = 'May' JUNE = 'June' JULY = 'July' AUGUST = 'August' SEPTEMBER = 'September' OCTOBER = 'October' NOVEMBER = 'November' DECEMBER = 'December' MONTHS = (JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER) # Weekday enum MONDAY = 'Monday' TUESDAY = 'Tuesday' WEDNESDAY = 'Wednesday' THURSDAY = 'Thursday' FRIDAY = 'Friday' SATURDAY = 'Saturday' SUNDAY = 'Sunday' WEEKDAY_NAMES = (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) # Used for weekday recurrences except weekly recurrences. E.g. for "First WeekendDay in March" DAY = 'Day' WEEK_DAY = 'Weekday' # Non-weekend day WEEKEND_DAY = 'WeekendDay' EXTRA_WEEKDAY_OPTIONS = (DAY, WEEK_DAY, WEEKEND_DAY) # DaysOfWeek enum: See # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/daysofweek-daysofweektype WEEKDAYS = WEEKDAY_NAMES + EXTRA_WEEKDAY_OPTIONS class InvalidField(ValueError): """Used when a field name does not match any defined fields.""" class InvalidFieldForVersion(ValueError): """Used when a field is not supported on the given Exchnage version.""" class InvalidChoiceForVersion(ValueError): """Used when a value is not valid for an enum-type field.""" def split_field_path(field_path): """Split a string path into its field, label and subfield parts. :param field_path: :return Examples: 'start' -> ('start', None, None) 'phone_numbers__PrimaryPhone' -> ('phone_numbers', 'PrimaryPhone', None) 'physical_addresses__Home__street' -> ('physical_addresses', 'Home', 'street') """ if not isinstance(field_path, str): raise ValueError("Field path %r must be a string" % field_path) search_parts = field_path.split('__') field = search_parts[0] try: label = search_parts[1] except IndexError: label = None try: subfield = search_parts[2] except IndexError: subfield = None return field, label, subfield def resolve_field_path(field_path, folder, strict=True): """Take the name of a field, or '__'-delimited path to a subfield, and return the corresponding Field object, label and SubField object. """ from .indexed_properties import SingleFieldIndexedElement, MultiFieldIndexedElement fieldname, label, subfieldname = split_field_path(field_path) field = folder.get_item_field_by_fieldname(fieldname) subfield = None if isinstance(field, IndexedField): if strict and not label: raise ValueError( "IndexedField path '%s' must specify label, e.g. '%s__%s'" % (field_path, fieldname, field.value_cls.get_field_by_fieldname('label').default) ) valid_labels = field.value_cls.get_field_by_fieldname('label').supported_choices( version=folder.account.version ) if label and label not in valid_labels: raise ValueError( "Label '%s' on IndexedField path '%s' must be one of %s" % (label, field_path, ', '.join(valid_labels)) ) if issubclass(field.value_cls, MultiFieldIndexedElement): if strict and not subfieldname: raise ValueError( "IndexedField path '%s' must specify subfield, e.g. '%s__%s__%s'" % (field_path, fieldname, label, field.value_cls.FIELDS[1].name) ) if subfieldname: try: subfield = field.value_cls.get_field_by_fieldname(subfieldname) except ValueError: fnames = ', '.join(f.name for f in field.value_cls.supported_fields( version=folder.account.version )) raise ValueError( "Subfield '%s' on IndexedField path '%s' must be one of %s" % (subfieldname, field_path, fnames) ) else: if not issubclass(field.value_cls, SingleFieldIndexedElement): raise ValueError("'field.value_cls' %r must be an SingleFieldIndexedElement instance" % field.value_cls) if subfieldname: raise ValueError( "IndexedField path '%s' must not specify subfield, e.g. just '%s__%s'" % (field_path, fieldname, label) ) subfield = field.value_cls.value_field(version=folder.account.version) else: if label or subfieldname: raise ValueError( "Field path '%s' must not specify label or subfield, e.g. just '%s'" % (field_path, fieldname) ) return field, label, subfield class FieldPath: """Holds values needed to point to a single field. For indexed properties, we allow setting either field, field and label, or field, label and subfield. This allows pointing to either the full indexed property set, a property with a specific label, or a particular subfield field on that property. """ def __init__(self, field, label=None, subfield=None): # 'label' and 'subfield' are only used for IndexedField fields if not isinstance(field, (FieldURIField, ExtendedPropertyField)): raise ValueError("'field' %r must be an FieldURIField, of ExtendedPropertyField instance" % field) if label and not isinstance(label, str): raise ValueError("'label' %r must be a %s instance" % (label, str)) if subfield and not isinstance(subfield, SubField): raise ValueError("'subfield' %r must be a SubField instance" % subfield) self.field = field self.label = label self.subfield = subfield @classmethod def from_string(cls, field_path, folder, strict=False): field, label, subfield = resolve_field_path(field_path, folder=folder, strict=strict) return cls(field=field, label=label, subfield=subfield) def get_value(self, item): # For indexed properties, get either the full property set, the property with matching label, or a particular # subfield. if self.label: for subitem in getattr(item, self.field.name): if subitem.label == self.label: if self.subfield: return getattr(subitem, self.subfield.name) return subitem return None # No item with this label return getattr(item, self.field.name) def get_sort_value(self, item): # For fields that allow values of different types, we need to return a value that is val = self.get_value(item) if isinstance(self.field, DateOrDateTimeField) and isinstance(val, EWSDate): return item.date_to_datetime(field_name=self.field.name) return val def to_xml(self): if isinstance(self.field, IndexedField): if not self.label or not self.subfield: raise ValueError("Field path for indexed field '%s' is missing label and/or subfield" % self.field.name) return self.subfield.field_uri_xml(field_uri=self.field.field_uri, label=self.label) return self.field.field_uri_xml() def expand(self, version): # If this path does not point to a specific subfield on an indexed property, return all the possible path # combinations for this field path. if isinstance(self.field, IndexedField): labels = [self.label] if self.label \ else self.field.value_cls.get_field_by_fieldname('label').supported_choices(version=version) subfields = [self.subfield] if self.subfield else self.field.value_cls.supported_fields(version=version) for label in labels: for subfield in subfields: yield FieldPath(field=self.field, label=label, subfield=subfield) else: yield self @property def path(self): if self.label: from .indexed_properties import SingleFieldIndexedElement if issubclass(self.field.value_cls, SingleFieldIndexedElement) or not self.subfield: return '%s__%s' % (self.field.name, self.label) return '%s__%s__%s' % (self.field.name, self.label, self.subfield.name) return self.field.name def __eq__(self, other): return hash(self) == hash(other) def __str__(self): return self.path def __repr__(self): return self.__class__.__name__ + repr((self.field, self.label, self.subfield)) def __hash__(self): return hash((self.field, self.label, self.subfield)) class FieldOrder: """Holds values needed to call server-side sorting on a single field path.""" def __init__(self, field_path, reverse=False): if not isinstance(field_path, FieldPath): raise ValueError("'field_path' %r must be a FieldPath instance" % field_path) if not isinstance(reverse, bool): raise ValueError("'reverse' %r must be a boolean" % reverse) self.field_path = field_path self.reverse = reverse @classmethod def from_string(cls, field_path, folder): return cls( field_path=FieldPath.from_string(field_path=field_path.lstrip('-'), folder=folder, strict=True), reverse=field_path.startswith('-') ) def to_xml(self): field_order = create_element('t:FieldOrder', attrs=dict(Order='Descending' if self.reverse else 'Ascending')) field_order.append(self.field_path.to_xml()) return field_order class Field(metaclass=abc.ABCMeta): """Holds information related to an item field.""" value_cls = None is_list = False # Is the field a complex EWS type? Quoting the EWS FindItem docs: # # The FindItem operation returns only the first 512 bytes of any streamable property. For Unicode, it returns # the first 255 characters by using a null-terminated Unicode string. It does not return any of the message # body formats or the recipient lists. # is_complex = False def __init__(self, name=None, is_required=False, is_required_after_save=False, is_read_only=False, is_read_only_after_send=False, is_searchable=True, is_attribute=False, default=None, supported_from=None, deprecated_from=None): self.name = name self.default = default # Default value if none is given self.is_required = is_required # Some fields cannot be deleted on update. Default to True if 'is_required' is set self.is_required_after_save = is_required or is_required_after_save self.is_read_only = is_read_only # Set this for fields that raise ErrorInvalidPropertyUpdateSentMessage on update after send. Default to True # if 'is_read_only' is set self.is_read_only_after_send = is_read_only or is_read_only_after_send # Define whether the field can be used in a QuerySet. For some reason, EWS disallows searching on some fields, # instead throwing ErrorInvalidValueForProperty self.is_searchable = is_searchable # When true, this field is treated as an XML attribute instead of an element self.is_attribute = is_attribute # The Exchange build when this field was introduced. When talking with versions prior to this version, # we will ignore this field. if supported_from is not None and not isinstance(supported_from, Build): raise ValueError("'supported_from' %r must be a Build instance" % supported_from) self.supported_from = supported_from # The Exchange build when this field was deprecated. When talking with versions at or later than this version, # we will ignore this field. if deprecated_from is not None and not isinstance(deprecated_from, Build): raise ValueError("'deprecated_from' %r must be a Build instance" % deprecated_from) self.deprecated_from = deprecated_from def clean(self, value, version=None): if version and not self.supports_version(version): raise InvalidFieldForVersion("Field '%s' does not support EWS builds prior to %s (server has %s)" % ( self.name, self.supported_from, version)) if value is None: if self.is_required and self.default is None: raise ValueError("'%s' is a required field with no default" % self.name) return self.default if self.is_list: if not is_iterable(value): raise ValueError("Field '%s' value %r must be a list" % (self.name, value)) for v in value: if not isinstance(v, self.value_cls): raise TypeError("Field '%s' value %r must be of type %s" % (self.name, v, self.value_cls)) if hasattr(v, 'clean'): v.clean(version=version) else: if not isinstance(value, self.value_cls): raise TypeError("Field '%s' value %r must be of type %s" % (self.name, value, self.value_cls)) if hasattr(value, 'clean'): value.clean(version=version) return value @abc.abstractmethod def from_xml(self, elem, account): pass @abc.abstractmethod def to_xml(self, value, version): pass def supports_version(self, version): # 'version' is a Version instance, for convenience by callers if not isinstance(version, Version): raise ValueError("'version' %r must be a Version instance" % version) if self.supported_from and version.build < self.supported_from: return False if self.deprecated_from and version.build >= self.deprecated_from: return False return True def __eq__(self, other): return hash(self) == hash(other) @abc.abstractmethod def __hash__(self): pass def __repr__(self): return self.__class__.__name__ + '(%s)' % ', '.join('%s=%r' % (f, getattr(self, f)) for f in ( 'name', 'value_cls', 'is_list', 'is_complex', 'default')) class FieldURIField(Field): """A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be 'itemtype:FieldName' """ def __init__(self, *args, **kwargs): self.field_uri = kwargs.pop('field_uri', None) self.namespace = kwargs.pop('namespace', TNS) super().__init__(*args, **kwargs) # See all valid FieldURI values at # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/fielduri # The field_uri has a prefix when the FieldURI points to an Item field. if self.field_uri is None: self.field_uri_postfix = None elif ':' in self.field_uri: self.field_uri_postfix = self.field_uri.split(':')[1] else: self.field_uri_postfix = self.field_uri def _get_val_from_elem(self, elem): if self.is_attribute: return elem.get(self.field_uri) or None return get_xml_attr(elem, self.response_tag()) def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None: try: return xml_text_to_value(val, self.value_cls) except (ValueError, InvalidOperation): log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls) return None return self.default def to_xml(self, value, version): field_elem = create_element(self.request_tag()) return set_xml_value(field_elem, value, version=version) def field_uri_xml(self): from .properties import FieldURI if not self.field_uri: raise ValueError("'field_uri' value is missing") return FieldURI(field_uri=self.field_uri).to_xml(version=None) def request_tag(self): if not self.field_uri_postfix: raise ValueError("'field_uri_postfix' value is missing") return 't:%s' % self.field_uri_postfix def response_tag(self): if not self.field_uri_postfix: raise ValueError("'field_uri_postfix' value is missing") return '{%s}%s' % (self.namespace, self.field_uri_postfix) def __hash__(self): return hash(self.field_uri) class BooleanField(FieldURIField): """A field that handles boolean values.""" value_cls = bool class OnOffField(BooleanField): """A field that handles boolean values that are On/Off instead of True/False.""" class IntegerField(FieldURIField): """A field that handles integer values.""" value_cls = int def __init__(self, *args, **kwargs): self.min = kwargs.pop('min', None) self.max = kwargs.pop('max', None) super().__init__(*args, **kwargs) def _clean_single_value(self, v): if self.min is not None and v < self.min: raise ValueError( "Value %r on field '%s' must be greater than %s" % (v, self.name, self.min)) if self.max is not None and v > self.max: raise ValueError("Value %r on field '%s' must be less than %s" % (v, self.name, self.max)) def clean(self, value, version=None): value = super().clean(value, version=version) if value is not None: if self.is_list: for v in value: self._clean_single_value(v) else: self._clean_single_value(value) return value class DecimalField(IntegerField): """A field that handles decimal values.""" value_cls = Decimal class EnumField(IntegerField): """A field type where you can enter either the 1-based index in an enum (tuple), or the enum value. Values will be stored internally as integers but output in XML as strings. """ def __init__(self, *args, **kwargs): self.enum = kwargs.pop('enum') # Set different min/max defaults than IntegerField if 'max' in kwargs: raise AttributeError("EnumField does not support the 'max' attribute") kwargs['min'] = kwargs.pop('min', 1) kwargs['max'] = kwargs['min'] + len(self.enum) - 1 super().__init__(*args, **kwargs) def clean(self, value, version=None): if self.is_list: value = list(value) # Convert to something we can index for i, v in enumerate(value): if isinstance(v, str): if v not in self.enum: raise ValueError( "List value '%s' on field '%s' must be one of %s" % (v, self.name, self.enum)) value[i] = self.enum.index(v) + 1 if not value: raise ValueError("Value '%s' on field '%s' must not be empty" % (value, self.name)) if len(value) > len(set(value)): raise ValueError("List entries '%s' on field '%s' must be unique" % (value, self.name)) else: if isinstance(value, str): if value not in self.enum: raise ValueError( "Value '%s' on field '%s' must be one of %s" % (value, self.name, self.enum)) value = self.enum.index(value) + 1 return super().clean(value, version=version) def as_string(self, value): # Converts an integer in the enum to its equivalent string if isinstance(value, str): return value if self.is_list: return [self.enum[v - 1] for v in sorted(value)] return self.enum[value - 1] def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None: try: if self.is_list: return [self.enum.index(v) + 1 for v in val.split(' ')] return self.enum.index(val) + 1 except ValueError: log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls) return None return self.default def to_xml(self, value, version): field_elem = create_element(self.request_tag()) if self.is_list: return set_xml_value(field_elem, ' '.join(self.as_string(value)), version=version) return set_xml_value(field_elem, self.as_string(value), version=version) class EnumListField(EnumField): """Like EnumField, but for lists of enum values.""" is_list = True class EnumAsIntField(EnumField): """Like EnumField, but communicates values with EWS in integers.""" def from_xml(self, elem, account): return super(EnumField, self).from_xml(elem=elem, account=account) def to_xml(self, value, version): field_elem = create_element(self.request_tag()) return set_xml_value(field_elem, value, version=version) class AppointmentStateField(IntegerField): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/appointmentstate""" NONE = 'None' MEETING = 'Meeting' RECEIVED = 'Received' CANCELLED = 'Cancelled' STATES = { NONE: 0x0000, MEETING: 0x0001, RECEIVED: 0x0002, CANCELLED: 0x0004, } def from_xml(self, elem, account): val = super().from_xml(elem=elem, account=account) if val is None: return val return tuple(name for name, mask in self.STATES.items() if bool(val & mask)) class Base64Field(FieldURIField): """A field that handles binary data and automatically Base64 encodes and decodes the data.""" value_cls = bytes is_complex = True def __init__(self, *args, **kwargs): if 'is_searchable' not in kwargs: kwargs['is_searchable'] = False super().__init__(*args, **kwargs) class MimeContentField(Base64Field): """Like Base64Field. This element has an optional 'CharacterSet' attribute, but it specifies the encoding of the base64-encoded string (which doesn't make sense since base64-encoded strings are always ASCII). We ignore it here because the decoded data could be in some other encoding, specified in the "Content-Type" HTTP header. """ class DateField(FieldURIField): """A field that handles date values.""" value_cls = EWSDate def clean(self, value, version=None): # Allow plain datetime.date values as input if type(value) is datetime.date: value = self.value_cls.from_date(value) return super().clean(value=value, version=version) class DateTimeBackedDateField(DateField): """A field that acts like a date, but where values are sent to EWS as EWSDateTime.""" def __init__(self, *args, **kwargs): # Not all fields assume a default time of 00:00, so make this configurable self._default_time = kwargs.pop('default_time', datetime.time(0, 0)) super().__init__(*args, **kwargs) # Create internal field to handle datetime-only logic self._datetime_field = DateTimeField(*args, **kwargs) def date_to_datetime(self, value): return self._datetime_field.value_cls.combine(value, self._default_time).replace(tzinfo=UTC) def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None and len(val) == 25: # This is a datetime string with timezone info, e.g. '2021-03-01T21:55:54+00:00'. We don't want to have # datetime values converted to UTC before converting to date. EWSDateTime.from_string() insists on # converting to UTC, but we don't have an EWSTimeZone we can convert the timezone info to. Instead, parse # the string with .fromisoformat(). return datetime.datetime.fromisoformat(val).date() # Revert to default parsing of datetime strings res = self._datetime_field.from_xml(elem=elem, account=account) if res is None: return res return res.date() def to_xml(self, value, version): # Convert date to datetime value = self.date_to_datetime(value) return self._datetime_field.to_xml(value=value, version=version) class TimeField(FieldURIField): """A field that handles time values.""" value_cls = datetime.time def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None: try: if ':' in val: # Assume a string of the form HH:MM:SS return datetime.datetime.strptime(val, '%H:%M:%S').time() # Assume an integer in minutes since midnight return (datetime.datetime(2000, 1, 1) + datetime.timedelta(minutes=int(val))).time() except ValueError: pass return self.default class DateTimeField(FieldURIField): """A field that handles datetime values.""" value_cls = EWSDateTime def clean(self, value, version=None): if isinstance(value, datetime.datetime): if not value.tzinfo: raise ValueError("Value '%s' on field '%s' must be timezone aware" % (value, self.name)) if type(value) is datetime.datetime: value = self.value_cls.from_datetime(value) return super().clean(value, version=version) def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None: try: return self.value_cls.from_string(val) except ValueError as e: if isinstance(e, NaiveDateTimeNotAllowed): # We encountered a naive datetime if account: # Convert to timezone-aware datetime using the default timezone of the account tz = account.default_timezone log.info('Found naive datetime %s on field %s. Assuming timezone %s', e.local_dt, self.name, tz) return e.local_dt.replace(tzinfo=tz) # There's nothing we can do but return the naive date. It's better than assuming e.g. UTC. log.warning('Returning naive datetime %s on field %s', e.local_dt, self.name) return e.local_dt log.info("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls) return None return self.default class DateOrDateTimeField(DateTimeField): """This field can handle both EWSDate and EWSDateTime. Used for calendar items where 'start' and 'end' values are conceptually dates when the calendar item is an all-day event, but datetimes in all other cases, and for recurrences where the returned 'start' and 'end' values may be either dates or datetimes depending on whether the recurring item is a task or a calendar item. For all-day calendar items, we assume both start and end dates are inclusive. For filtering kwarg validation and other places where we must decide on a specific class, we settle on datetime. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Create internal field to handle date-only logic self._date_field = DateField(*args, **kwargs) def clean(self, value, version=None): # Most calendar items will contain datetime values. We can't access the is_all_day value here, so CalendarItem # must handle that sanity check. if type(value) in (EWSDate, datetime.date): return self._date_field.clean(value=value, version=version) return super().clean(value=value, version=version) def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None and len(val) == 16: # This is a date format with timezone info, as sent by task recurrences. Eg: '2006-01-09+01:00' return self._date_field.from_xml(elem=elem, account=account) return super().from_xml(elem=elem, account=account) class TimeZoneField(FieldURIField): """A field that handles timezone values.""" value_cls = EWSTimeZone def clean(self, value, version=None): # Allow other timezone implementations as input if value is not None: value = self.value_cls.from_timezone(value) return super().clean(value=value, version=version) def from_xml(self, elem, account): field_elem = elem.find(self.response_tag()) if field_elem is not None: ms_id = field_elem.get('Id') ms_name = field_elem.get('Name') try: return self.value_cls.from_ms_id(ms_id or ms_name) except UnknownTimeZone: log.warning( "Cannot convert value '%s' on field '%s' to type %s (unknown timezone ID)", (ms_id or ms_name), self.name, self.value_cls ) return None return self.default def to_xml(self, value, version): attrs = OrderedDict([('Id', value.ms_id)]) if value.ms_name: attrs['Name'] = value.ms_name return create_element(self.request_tag(), attrs=attrs) class TextField(FieldURIField): """A field that stores a string value with no length limit.""" value_cls = str is_complex = True class TextListField(TextField): """Like TextField, but for lists of text.""" is_list = True def from_xml(self, elem, account): iter_elem = elem.find(self.response_tag()) if iter_elem is not None: return get_xml_attrs(iter_elem, '{%s}String' % TNS) return self.default class MessageField(TextField): """A field that handles the Message element.""" INNER_ELEMENT_NAME = 'Message' def from_xml(self, elem, account): reply = elem.find(self.response_tag()) if reply is None: return None message = reply.find('{%s}%s' % (TNS, self.INNER_ELEMENT_NAME)) if message is None: return None return message.text def to_xml(self, value, version): field_elem = create_element(self.request_tag()) message = create_element('t:%s' % self.INNER_ELEMENT_NAME) message.text = value return set_xml_value(field_elem, message, version=version) class CharField(TextField): """A field that stores a string value with a limited length.""" is_complex = False def __init__(self, *args, **kwargs): self.max_length = kwargs.pop('max_length', 255) if not 1 <= self.max_length <= 255: # A field supporting messages longer than 255 chars should be TextField raise ValueError("'max_length' must be in the range 1-255") super().__init__(*args, **kwargs) def clean(self, value, version=None): value = super().clean(value, version=version) if value is not None: if self.is_list: for v in value: if len(v) > self.max_length: raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, v, self.max_length)) else: if len(value) > self.max_length: raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, value, self.max_length)) return value class IdField(CharField): """A field to hold the 'Id' and 'Changekey' attributes on 'ItemId' type items. There is no guaranteed max length, but we can assume 512 bytes in practice. See https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/ews-identifiers-in-exchange """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.max_length = 512 # This is above the normal 255 limit, but this is actually an attribute, not a field self.is_searchable = False self.is_attribute = True class CharListField(CharField): """Like CharField, but for lists of strings.""" is_list = True def __init__(self, *args, **kwargs): self.list_elem_name = kwargs.pop('list_elem_name', 'String') super().__init__(*args, **kwargs) def list_elem_tag(self): return '{%s}%s' % (self.namespace, self.list_elem_name) def from_xml(self, elem, account): iter_elem = elem.find(self.response_tag()) if iter_elem is not None: return get_xml_attrs(iter_elem, self.list_elem_tag()) return self.default class URIField(TextField): """Helper to mark strings that must conform to xsd:anyURI. If we want an URI validator, see http://stackoverflow.com/questions/14466585/is-this-regex-correct-for-xsdanyuri """ class EmailAddressField(CharField): """A helper class used for email address string that we can use for email validation.""" class CultureField(CharField): """Helper to mark strings that are # RFC 1766 culture values.""" class Choice: """Implement versioned choices for the ChoiceField field.""" def __init__(self, value, supported_from=None): self.value = value self.supported_from = supported_from def supports_version(self, version): # 'version' is a Version instance, for convenience by callers if not isinstance(version, Version): raise ValueError("'version' %r must be a Version instance" % version) if not self.supported_from: return True return version.build >= self.supported_from class ChoiceField(CharField): """Like CharField, but restricts the value to a limited set of strings.""" def __init__(self, *args, **kwargs): self.choices = kwargs.pop('choices') super().__init__(*args, **kwargs) def clean(self, value, version=None): value = super().clean(value, version=version) if value is None: return None valid_choices = [c.value for c in self.choices] if version: valid_choices_for_version = self.supported_choices(version=version) if value in valid_choices_for_version: return value if value in valid_choices: raise InvalidChoiceForVersion("Choice '%s' only supports EWS builds from %s to %s (server has %s)" % ( self.name, self.supported_from or '*', self.deprecated_from or '*', version)) else: if value in valid_choices: return value raise ValueError("Invalid choice '%s' for field '%s'. Valid choices are: %s" % ( value, self.name, ', '.join(valid_choices) )) def supported_choices(self, version): return [c.value for c in self.choices if c.supports_version(version)] FREE_BUSY_CHOICES = [Choice('Free'), Choice('Tentative'), Choice('Busy'), Choice('OOF'), Choice('NoData'), Choice('WorkingElsewhere', supported_from=EXCHANGE_2013)] class FreeBusyStatusField(ChoiceField): """Like ChoiceField, but specifically for Free/Busy values.""" def __init__(self, *args, **kwargs): kwargs['choices'] = set(FREE_BUSY_CHOICES) super().__init__(*args, **kwargs) class BodyField(TextField): """A TextField with specific requirements for the Item body.""" def __init__(self, *args, **kwargs): from .properties import Body self.value_cls = Body super().__init__(*args, **kwargs) def clean(self, value, version=None): if value is not None and not isinstance(value, self.value_cls): value = self.value_cls(value) return super().clean(value, version=version) def from_xml(self, elem, account): from .properties import Body, HTMLBody field_elem = elem.find(self.response_tag()) val = None if field_elem is None else field_elem.text or None if val is not None: body_type = field_elem.get('BodyType') return { Body.body_type: Body, HTMLBody.body_type: HTMLBody, }[body_type](val) return self.default def to_xml(self, value, version): from .properties import Body, HTMLBody field_elem = create_element(self.request_tag()) body_type = { Body: Body.body_type, HTMLBody: HTMLBody.body_type, }[type(value)] field_elem.set('BodyType', body_type) return set_xml_value(field_elem, value, version=version) class EWSElementField(FieldURIField): """A generic field for any EWSElement object.""" def __init__(self, *args, **kwargs): self._value_cls = kwargs.pop('value_cls') if 'namespace' not in kwargs: kwargs['namespace'] = self.value_cls.NAMESPACE super().__init__(*args, **kwargs) @property def value_cls(self): if isinstance(self._value_cls, str): # Support 'value_cls' as string to allow self-referencing classes. The class must be importable from the # top-level module. self._value_cls = getattr(import_module(self.__module__.split('.')[0]), self._value_cls) return self._value_cls def from_xml(self, elem, account): if self.is_list: iter_elem = elem.find(self.response_tag()) if iter_elem is not None: return [self.value_cls.from_xml(elem=e, account=account) for e in iter_elem.findall(self.value_cls.response_tag())] else: if self.field_uri is None: sub_elem = elem.find(self.value_cls.response_tag()) else: sub_elem = elem.find(self.response_tag()) if sub_elem is not None: return self.value_cls.from_xml(elem=sub_elem, account=account) return self.default def to_xml(self, value, version): if self.field_uri is None: return value.to_xml(version=version) field_elem = create_element(self.request_tag()) return set_xml_value(field_elem, value, version=version) class EWSElementListField(EWSElementField): """Like EWSElementField, but for lists of EWSElement objects.""" is_list = True is_complex = True class AssociatedCalendarItemIdField(EWSElementField): is_complex = True def __init__(self, *args, **kwargs): from .properties import AssociatedCalendarItemId kwargs['value_cls'] = AssociatedCalendarItemId super().__init__(*args, **kwargs) def to_xml(self, value, version): return value.to_xml(version=version) class RecurrenceField(EWSElementField): is_complex = True def __init__(self, *args, **kwargs): from .recurrence import Recurrence kwargs['value_cls'] = Recurrence super().__init__(*args, **kwargs) def to_xml(self, value, version): return value.to_xml(version=version) class TaskRecurrenceField(EWSElementField): is_complex = True def __init__(self, *args, **kwargs): from .recurrence import TaskRecurrence kwargs['value_cls'] = TaskRecurrence super().__init__(*args, **kwargs) def to_xml(self, value, version): return value.to_xml(version=version) class ReferenceItemIdField(EWSElementField): is_complex = True def __init__(self, *args, **kwargs): from .properties import ReferenceItemId kwargs['value_cls'] = ReferenceItemId super().__init__(*args, **kwargs) def to_xml(self, value, version): return value.to_xml(version=version) class OccurrenceField(EWSElementField): is_complex = True class OccurrenceListField(OccurrenceField): is_list = True class MessageHeaderField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import MessageHeader kwargs['value_cls'] = MessageHeader super().__init__(*args, **kwargs) class BaseEmailField(EWSElementField): """A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for.""" is_complex = True # FindItem only returns the name, not the email address def clean(self, value, version=None): if isinstance(value, str): value = self.value_cls(email_address=value) return super().clean(value, version=version) def from_xml(self, elem, account): if self.field_uri is None: sub_elem = elem.find(self.value_cls.response_tag()) else: sub_elem = elem.find(self.response_tag()) if sub_elem is not None: if self.field_uri is not None: # We want the nested Mailbox, not the wrapper element nested_elem = sub_elem.find(self.value_cls.response_tag()) if nested_elem is None: raise ValueError( 'Expected XML element %r missing on field %r' % (self.value_cls.response_tag(), self.name) ) return self.value_cls.from_xml(elem=nested_elem, account=account) return self.value_cls.from_xml(elem=sub_elem, account=account) return self.default class EmailField(BaseEmailField): def __init__(self, *args, **kwargs): from .properties import Email kwargs['value_cls'] = Email super().__init__(*args, **kwargs) class RecipientAddressField(BaseEmailField): def __init__(self, *args, **kwargs): from .properties import RecipientAddress kwargs['value_cls'] = RecipientAddress super().__init__(*args, **kwargs) class MailboxField(BaseEmailField): def __init__(self, *args, **kwargs): from .properties import Mailbox kwargs['value_cls'] = Mailbox super().__init__(*args, **kwargs) class MailboxListField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import Mailbox kwargs['value_cls'] = Mailbox super().__init__(*args, **kwargs) def clean(self, value, version=None): if value is not None: value = [self.value_cls(email_address=s) if isinstance(s, str) else s for s in value] return super().clean(value, version=version) class MemberListField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import Member kwargs['value_cls'] = Member super().__init__(*args, **kwargs) def clean(self, value, version=None): from .properties import Mailbox if value is not None: value = [ self.value_cls(mailbox=Mailbox(email_address=s)) if isinstance(s, str) else s for s in value ] return super().clean(value, version=version) class AttendeesField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import Attendee kwargs['value_cls'] = Attendee super().__init__(*args, **kwargs) def clean(self, value, version=None): from .properties import Mailbox if value is not None: value = [self.value_cls(mailbox=Mailbox(email_address=s), response_type='Accept') if isinstance(s, str) else s for s in value] return super().clean(value, version=version) class AttachmentField(EWSElementListField): """A field for item attachments.""" def __init__(self, *args, **kwargs): from .attachments import Attachment kwargs['value_cls'] = Attachment super().__init__(*args, **kwargs) def from_xml(self, elem, account): from .attachments import FileAttachment, ItemAttachment iter_elem = elem.find(self.response_tag()) # Look for both FileAttachment and ItemAttachment if iter_elem is not None: attachments = [] for att_type in (ItemAttachment, FileAttachment): attachments.extend( [att_type.from_xml(elem=e, account=account) for e in iter_elem.findall(att_type.response_tag())] ) return attachments return self.default class LabelField(ChoiceField): """A field to hold the label on an IndexedElement.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.is_attribute = True def from_xml(self, elem, account): return elem.get(self.field_uri) class SubField(Field): """A field to hold the value on an IndexedElement.""" namespace = TNS value_cls = str def from_xml(self, elem, account): return elem.text def to_xml(self, value, version): return value @staticmethod def field_uri_xml(field_uri, label): from .properties import IndexedFieldURI return IndexedFieldURI(field_uri=field_uri, field_index=label).to_xml(version=None) def clean(self, value, version=None): value = super().clean(value, version=version) if self.is_required and not value: raise ValueError('Value for subfield %r must be non-empty' % self.name) return value def __hash__(self): return hash(self.name) class EmailSubField(SubField): """A field to hold the value on an SingleFieldIndexedElement.""" value_cls = str def from_xml(self, elem, account): return elem.text or elem.get('Name') # Sometimes elem.text is empty. Exchange saves the same in 'Name' attr class NamedSubField(SubField): """A field to hold the value on an MultiFieldIndexedElement.""" value_cls = str def __init__(self, *args, **kwargs): self.field_uri = kwargs.pop('field_uri') if ':' in self.field_uri: raise ValueError("'field_uri' value must not contain a colon") super().__init__(*args, **kwargs) def from_xml(self, elem, account): field_elem = elem.find(self.response_tag()) val = None if field_elem is None else field_elem.text or None if val is not None: return val return self.default def to_xml(self, value, version): field_elem = create_element(self.request_tag()) return set_xml_value(field_elem, value, version=version) def field_uri_xml(self, field_uri, label): from .properties import IndexedFieldURI return IndexedFieldURI(field_uri='%s:%s' % (field_uri, self.field_uri), field_index=label).to_xml(version=None) def request_tag(self): return 't:%s' % self.field_uri def response_tag(self): return '{%s}%s' % (self.namespace, self.field_uri) class IndexedField(EWSElementField): """A base class for all indexed fields.""" PARENT_ELEMENT_NAME = None def __init__(self, *args, **kwargs): from .indexed_properties import IndexedElement value_cls = kwargs['value_cls'] if not issubclass(value_cls, IndexedElement): raise ValueError("'value_cls' %r must be a subclass of IndexedElement" % value_cls) super().__init__(*args, **kwargs) def to_xml(self, value, version): return set_xml_value(create_element('t:%s' % self.PARENT_ELEMENT_NAME), value, version) def response_tag(self): return '{%s}%s' % (self.namespace, self.PARENT_ELEMENT_NAME) def __hash__(self): return hash(self.field_uri) class EmailAddressesField(IndexedField): is_list = True PARENT_ELEMENT_NAME = 'EmailAddresses' def __init__(self, *args, **kwargs): from .indexed_properties import EmailAddress kwargs['value_cls'] = EmailAddress super().__init__(*args, **kwargs) def clean(self, value, version=None): if value is not None: default_labels = self.value_cls.LABEL_CHOICES if len(value) > len(default_labels): raise ValueError('This field can handle at most %s values (value: %r)' % (len(default_labels), value)) tmp = [] for s, default_label in zip(value, default_labels): if not isinstance(s, str): tmp.append(s) continue tmp.append(self.value_cls(email=s, label=default_label)) value = tmp return super().clean(value, version=version) class PhoneNumberField(IndexedField): is_list = True PARENT_ELEMENT_NAME = 'PhoneNumbers' def __init__(self, *args, **kwargs): from .indexed_properties import PhoneNumber kwargs['value_cls'] = PhoneNumber super().__init__(*args, **kwargs) class PhysicalAddressField(IndexedField): is_list = True PARENT_ELEMENT_NAME = 'PhysicalAddresses' def __init__(self, *args, **kwargs): from .indexed_properties import PhysicalAddress kwargs['value_cls'] = PhysicalAddress super().__init__(*args, **kwargs) class ExtendedPropertyField(Field): def __init__(self, *args, **kwargs): self.value_cls = kwargs.pop('value_cls') super().__init__(*args, **kwargs) def clean(self, value, version=None): if value is None: if self.is_required: raise ValueError("'%s' is a required field" % self.name) return self.default if not isinstance(value, self.value_cls): # Allow keeping ExtendedProperty field values as their simple Python type, but run clean() anyway tmp = self.value_cls(value) tmp.clean(version=version) return value value.clean(version=version) return value def field_uri_xml(self): from .properties import ExtendedFieldURI cls = self.value_cls return ExtendedFieldURI( distinguished_property_set_id=cls.distinguished_property_set_id, property_set_id=cls.property_set_id.lower() if cls.property_set_id else None, property_tag=cls.property_tag_as_hex(), property_name=cls.property_name, property_id=value_to_xml_text(cls.property_id) if cls.property_id else None, property_type=cls.property_type, ).to_xml(version=None) def from_xml(self, elem, account): extended_properties = elem.findall(self.value_cls.response_tag()) for extended_property in extended_properties: if self.value_cls.is_property_instance(extended_property): return self.value_cls.from_xml(elem=extended_property, account=account) return self.default def to_xml(self, value, version): extended_property = create_element(self.value_cls.request_tag()) set_xml_value(extended_property, self.field_uri_xml(), version=version) if isinstance(value, self.value_cls): set_xml_value(extended_property, value, version=version) else: # Allow keeping ExtendedProperty field values as their simple Python type set_xml_value(extended_property, self.value_cls(value), version=version) return extended_property def __hash__(self): return hash(self.name) class ItemField(FieldURIField): @property def value_cls(self): from .items import Item return Item def from_xml(self, elem, account): from .items import ITEM_CLASSES for item_cls in ITEM_CLASSES: item_elem = elem.find(item_cls.response_tag()) if item_elem is not None: return item_cls.from_xml(elem=item_elem, account=account) return None def to_xml(self, value, version): # We don't want to wrap in an Item element return value.to_xml(version=version) class UnknownEntriesField(CharListField): def list_elem_tag(self): return '{%s}UnknownEntry' % self.namespace class PermissionSetField(EWSElementField): is_complex = True def __init__(self, *args, **kwargs): from .properties import PermissionSet kwargs['value_cls'] = PermissionSet super().__init__(*args, **kwargs) class EffectiveRightsField(EWSElementField): def __init__(self, *args, **kwargs): from .properties import EffectiveRights kwargs['value_cls'] = EffectiveRights super().__init__(*args, **kwargs) class BuildField(CharField): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.value_cls = Build def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val: try: return self.value_cls.from_hex_string(val) except (TypeError, ValueError): log.warning('Invalid server version string: %r', val) return val class ProtocolListField(EWSElementListField): # There is not containing element for this field. Just multiple 'Protocol' elements on the 'Account' element. def __init__(self, *args, **kwargs): from .autodiscover.properties import Protocol kwargs['value_cls'] = Protocol super().__init__(*args, **kwargs) def from_xml(self, elem, account): return [self.value_cls.from_xml(elem=e, account=account) for e in elem.findall(self.value_cls.response_tag())] class RoutingTypeField(ChoiceField): def __init__(self, *args, **kwargs): kwargs['choices'] = {Choice('SMTP'), Choice('EX')} kwargs['default'] = 'SMTP' super().__init__(*args, **kwargs) class IdElementField(EWSElementField): def __init__(self, *args, **kwargs): kwargs['is_searchable'] = False kwargs['is_read_only'] = True super().__init__(*args, **kwargs) class TypeValueField(FieldURIField): """This field type has no value_cls because values may have many different types.""" TYPES_MAP = { 'Boolean': bool, 'Integer32': int, 'UnsignedInteger32': int, 'Integer64': int, 'UnsignedInteger64': int, # Python doesn't have a single-byte type to represent 'Byte' 'ByteArray': bytes, 'String': str, 'StringArray': str, # A list of strings 'DateTime': EWSDateTime, } TYPES_MAP_REVERSED = { bool: 'Boolean', int: 'Integer64', # Python doesn't have a single-byte type to represent 'Byte' bytes: 'ByteArray', str: 'String', datetime.datetime: 'DateTime', EWSDateTime: 'DateTime', } @classmethod def get_type(cls, value): if isinstance(value, bytes) and len(value) == 1: # This is a single byte. Translate it to the 'Byte' type return 'Byte' if is_iterable(value): # We don't allow generators as values, so keep the logic simple try: first = next(iter(value)) except StopIteration: first = None value_type = '%sArray' % cls.TYPES_MAP_REVERSED[type(first)] if value_type not in cls.TYPES_MAP: raise ValueError('%r is not a supported type' % value) return value_type return cls.TYPES_MAP_REVERSED[type(value)] @classmethod def is_array_type(cls, value_type): return value_type == 'StringArray' def clean(self, value, version=None): if value is None: if self.is_required and self.default is None: raise ValueError("'%s' is a required field with no default" % self.name) return self.default return value def from_xml(self, elem, account): field_elem = elem.find(self.response_tag()) if field_elem is None: return self.default value_type_str = get_xml_attr(field_elem, '{%s}Type' % TNS) value = get_xml_attr(field_elem, '{%s}Value' % TNS) if value_type_str == 'Byte': try: # The value is an unsigned integer in the range 0 -> 255. Convert it to a single byte return xml_text_to_value(value, int).to_bytes(1, 'little', signed=False) except OverflowError as e: log.warning('Invalid byte value %r (%e)', value, e) return None value_type = self.TYPES_MAP[value_type_str] if self. is_array_type(value_type_str): return tuple(xml_text_to_value(value=v, value_type=value_type) for v in value.split(' ')) return xml_text_to_value(value=value, value_type=value_type) def to_xml(self, value, version): value_type_str = self.get_type(value) if value_type_str == 'Byte': # A single byte is encoded to an unsigned integer in the range 0 -> 255 value = int.from_bytes(value, byteorder='little', signed=False) elif is_iterable(value): value = ' '.join(value_to_xml_text(v) for v in value) field_elem = create_element(self.request_tag()) field_elem.append(set_xml_value(create_element('t:Type'), value_type_str, version=version)) field_elem.append(set_xml_value(create_element('t:Value'), value, version=version)) return field_elem class DictionaryField(FieldURIField): value_cls = dict def from_xml(self, elem, account): from .properties import DictionaryEntry iter_elem = elem.find(self.response_tag()) if iter_elem is not None: entries = [ DictionaryEntry.from_xml(elem=e, account=account) for e in iter_elem.findall(DictionaryEntry.response_tag()) ] return {e.key: e.value for e in entries} return self.default def clean(self, value, version=None): if isinstance(value, dict): cleaned = {} for k, v in value.items(): if type(k) is datetime.datetime: k = EWSDateTime.from_datetime(k) if type(v) is datetime.datetime: v = EWSDateTime.from_datetime(v) cleaned[k] = v value = cleaned return super().clean(value=value, version=version) def to_xml(self, value, version): from .properties import DictionaryEntry field_elem = create_element(self.request_tag()) entries = [DictionaryEntry(key=k, value=v) for k, v in value.items()] return set_xml_value(field_elem, entries, version=version) class PersonaPhoneNumberField(EWSElementField): is_complex = True def __init__(self, *args, **kwargs): from .properties import PhoneNumber kwargs['value_cls'] = PhoneNumber super().__init__(*args, **kwargs) class BodyContentAttributedValueField(EWSElementField): is_complex = True def __init__(self, *args, **kwargs): from .properties import BodyContentAttributedValue kwargs['value_cls'] = BodyContentAttributedValue super().__init__(*args, **kwargs) class StringAttributedValueField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import StringAttributedValue kwargs['value_cls'] = StringAttributedValue super().__init__(*args, **kwargs) class PhoneNumberAttributedValueField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import PhoneNumberAttributedValue kwargs['value_cls'] = PhoneNumberAttributedValue super().__init__(*args, **kwargs) class EmailAddressAttributedValueField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import EmailAddressAttributedValue kwargs['value_cls'] = EmailAddressAttributedValue super().__init__(*args, **kwargs) class PostalAddressAttributedValueField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import PostalAddressAttributedValue kwargs['value_cls'] = PostalAddressAttributedValue super().__init__(*args, **kwargs) class GenericEventListField(EWSElementField): """A list field that can contain all subclasses of Event.""" is_list = True @property def _event_types_map(self): return {v.response_tag(): v for v in self.value_classes} def __init__(self, *args, **kwargs): from .properties import CopiedEvent, CreatedEvent, DeletedEvent, ModifiedEvent, MovedEvent, \ NewMailEvent, StatusEvent, FreeBusyChangedEvent kwargs['value_cls'] = None # Parent class requires this kwarg kwargs['namespace'] = None # Parent class requires this kwarg super().__init__(*args, **kwargs) self.value_classes = ( CopiedEvent, CreatedEvent, DeletedEvent, ModifiedEvent, MovedEvent, NewMailEvent, StatusEvent, FreeBusyChangedEvent, ) def from_xml(self, elem, account): events = [] for event in elem: # This may or may not be an event element. Could also be other child elements of Notification try: value_cls = self._event_types_map[event.tag] except KeyError: continue events.append(value_cls.from_xml(elem=event, account=account)) return events or self.default exchangelib-4.6.1/exchangelib/folders/000077500000000000000000000000001414601472700177105ustar00rootroot00000000000000exchangelib-4.6.1/exchangelib/folders/__init__.py000066400000000000000000000064271414601472700220320ustar00rootroot00000000000000from .base import BaseFolder, Folder from .collections import FolderCollection from .known_folders import AdminAuditLogs, AllContacts, AllItems, ArchiveDeletedItems, ArchiveInbox, \ ArchiveMsgFolderRoot, ArchiveRecoverableItemsDeletions, ArchiveRecoverableItemsPurges, \ ArchiveRecoverableItemsRoot, ArchiveRecoverableItemsVersions, Audits, Calendar, CalendarLogging, CommonViews, \ Conflicts, Contacts, ConversationHistory, ConversationSettings, DefaultFoldersChangeHistory, DeferredAction, \ DeletedItems, Directory, Drafts, ExchangeSyncData, Favorites, Files, FreebusyData, Friends, GALContacts, \ GraphAnalytics, IMContactList, Inbox, Journal, JunkEmail, LocalFailures, Location, MailboxAssociations, Messages, \ MsgFolderRoot, MyContacts, MyContactsExtended, NonDeletableFolderMixin, Notes, Outbox, ParkedMessages, \ PassThroughSearchResults, PdpProfileV2Secured, PeopleConnect, QuickContacts, RSSFeeds, RecipientCache, \ RecoverableItemsDeletions, RecoverableItemsPurges, RecoverableItemsRoot, RecoverableItemsVersions, Reminders, \ Schedule, SearchFolders, SentItems, ServerFailures, Sharing, Shortcuts, Signal, SmsAndChatsSync, SpoolerQueue, \ SyncIssues, System, Tasks, TemporarySaves, ToDoSearch, Views, VoiceMail, WellknownFolder, WorkingSet, \ Companies, OrganizationalContacts, PeopleCentricConversationBuddies, NON_DELETABLE_FOLDERS from .queryset import FolderQuerySet, SingleFolderQuerySet, FOLDER_TRAVERSAL_CHOICES, SHALLOW, DEEP, SOFT_DELETED from .roots import Root, ArchiveRoot, PublicFoldersRoot, RootOfHierarchy from ..properties import FolderId, DistinguishedFolderId __all__ = [ 'FolderId', 'DistinguishedFolderId', 'FolderCollection', 'BaseFolder', 'Folder', 'AdminAuditLogs', 'AllContacts', 'AllItems', 'ArchiveDeletedItems', 'ArchiveInbox', 'ArchiveMsgFolderRoot', 'ArchiveRecoverableItemsDeletions', 'ArchiveRecoverableItemsPurges', 'ArchiveRecoverableItemsRoot', 'ArchiveRecoverableItemsVersions', 'Audits', 'Calendar', 'CalendarLogging', 'CommonViews', 'Conflicts', 'Contacts', 'ConversationHistory', 'ConversationSettings', 'DefaultFoldersChangeHistory', 'DeferredAction', 'DeletedItems', 'Directory', 'Drafts', 'ExchangeSyncData', 'Favorites', 'Files', 'FreebusyData', 'Friends', 'GALContacts', 'GraphAnalytics', 'IMContactList', 'Inbox', 'Journal', 'JunkEmail', 'LocalFailures', 'Location', 'MailboxAssociations', 'Messages', 'MsgFolderRoot', 'MyContacts', 'MyContactsExtended', 'NonDeletableFolderMixin', 'Notes', 'Outbox', 'ParkedMessages', 'PassThroughSearchResults', 'PdpProfileV2Secured', 'PeopleConnect', 'QuickContacts', 'RSSFeeds', 'RecipientCache', 'RecoverableItemsDeletions', 'RecoverableItemsPurges', 'RecoverableItemsRoot', 'RecoverableItemsVersions', 'Reminders', 'Schedule', 'SearchFolders', 'SentItems', 'ServerFailures', 'Sharing', 'Shortcuts', 'Signal', 'SmsAndChatsSync', 'SpoolerQueue', 'SyncIssues', 'System', 'Tasks', 'TemporarySaves', 'ToDoSearch', 'Views', 'VoiceMail', 'WellknownFolder', 'WorkingSet', 'Companies', 'OrganizationalContacts', 'PeopleCentricConversationBuddies', 'NON_DELETABLE_FOLDERS', 'FolderQuerySet', 'SingleFolderQuerySet', 'FOLDER_TRAVERSAL_CHOICES', 'SHALLOW', 'DEEP', 'SOFT_DELETED', 'Root', 'ArchiveRoot', 'PublicFoldersRoot', 'RootOfHierarchy', ] exchangelib-4.6.1/exchangelib/folders/base.py000066400000000000000000001170351414601472700212030ustar00rootroot00000000000000import abc import logging from fnmatch import fnmatch from operator import attrgetter from .collections import FolderCollection, SyncCompleted from .queryset import SingleFolderQuerySet, SHALLOW as SHALLOW_FOLDERS, DEEP as DEEP_FOLDERS from ..errors import ErrorAccessDenied, ErrorFolderNotFound, ErrorCannotEmptyFolder, ErrorCannotDeleteObject, \ ErrorDeleteDistinguishedFolder, ErrorInvalidSubscription, ErrorNoPublicFolderReplicaAvailable, ErrorItemNotFound from ..fields import IntegerField, CharField, FieldPath, EffectiveRightsField, PermissionSetField, EWSElementField, \ Field, IdElementField, InvalidField from ..items import CalendarItem, RegisterMixIn, ITEM_CLASSES, DELETE_TYPE_CHOICES, HARD_DELETE, \ SHALLOW as SHALLOW_ITEMS from ..properties import Mailbox, FolderId, ParentFolderId, DistinguishedFolderId, UserConfiguration, \ UserConfigurationName, UserConfigurationNameMNS, EWSMeta from ..queryset import SearchableMixIn, DoesNotExist from ..services import CreateFolder, UpdateFolder, DeleteFolder, EmptyFolder, GetUserConfiguration, \ CreateUserConfiguration, UpdateUserConfiguration, DeleteUserConfiguration, SubscribeToPush, SubscribeToPull, \ Unsubscribe, GetEvents, GetStreamingEvents, MoveFolder from ..services.get_user_configuration import ALL from ..util import TNS, require_id from ..version import Version, EXCHANGE_2007_SP1, EXCHANGE_2010 log = logging.getLogger(__name__) MISSING_FOLDER_ERRORS = (ErrorFolderNotFound, ErrorItemNotFound, ErrorNoPublicFolderReplicaAvailable) class BaseFolder(RegisterMixIn, SearchableMixIn, metaclass=EWSMeta): """Base class for all classes that implement a folder.""" ELEMENT_NAME = 'Folder' NAMESPACE = TNS # See https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid DISTINGUISHED_FOLDER_ID = None # Default item type for this folder. See # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxosfld/68a85898-84fe-43c4-b166-4711c13cdd61 CONTAINER_CLASS = None supported_item_models = ITEM_CLASSES # The Item types that this folder can contain. Default is all # Marks the version from which a distinguished folder was introduced. A possibly authoritative source is: # https://github.com/OfficeDev/ews-managed-api/blob/master/Enumerations/WellKnownFolderName.cs supported_from = None # Whether this folder type is allowed with the GetFolder service get_folder_allowed = True DEFAULT_FOLDER_TRAVERSAL_DEPTH = DEEP_FOLDERS DEFAULT_ITEM_TRAVERSAL_DEPTH = SHALLOW_ITEMS LOCALIZED_NAMES = {} # A map of (str)locale: (tuple)localized_folder_names ITEM_MODEL_MAP = {cls.response_tag(): cls for cls in ITEM_CLASSES} ID_ELEMENT_CLS = FolderId _id = IdElementField(field_uri='folder:FolderId', value_cls=ID_ELEMENT_CLS) parent_folder_id = EWSElementField(field_uri='folder:ParentFolderId', value_cls=ParentFolderId, is_read_only=True) folder_class = CharField(field_uri='folder:FolderClass', is_required_after_save=True) name = CharField(field_uri='folder:DisplayName') total_count = IntegerField(field_uri='folder:TotalCount', is_read_only=True) child_folder_count = IntegerField(field_uri='folder:ChildFolderCount', is_read_only=True) unread_count = IntegerField(field_uri='folder:UnreadCount', is_read_only=True) __slots__ = 'is_distinguished', 'item_sync_state', 'folder_sync_state' # Used to register extended properties INSERT_AFTER_FIELD = 'child_folder_count' def __init__(self, **kwargs): self.is_distinguished = kwargs.pop('is_distinguished', False) self.item_sync_state = kwargs.pop('item_sync_state', None) self.folder_sync_state = kwargs.pop('folder_sync_state', None) super().__init__(**kwargs) @property @abc.abstractmethod def account(self): pass @property @abc.abstractmethod def root(self): pass @property @abc.abstractmethod def parent(self): pass @property def is_deletable(self): return not self.is_distinguished def clean(self, version=None): super().clean(version=version) # Set a default folder class for new folders. A folder class cannot be changed after saving. if self.id is None and self.folder_class is None: self.folder_class = self.CONTAINER_CLASS @property def children(self): # It's dangerous to return a generator here because we may then call methods on a child that result in the # cache being updated while it's iterated. return FolderCollection(account=self.account, folders=self.root.get_children(self)) @property def parts(self): parts = [self] f = self.parent while f: parts.insert(0, f) f = f.parent return parts @property def absolute(self): return ''.join('/%s' % p.name for p in self.parts) def _walk(self): for c in self.children: yield c yield from c.walk() def walk(self): return FolderCollection(account=self.account, folders=self._walk()) def _glob(self, pattern): split_pattern = pattern.rsplit('/', 1) head, tail = (split_pattern[0], None) if len(split_pattern) == 1 else split_pattern if head == '': # We got an absolute path. Restart globbing at root yield from self.root.glob(tail or '*') elif head == '..': # Relative path with reference to parent. Restart globbing at parent if not self.parent: raise ValueError('Already at top') yield from self.parent.glob(tail or '*') elif head == '**': # Match anything here or in any subfolder at arbitrary depth for c in self.walk(): # fnmatch() may be case-sensitive depending on operating system: # force a case-insensitive match since case appears not to # matter for folders in Exchange if fnmatch(c.name.lower(), (tail or '*').lower()): yield c else: # Regular pattern for c in self.children: # See note above on fnmatch() case-sensitivity if not fnmatch(c.name.lower(), head.lower()): continue if tail is None: yield c continue yield from c.glob(tail) def glob(self, pattern): return FolderCollection(account=self.account, folders=self._glob(pattern)) def tree(self): """Return a string representation of the folder structure of this folder. Example: root ├── inbox │ └── todos └── archive ├── Last Job ├── exchangelib issues └── Mom """ tree = '%s\n' % self.name children = list(self.children) for i, c in enumerate(sorted(children, key=attrgetter('name')), start=1): nodes = c.tree().split('\n') for j, node in enumerate(nodes, start=1): if i != len(children) and j == 1: # Not the last child, but the first node, which is the name of the child tree += '├── %s\n' % node elif i != len(children) and j > 1: # Not the last child, and not name of child tree += '│ %s\n' % node elif i == len(children) and j == 1: # Not the last child, but the first node, which is the name of the child tree += '└── %s\n' % node else: # Last child, and not name of child tree += ' %s\n' % node return tree.strip() @classmethod def supports_version(cls, version): # 'version' is a Version instance, for convenience by callers if not isinstance(version, Version): raise ValueError("'version' %r must be a Version instance" % version) if not cls.supported_from: return True return version.build >= cls.supported_from @property def has_distinguished_name(self): return self.name and self.DISTINGUISHED_FOLDER_ID and self.name.lower() == self.DISTINGUISHED_FOLDER_ID.lower() @classmethod def localized_names(cls, locale): # Return localized names for a specific locale. If no locale-specific names exist, return the default names, # if any. return tuple(s.lower() for s in cls.LOCALIZED_NAMES.get(locale, cls.LOCALIZED_NAMES.get(None, []))) @staticmethod def folder_cls_from_container_class(container_class): """Return a reasonable folder class given a container class, e.g. 'IPF.Note'. Don't iterate WELLKNOWN_FOLDERS because many folder classes have the same CONTAINER_CLASS. :param container_class: :return: """ from .known_folders import Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, \ RecipientCache, RSSFeeds for folder_cls in ( Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, RecipientCache, RSSFeeds): if folder_cls.CONTAINER_CLASS == container_class: return folder_cls raise KeyError() @classmethod def item_model_from_tag(cls, tag): try: return cls.ITEM_MODEL_MAP[tag] except KeyError: raise ValueError('Item type %s was unexpected in a %s folder' % (tag, cls.__name__)) @classmethod def allowed_item_fields(cls, version): # Return non-ID fields of all item classes allowed in this folder type fields = set() for item_model in cls.supported_item_models: fields.update( set(item_model.supported_fields(version=version)) ) return fields def validate_item_field(self, field, version): # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid # for the item types supported by this folder. # For each field, check if the field is valid for any of the item models supported by this folder for item_model in self.supported_item_models: try: item_model.validate_field(field=field, version=version) break except InvalidField: continue else: raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models)) def normalize_fields(self, fields): # Takes a list of fieldnames, Field or FieldPath objects pointing to item fields. Turns them into FieldPath # objects and adds internal timezone fields if necessary. Assume fields are already validated. fields = list(fields) has_start, has_end = False, False for i, field_path in enumerate(fields): # Allow both Field and FieldPath instances and string field paths as input if isinstance(field_path, str): field_path = FieldPath.from_string(field_path=field_path, folder=self) fields[i] = field_path elif isinstance(field_path, Field): field_path = FieldPath(field=field_path) fields[i] = field_path if not isinstance(field_path, FieldPath): raise ValueError("Field %r must be a string or FieldPath instance" % field_path) if field_path.field.name == 'start': has_start = True elif field_path.field.name == 'end': has_end = True # For CalendarItem items, we want to inject internal timezone fields. See also CalendarItem.clean() if CalendarItem in self.supported_item_models: meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields() if self.account.version.build < EXCHANGE_2010: if has_start or has_end: fields.append(FieldPath(field=meeting_tz_field)) else: if has_start: fields.append(FieldPath(field=start_tz_field)) if has_end: fields.append(FieldPath(field=end_tz_field)) return fields @classmethod def get_item_field_by_fieldname(cls, fieldname): for item_model in cls.supported_item_models: try: return item_model.get_field_by_fieldname(fieldname) except InvalidField: pass raise InvalidField("%r is not a valid field name on %s" % (fieldname, cls.supported_item_models)) def get(self, *args, **kwargs): return FolderCollection(account=self.account, folders=[self]).get(*args, **kwargs) def all(self): return FolderCollection(account=self.account, folders=[self]).all() def none(self): return FolderCollection(account=self.account, folders=[self]).none() def filter(self, *args, **kwargs): return FolderCollection(account=self.account, folders=[self]).filter(*args, **kwargs) def exclude(self, *args, **kwargs): return FolderCollection(account=self.account, folders=[self]).exclude(*args, **kwargs) def people(self): # No point in using a FolderCollection because FindPeople only supports one folder return FolderCollection(account=self.account, folders=[self]).people() def bulk_create(self, items, *args, **kwargs): return self.account.bulk_create(folder=self, items=items, *args, **kwargs) def save(self, update_fields=None): if self.id is None: # New folder if update_fields: raise ValueError("'update_fields' is only valid for updates") res = CreateFolder(account=self.account).get(parent_folder=self.parent, folders=[self]) self._id = self.ID_ELEMENT_CLS(res.id, res.changekey) self.root.add_folder(self) # Add this folder to the cache return self # Update folder if not update_fields: # The fields to update was not specified explicitly. Update all fields where update is possible update_fields = [] for f in self.supported_fields(version=self.account.version): if f.is_read_only: # These cannot be changed continue if (f.is_required or f.is_required_after_save) and ( getattr(self, f.name) is None or (f.is_list and not getattr(self, f.name)) ): # These are required and cannot be deleted continue update_fields.append(f.name) res = UpdateFolder(account=self.account).get(folders=[(self, update_fields)]) folder_id, changekey = res.id, res.changekey if self.id != folder_id: raise ValueError('ID mismatch') # Don't check changekey value. It may not change on no-op updates self.changekey = changekey self.root.update_folder(self) # Update the folder in the cache return self def move(self, to_folder): res = MoveFolder(account=self.account).get(folders=[self], to_folder=to_folder) folder_id, changekey = res.id, res.changekey if self.id != folder_id: raise ValueError('ID mismatch') # Don't check changekey value. It may not change on no-op moves self.changekey = changekey self.parent_folder_id = ParentFolderId(id=to_folder.id, changekey=to_folder.changekey) self.root.update_folder(self) # Update the folder in the cache def delete(self, delete_type=HARD_DELETE): if delete_type not in DELETE_TYPE_CHOICES: raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES)) DeleteFolder(account=self.account).get(folders=[self], delete_type=delete_type) self.root.remove_folder(self) # Remove the updated folder from the cache self._id = None def empty(self, delete_type=HARD_DELETE, delete_sub_folders=False): if delete_type not in DELETE_TYPE_CHOICES: raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES)) EmptyFolder(account=self.account).get( folders=[self], delete_type=delete_type, delete_sub_folders=delete_sub_folders ) if delete_sub_folders: # We don't know exactly what was deleted, so invalidate the entire folder cache to be safe self.root.clear_cache() def wipe(self, page_size=None, _seen=None, _level=0): # Recursively deletes all items in this folder, and all subfolders and their content. Attempts to protect # distinguished folders from being deleted. Use with caution! _seen = _seen or set() if self.id in _seen: raise RecursionError('We already tried to wipe %s' % self) if _level > 16: raise RecursionError('Max recursion level reached: %s' % _level) _seen.add(self.id) log.warning('Wiping %s', self) has_distinguished_subfolders = any(f.is_distinguished for f in self.children) try: if has_distinguished_subfolders: self.empty(delete_sub_folders=False) else: self.empty(delete_sub_folders=True) except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound): try: if has_distinguished_subfolders: raise # We already tried this self.empty(delete_sub_folders=False) except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound): log.warning('Not allowed to empty %s. Trying to delete items instead', self) try: self.all().delete(**dict(page_size=page_size) if page_size else {}) except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound): log.warning('Not allowed to delete items in %s', self) _level += 1 for f in self.children: f.wipe(page_size=page_size, _seen=_seen, _level=_level) # Remove non-distinguished children that are empty and have no subfolders if f.is_deletable and not f.children: log.warning('Deleting folder %s', f) try: f.delete() except ErrorDeleteDistinguishedFolder: log.warning('Tried to delete a distinguished folder (%s)', f) def test_access(self): """Does a simple FindItem to test (read) access to the folder. Maybe the account doesn't exist, maybe the service user doesn't have access to the calendar. This will throw the most common errors. """ self.all().exists() return True @classmethod def _kwargs_from_elem(cls, elem, account): # Check for 'DisplayName' element before collecting kwargs because because that clears the elements has_name_elem = elem.find(cls.get_field_by_fieldname('name').response_tag()) is not None kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS} if has_name_elem and not kwargs['name']: # When we request the 'DisplayName' property, some folders may still be returned with an empty value. # Assign a default name to these folders. kwargs['name'] = cls.DISTINGUISHED_FOLDER_ID return kwargs def to_folder_id(self): if self.is_distinguished: # Don't add the changekey here. When modifying folder content, we usually don't care if others have changed # the folder content since we fetched the changekey. if self.account: return DistinguishedFolderId( id=self.DISTINGUISHED_FOLDER_ID, mailbox=Mailbox(email_address=self.account.primary_smtp_address) ) return DistinguishedFolderId(id=self.DISTINGUISHED_FOLDER_ID) if self.id: return FolderId(id=self.id, changekey=self.changekey) raise ValueError('Must be a distinguished folder or have an ID') def to_xml(self, version): try: return self.to_folder_id().to_xml(version=version) except ValueError: return super().to_xml(version=version) def to_id_xml(self, version): # Folder(name='Foo') is a perfectly valid ID to e.g. create a folder return self.to_xml(version=version) @classmethod def resolve(cls, account, folder): # Resolve a single folder folders = list(FolderCollection(account=account, folders=[folder]).resolve()) if not folders: raise ErrorFolderNotFound('Could not find folder %r' % folder) if len(folders) != 1: raise ValueError('Expected result length 1, but got %s' % folders) f = folders[0] if isinstance(f, Exception): raise f if f.__class__ != cls: raise ValueError("Expected folder %r to be a %s instance" % (f, cls)) return f @require_id def refresh(self): fresh_folder = self.resolve(account=self.account, folder=self) if self.id != fresh_folder.id: raise ValueError('ID mismatch') # Apparently, the changekey may get updated for f in self.FIELDS: setattr(self, f.name, getattr(fresh_folder, f.name)) return self @require_id def get_user_configuration(self, name, properties=ALL): return GetUserConfiguration(account=self.account).get( user_configuration_name=UserConfigurationNameMNS(name=name, folder=self), properties=properties, ) @require_id def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None): user_configuration = UserConfiguration( user_configuration_name=UserConfigurationName(name=name, folder=self), dictionary=dictionary, xml_data=xml_data, binary_data=binary_data, ) return CreateUserConfiguration(account=self.account).get(user_configuration=user_configuration) @require_id def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None): user_configuration = UserConfiguration( user_configuration_name=UserConfigurationName(name=name, folder=self), dictionary=dictionary, xml_data=xml_data, binary_data=binary_data, ) return UpdateUserConfiguration(account=self.account).get(user_configuration=user_configuration) @require_id def delete_user_configuration(self, name): return DeleteUserConfiguration(account=self.account).get( user_configuration_name=UserConfigurationNameMNS(name=name, folder=self) ) @require_id def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, 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 """ s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_pull( event_types=event_types, watermark=watermark, timeout=timeout, )) if len(s_ids) != 1: raise ValueError('Expected result length 1, but got %s' % s_ids) s_id = s_ids[0] if isinstance(s_id, Exception): raise s_id return s_id @require_id def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, 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 """ s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_push( event_types=event_types, watermark=watermark, status_frequency=status_frequency, callback_url=callback_url, )) if len(s_ids) != 1: raise ValueError('Expected result length 1, but got %s' % s_ids) s_id = s_ids[0] if isinstance(s_id, Exception): raise s_id return s_id @require_id def subscribe_to_streaming(self, event_types=SubscribeToPush.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 """ s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming( event_types=event_types, )) if len(s_ids) != 1: raise ValueError('Expected result length 1, but got %s' % s_ids) s_id = s_ids[0] if isinstance(s_id, Exception): raise s_id return s_id @require_id def pull_subscription(self, **kwargs): return PullSubscription(folder=self, **kwargs) @require_id def push_subscription(self, **kwargs): return PushSubscription(folder=self, **kwargs) @require_id def streaming_subscription(self, **kwargs): return StreamingSubscription(folder=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 folder instance, but it makes sense to keep the method along the other sync methods. """ return Unsubscribe(account=self.account).get(subscription_id=subscription_id) def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None): """Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state. :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service. :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields :param ignore: A list of Item IDs to ignore in the sync :param max_changes_returned: The max number of change :param sync_scope: Specify whether to return just items, or items and folder associated information. Possible values are specified in SyncFolderitems.SYNC_SCOPES :return: A generator of (change_type, item) tuples """ if not sync_state: sync_state = self.item_sync_state try: yield from FolderCollection(account=self.account, folders=[self]).sync_items( sync_state=sync_state, only_fields=only_fields, ignore=ignore, max_changes_returned=max_changes_returned, sync_scope=sync_scope, ) except SyncCompleted as e: # Set the new sync state on the folder instance self.item_sync_state = e.sync_state def sync_hierarchy(self, sync_state=None, only_fields=None): """Return all folder changes to a folder hierarchy, as a generator. If sync_state is specified, get all folder changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new sync state. :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service. :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields :return: """ if not sync_state: sync_state = self.folder_sync_state try: yield from FolderCollection(account=self.account, folders=[self]).sync_hierarchy( sync_state=sync_state, only_fields=only_fields, ) except SyncCompleted as e: # Set the new sync state on the folder instance self.folder_sync_state = e.sync_state def get_events(self, subscription_id, watermark): """Get events since the given watermark. Non-blocking. :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|push]() :param watermark: Either the watermark from the subscription, or as returned in the last .get_events() call. :return: A Notification object containing a list of events This method doesn't need the current folder instance, but it makes sense to keep the method along the other sync methods. """ svc = GetEvents(account=self.account) while True: notification = svc.get(subscription_id=subscription_id, watermark=watermark) yield notification if not notification.more_events: break def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None): """Get events since the subscription was created, in streaming mode. This method will block as many minutes as specified by 'connection_timeout'. :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming() :param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout is reached. :param max_notifications_returned: If specified, will exit after receiving this number of notifications :return: A generator of Notification objects, each containing a list of events This method doesn't need the current folder instance, but it makes sense to keep the method along the other sync methods. """ # Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed request_timeout = connection_timeout*60 + 60 svc = GetStreamingEvents(account=self.account, timeout=request_timeout) for i, notification in enumerate( svc.call(subscription_ids=[subscription_id], connection_timeout=connection_timeout), start=1 ): yield notification if max_notifications_returned and i >= max_notifications_returned: svc.stop_streaming() break if svc.error_subscription_ids: raise ErrorInvalidSubscription('Invalid subscription IDs: %s' % svc.error_subscription_ids) def __floordiv__(self, other): """Support the some_folder // 'child_folder' // 'child_of_child_folder' navigation syntax. Works like as __truediv__ but does not touch the folder cache. This is useful if the folder hierarchy contains a huge number of folders and you don't want to fetch them all :param other: :return: """ if other == '..': raise ValueError('Cannot get parent without a folder cache') if other == '.': return self # Assume an exact match on the folder name in a shallow search will only return at most one folder try: return SingleFolderQuerySet(account=self.account, folder=self).depth(SHALLOW_FOLDERS).get(name=other) except DoesNotExist: raise ErrorFolderNotFound("No subfolder with name '%s'" % other) def __truediv__(self, other): """Support the some_folder / 'child_folder' / 'child_of_child_folder' navigation syntax.""" if other == '..': if not self.parent: raise ValueError('Already at top') return self.parent if other == '.': return self for c in self.children: if c.name == other: return c raise ErrorFolderNotFound("No subfolder with name '%s'" % other) def __repr__(self): return self.__class__.__name__ + \ repr((self.root, self.name, self.total_count, self.unread_count, self.child_folder_count, self.folder_class, self.id, self.changekey)) def __str__(self): return '%s (%s)' % (self.__class__.__name__, self.name) class Folder(BaseFolder): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/folder""" permission_set = PermissionSetField(field_uri='folder:PermissionSet', supported_from=EXCHANGE_2007_SP1) effective_rights = EffectiveRightsField(field_uri='folder:EffectiveRights', is_read_only=True, supported_from=EXCHANGE_2007_SP1) __slots__ = '_root', def __init__(self, **kwargs): self._root = kwargs.pop('root', None) # This is a pointer to the root of the folder hierarchy parent = kwargs.pop('parent', None) if parent: if self.root: if parent.root != self.root: raise ValueError("'parent.root' must match 'root'") else: self.root = parent.root if 'parent_folder_id' in kwargs and parent.id != kwargs['parent_folder_id']: raise ValueError("'parent_folder_id' must match 'parent' ID") kwargs['parent_folder_id'] = ParentFolderId(id=parent.id, changekey=parent.changekey) super().__init__(**kwargs) @property def account(self): if self.root is None: return None return self.root.account @property def root(self): return self._root @root.setter def root(self, value): self._root = value @classmethod def register(cls, *args, **kwargs): if cls is not Folder: raise TypeError('For folders, custom fields must be registered on the Folder class') return super().register(*args, **kwargs) @classmethod def deregister(cls, *args, **kwargs): if cls is not Folder: raise TypeError('For folders, custom fields must be registered on the Folder class') return super().deregister(*args, **kwargs) @classmethod def get_distinguished(cls, root): """Get the distinguished folder for this folder class. :param root: :return: """ try: return cls.resolve( account=root.account, folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True) ) except MISSING_FOLDER_ERRORS: raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID) @property def parent(self): if not self.parent_folder_id: return None if self.parent_folder_id.id == self.id: # Some folders have a parent that references itself. Avoid circular references here return None return self.root.get_folder(self.parent_folder_id) @parent.setter def parent(self, value): if value is None: self.parent_folder_id = None else: if not isinstance(value, BaseFolder): raise ValueError("'value' %r must be a Folder instance" % value) self.root = value.root self.parent_folder_id = ParentFolderId(id=value.id, changekey=value.changekey) def clean(self, version=None): from .roots import RootOfHierarchy super().clean(version=version) if self.root and not isinstance(self.root, RootOfHierarchy): raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root) @classmethod def from_xml_with_root(cls, elem, root): folder = cls.from_xml(elem=elem, account=root.account) folder_cls = cls if cls == Folder: # We were called on the generic Folder class. Try to find a more specific class to return objects as. # # The "FolderClass" element value is the only indication we have in the FindFolder response of which # folder class we should create the folder with. And many folders share the same 'FolderClass' value, e.g. # Inbox and DeletedItems. We want to distinguish between these because otherwise we can't locate the right # folders types for e.g. Account.inbox and Account.trash. # # We should be able to just use the name, but apparently default folder names can be renamed to a set of # localized names using a PowerShell command: # https://docs.microsoft.com/en-us/powershell/module/exchange/client-access/Set-MailboxRegionalConfiguration # # Instead, search for a folder class using the localized name. If none are found, fall back to getting the # folder class by the "FolderClass" value. # # The returned XML may contain neither folder class nor name. In that case, we default to the generic # Folder class. if folder.name: try: # TODO: fld_class.LOCALIZED_NAMES is most definitely neither complete nor authoritative folder_cls = root.folder_cls_from_folder_name(folder_name=folder.name, locale=root.account.locale) log.debug('Folder class %s matches localized folder name %s', folder_cls, folder.name) except KeyError: pass if folder.folder_class and folder_cls == Folder: try: folder_cls = cls.folder_cls_from_container_class(container_class=folder.folder_class) log.debug('Folder class %s matches container class %s (%s)', folder_cls, folder.folder_class, folder.name) except KeyError: pass if folder_cls == Folder: log.debug('Fallback to class Folder (folder_class %s, name %s)', folder.folder_class, folder.name) return folder_cls(root=root, **{f.name: getattr(folder, f.name) for f in folder.FIELDS}) class BaseSubscription(metaclass=abc.ABCMeta): def __init__(self, folder, **subscription_kwargs): self.folder = folder self.subscription_kwargs = subscription_kwargs self.subscription_id = None def __enter__(self): pass def __exit__(self, *args, **kwargs): self.folder.unsubscribe(subscription_id=self.subscription_id) self.subscription_id = None class PullSubscription(BaseSubscription): def __enter__(self): self.subscription_id, watermark = self.folder.subscribe_to_pull(**self.subscription_kwargs) return self.subscription_id, watermark class PushSubscription(BaseSubscription): def __enter__(self): self.subscription_id, watermark = self.folder.subscribe_to_push(**self.subscription_kwargs) return self.subscription_id, watermark def __exit__(self, *args, **kwargs): # Cannot unsubscribe to push subscriptions pass class StreamingSubscription(BaseSubscription): def __enter__(self): self.subscription_id = self.folder.subscribe_to_streaming(**self.subscription_kwargs) return self.subscription_id exchangelib-4.6.1/exchangelib/folders/collections.py000066400000000000000000000535331414601472700226110ustar00rootroot00000000000000import logging from cached_property import threaded_cached_property from .queryset import FOLDER_TRAVERSAL_CHOICES from ..fields import FieldPath, InvalidField from ..items import Persona, ITEM_TRAVERSAL_CHOICES, SHAPE_CHOICES, ID_ONLY from ..properties import CalendarView from ..queryset import QuerySet, SearchableMixIn, Q from ..restriction import Restriction from ..services import FindFolder, GetFolder, FindItem, FindPeople, SyncFolderItems, SyncFolderHierarchy, \ SubscribeToPull, SubscribeToPush, SubscribeToStreaming from ..util import require_account log = logging.getLogger(__name__) class SyncCompleted(Exception): """This is a really ugly way of returning the sync state.""" def __init__(self, sync_state): super().__init__(sync_state) self.sync_state = sync_state class FolderCollection(SearchableMixIn): """A class that implements an API for searching folders.""" # These fields are required in a FindFolder or GetFolder call to properly identify folder types REQUIRED_FOLDER_FIELDS = ('name', 'folder_class') def __init__(self, account, folders): """Implement a search API on a collection of folders. :param account: An Account object :param folders: An iterable of folders, e.g. Folder.walk(), Folder.glob(), or [a.calendar, a.inbox] """ self.account = account self._folders = folders @threaded_cached_property def folders(self): # Resolve the list of folders, in case it's a generator return list(self._folders) def __len__(self): return len(self.folders) def __iter__(self): yield from self.folders def get(self, *args, **kwargs): return QuerySet(self).get(*args, **kwargs) def all(self): return QuerySet(self).all() def none(self): return QuerySet(self).none() def filter(self, *args, **kwargs): """Find items in the folder(s). Non-keyword args may be a list of Q instances. Optional extra keyword arguments follow a Django-like QuerySet filter syntax (see https://docs.djangoproject.com/en/1.10/ref/models/querysets/#field-lookups). We don't support '__year' and other date-related lookups. We also don't support '__endswith' or '__iendswith'. We support the additional '__not' lookup in place of Django's exclude() for simple cases. For more complicated cases you need to create a Q object and use ~Q(). Examples: my_account.inbox.filter(datetime_received__gt=EWSDateTime(2016, 1, 1)) my_account.calendar.filter(start__range=(EWSDateTime(2016, 1, 1), EWSDateTime(2017, 1, 1))) my_account.tasks.filter(subject='Hi mom') my_account.tasks.filter(subject__not='Hi mom') my_account.tasks.filter(subject__contains='Foo') my_account.tasks.filter(subject__icontains='foo') 'endswith' and 'iendswith' could be emulated by searching with 'contains' or 'icontains' and then post-processing items. Fetch the field in question with additional_fields and remove items where the search string is not a postfix. """ return QuerySet(self).filter(*args, **kwargs) def exclude(self, *args, **kwargs): return QuerySet(self).exclude(*args, **kwargs) def people(self): return QuerySet(self).people() def view(self, start, end, max_items=None, *args, **kwargs): """Implement the CalendarView option to FindItem. The difference between 'filter' and 'view' is that 'filter' only returns the master CalendarItem for recurring items, while 'view' unfolds recurring items and returns all CalendarItem occurrences as one would normally expect when presenting a calendar. Supports the same semantics as filter, except for 'start' and 'end' keyword attributes which are both required and behave differently than filter. Here, they denote the start and end of the timespan of the view. All items the overlap the timespan are returned (items that end exactly on 'start' are also returned, for some reason). EWS does not allow combining CalendarView with search restrictions (filter and exclude). 'max_items' defines the maximum number of items returned in this view. Optional. :param start: :param end: :param max_items: (Default value = None) :return: """ qs = QuerySet(self).filter(*args, **kwargs) qs.calendar_view = CalendarView(start=start, end=end, max_items=max_items) return qs def allowed_item_fields(self): # Return non-ID fields of all item classes allowed in this folder type fields = set() for item_model in self.supported_item_models: fields.update(set(item_model.supported_fields(version=self.account.version))) return fields @property def supported_item_models(self): return tuple(item_model for folder in self.folders for item_model in folder.supported_item_models) def validate_item_field(self, field, version): # For each field, check if the field is valid for any of the item models supported by this folder for item_model in self.supported_item_models: try: item_model.validate_field(field=field, version=version) break except InvalidField: continue else: raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models)) def find_items(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None, calendar_view=None, page_size=None, max_items=None, offset=0): """Private method to call the FindItem service. :param q: a Q instance containing any restrictions :param shape: controls whether to return (id, chanegkey) tuples or Item objects. If additional_fields is non-null, we always return Item objects. (Default value = ID_ONLY) :param depth: controls the whether to return soft-deleted items or not. (Default value = None) :param additional_fields: the extra properties we want on the return objects. Default is no properties. Be aware that complex fields can only be fetched with fetch() (i.e. the GetItem service). :param order_fields: the SortOrder fields, if any (Default value = None) :param calendar_view: a CalendarView instance, if any (Default value = None) :param page_size: the requested number of items per page (Default value = None) :param max_items: the max number of items to return (Default value = None) :param offset: the offset relative to the first item in the item collection (Default value = 0) :return: a generator for the returned item IDs or items """ if not self.folders: log.debug('Folder list is empty') return if q.is_never(): log.debug('Query will never return results') return if shape not in SHAPE_CHOICES: raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES)) if depth is None: depth = self._get_default_item_traversal_depth() if depth not in ITEM_TRAVERSAL_CHOICES: raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES)) if additional_fields: for f in additional_fields: self.validate_item_field(field=f, version=self.account.version) if f.field.is_complex: raise ValueError("find_items() does not support field '%s'. Use fetch() instead" % f.field.name) if calendar_view is not None and not isinstance(calendar_view, CalendarView): raise ValueError("'calendar_view' %s must be a CalendarView instance" % calendar_view) # Build up any restrictions if q.is_empty(): restriction = None query_string = None elif q.query_string: restriction = None query_string = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS) else: restriction = Restriction(q, folders=self.folders, applies_to=Restriction.ITEMS) query_string = None log.debug( 'Finding %s items in folders %s (shape: %s, depth: %s, additional_fields: %s, restriction: %s)', self.account, self.folders, shape, depth, additional_fields, restriction.q if restriction else None, ) yield from FindItem(account=self.account, chunk_size=page_size).call( folders=self.folders, additional_fields=additional_fields, restriction=restriction, order_fields=order_fields, shape=shape, query_string=query_string, depth=depth, calendar_view=calendar_view, max_items=calendar_view.max_items if calendar_view else max_items, offset=offset, ) def _get_single_folder(self): if len(self.folders) > 1: raise ValueError('Syncing folder hierarchy can only be done on a single folder') if not self.folders: log.debug('Folder list is empty') return None return self.folders[0] def find_people(self, q, shape=ID_ONLY, depth=None, additional_fields=None, order_fields=None, page_size=None, max_items=None, offset=0): """Private method to call the FindPeople service. :param q: a Q instance containing any restrictions :param shape: controls whether to return (id, chanegkey) tuples or Persona objects. If additional_fields is non-null, we always return Persona objects. (Default value = ID_ONLY) :param depth: controls the whether to return soft-deleted items or not. (Default value = None) :param additional_fields: the extra properties we want on the return objects. Default is no properties. :param order_fields: the SortOrder fields, if any (Default value = None) :param page_size: the requested number of items per page (Default value = None) :param max_items: the max number of items to return (Default value = None) :param offset: the offset relative to the first item in the item collection (Default value = 0) :return: a generator for the returned personas """ folder = self._get_single_folder() if not folder: return if q.is_never(): log.debug('Query will never return results') return if shape not in SHAPE_CHOICES: raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES)) if depth is None: depth = self._get_default_item_traversal_depth() if depth not in ITEM_TRAVERSAL_CHOICES: raise ValueError("'depth' %s must be one of %s" % (depth, ITEM_TRAVERSAL_CHOICES)) if additional_fields: for f in additional_fields: Persona.validate_field(field=f, version=self.account.version) if f.field.is_complex: raise ValueError("find_people() does not support field '%s'" % f.field.name) # Build up any restrictions if q.is_empty(): restriction = None query_string = None elif q.query_string: restriction = None query_string = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS) else: restriction = Restriction(q, folders=[folder], applies_to=Restriction.ITEMS) query_string = None yield from FindPeople(account=self.account, chunk_size=page_size).call( folder=folder, additional_fields=additional_fields, restriction=restriction, order_fields=order_fields, shape=shape, query_string=query_string, depth=depth, max_items=max_items, offset=offset, ) def get_folder_fields(self, target_cls, is_complex=None): return { FieldPath(field=f) for f in target_cls.supported_fields(version=self.account.version) if is_complex is None or f.is_complex is is_complex } def _get_target_cls(self): # We may have root folders that don't support the same set of fields as normal folders. If there is a mix of # both folder types in self.folders, raise an error so we don't risk losing some fields in the query. from .base import Folder from .roots import RootOfHierarchy has_roots = False has_non_roots = False for f in self.folders: if isinstance(f, RootOfHierarchy): if has_non_roots: raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders)) has_roots = True else: if has_roots: raise ValueError('Cannot call GetFolder on a mix of folder types: {}'.format(self.folders)) has_non_roots = True return RootOfHierarchy if has_roots else Folder def _get_default_traversal_depth(self, traversal_attr): unique_depths = {getattr(f, traversal_attr) for f in self.folders} if len(unique_depths) == 1: return unique_depths.pop() raise ValueError( 'Folders in this collection do not have a common %s value. You need to define an explicit traversal depth' 'with QuerySet.depth() (values: %s)' % (traversal_attr, unique_depths) ) def _get_default_item_traversal_depth(self): # When searching folders, some folders require 'Shallow' and others 'Associated' traversal depth. return self._get_default_traversal_depth('DEFAULT_ITEM_TRAVERSAL_DEPTH') def _get_default_folder_traversal_depth(self): # When searching folders, some folders require 'Shallow' and others 'Deep' traversal depth. return self._get_default_traversal_depth('DEFAULT_FOLDER_TRAVERSAL_DEPTH') def resolve(self): # Looks up the folders or folder IDs in the collection and returns full Folder instances with all fields set. from .base import BaseFolder resolveable_folders = [] for f in self.folders: if isinstance(f, BaseFolder) and not f.get_folder_allowed: log.debug('GetFolder not allowed on folder %s. Non-complex fields must be fetched with FindFolder', f) yield f else: resolveable_folders.append(f) # Fetch all properties for the remaining folders of folder IDs additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=None) yield from self.__class__(account=self.account, folders=resolveable_folders).get_folders( additional_fields=additional_fields ) @require_account def find_folders(self, q=None, shape=ID_ONLY, depth=None, additional_fields=None, page_size=None, max_items=None, offset=0): # 'depth' controls whether to return direct children or recurse into sub-folders from .base import BaseFolder, Folder if q is None: q = Q() if not self.folders: log.debug('Folder list is empty') return if q.is_never(): log.debug('Query will never return results') return if q.is_empty(): restriction = None else: restriction = Restriction(q, folders=self.folders, applies_to=Restriction.FOLDERS) if shape not in SHAPE_CHOICES: raise ValueError("'shape' %s must be one of %s" % (shape, SHAPE_CHOICES)) if depth is None: depth = self._get_default_folder_traversal_depth() if depth not in FOLDER_TRAVERSAL_CHOICES: raise ValueError("'depth' %s must be one of %s" % (depth, FOLDER_TRAVERSAL_CHOICES)) if additional_fields is None: # Default to all non-complex properties. Subfolders will always be of class Folder additional_fields = self.get_folder_fields(target_cls=Folder, is_complex=False) else: for f in additional_fields: if f.field.is_complex: raise ValueError("find_folders() does not support field '%s'. Use get_folders()." % f.field.name) # Add required fields additional_fields.update( (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS) ) yield from FindFolder(account=self.account, chunk_size=page_size).call( folders=self.folders, additional_fields=additional_fields, restriction=restriction, shape=shape, depth=depth, max_items=max_items, offset=offset, ) def get_folders(self, additional_fields=None): # Expand folders with their full set of properties from .base import BaseFolder if not self.folders: log.debug('Folder list is empty') return if additional_fields is None: # Default to all complex properties additional_fields = self.get_folder_fields(target_cls=self._get_target_cls(), is_complex=True) # Add required fields additional_fields.update( (FieldPath(field=BaseFolder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS) ) yield from GetFolder(account=self.account).call( folders=self.folders, additional_fields=additional_fields, shape=ID_ONLY, ) def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60): if not self.folders: log.debug('Folder list is empty') return yield from SubscribeToPull(account=self.account).call( folders=self.folders, event_types=event_types, watermark=watermark, timeout=timeout, ) def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None, status_frequency=1): if not self.folders: log.debug('Folder list is empty') return yield from SubscribeToPush(account=self.account).call( folders=self.folders, event_types=event_types, watermark=watermark, status_frequency=status_frequency, url=callback_url, ) def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES): if not self.folders: log.debug('Folder list is empty') return yield from SubscribeToStreaming(account=self.account).call(folders=self.folders, event_types=event_types) def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None): folder = self._get_single_folder() if not folder: return 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 folder.allowed_item_fields(version=self.account.version)} else: for field in only_fields: folder.validate_item_field(field=field, version=self.account.version) # Remove ItemId and ChangeKey. We get them unconditionally additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute} svc = SyncFolderItems(account=self.account) while True: yield from svc.call( folder=folder, shape=ID_ONLY, additional_fields=additional_fields, sync_state=sync_state, ignore=ignore, max_changes_returned=max_changes_returned, sync_scope=sync_scope, ) if svc.sync_state == sync_state: # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here break sync_state = svc.sync_state # Set the new sync state in the next call if svc.includes_last_item_in_range: # Try again if there are more items break raise SyncCompleted(sync_state=svc.sync_state) def sync_hierarchy(self, sync_state=None, only_fields=None): folder = self._get_single_folder() if not folder: return 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 folder.supported_fields(version=self.account.version)} else: for f in only_fields: folder.validate_field(field=f, version=self.account.version) # Remove ItemId and ChangeKey. We get them unconditionally additional_fields = {f for f in folder.normalize_fields(fields=only_fields) if not f.field.is_attribute} # Add required fields additional_fields.update( (FieldPath(field=folder.get_field_by_fieldname(f)) for f in self.REQUIRED_FOLDER_FIELDS) ) svc = SyncFolderHierarchy(account=self.account) while True: yield from svc.call( folder=folder, shape=ID_ONLY, additional_fields=additional_fields, sync_state=sync_state, ) if svc.sync_state == sync_state: # We sometimes get the same sync_state back, even though includes_last_item_in_range is False. Stop here break sync_state = svc.sync_state # Set the new sync state in the next call if svc.includes_last_item_in_range: # Try again if there are more items break raise SyncCompleted(sync_state=svc.sync_state) exchangelib-4.6.1/exchangelib/folders/known_folders.py000066400000000000000000000406221414601472700231400ustar00rootroot00000000000000from .base import Folder from .collections import FolderCollection from ..items import CalendarItem, Contact, Message, Task, DistributionList, MeetingRequest, MeetingResponse, \ MeetingCancellation, ITEM_CLASSES, ASSOCIATED from ..version import EXCHANGE_2010_SP1, EXCHANGE_2013, EXCHANGE_2013_SP1 class Calendar(Folder): """An interface for the Exchange calendar.""" DISTINGUISHED_FOLDER_ID = 'calendar' CONTAINER_CLASS = 'IPF.Appointment' supported_item_models = (CalendarItem,) LOCALIZED_NAMES = { 'da_DK': ('Kalender',), 'de_DE': ('Kalender',), 'en_US': ('Calendar',), 'es_ES': ('Calendario',), 'fr_CA': ('Calendrier',), 'nl_NL': ('Agenda',), 'ru_RU': ('Календарь',), 'sv_SE': ('Kalender',), 'zh_CN': ('日历',), } def view(self, *args, **kwargs): return FolderCollection(account=self.account, folders=[self]).view(*args, **kwargs) class DeletedItems(Folder): DISTINGUISHED_FOLDER_ID = 'deleteditems' CONTAINER_CLASS = 'IPF.Note' supported_item_models = ITEM_CLASSES LOCALIZED_NAMES = { 'da_DK': ('Slettet post',), 'de_DE': ('Gelöschte Elemente',), 'en_US': ('Deleted Items',), 'es_ES': ('Elementos eliminados',), 'fr_CA': ('Éléments supprimés',), 'nl_NL': ('Verwijderde items',), 'ru_RU': ('Удаленные',), 'sv_SE': ('Borttaget',), 'zh_CN': ('已删除邮件',), } class Messages(Folder): CONTAINER_CLASS = 'IPF.Note' supported_item_models = (Message, MeetingRequest, MeetingResponse, MeetingCancellation) class Drafts(Messages): DISTINGUISHED_FOLDER_ID = 'drafts' LOCALIZED_NAMES = { 'da_DK': ('Kladder',), 'de_DE': ('Entwürfe',), 'en_US': ('Drafts',), 'es_ES': ('Borradores',), 'fr_CA': ('Brouillons',), 'nl_NL': ('Concepten',), 'ru_RU': ('Черновики',), 'sv_SE': ('Utkast',), 'zh_CN': ('草稿',), } class Inbox(Messages): DISTINGUISHED_FOLDER_ID = 'inbox' LOCALIZED_NAMES = { 'da_DK': ('Indbakke',), 'de_DE': ('Posteingang',), 'en_US': ('Inbox',), 'es_ES': ('Bandeja de entrada',), 'fr_CA': ('Boîte de réception',), 'nl_NL': ('Postvak IN',), 'ru_RU': ('Входящие',), 'sv_SE': ('Inkorgen',), 'zh_CN': ('收件箱',), } class Outbox(Messages): DISTINGUISHED_FOLDER_ID = 'outbox' LOCALIZED_NAMES = { 'da_DK': ('Udbakke',), 'de_DE': ('Postausgang',), 'en_US': ('Outbox',), 'es_ES': ('Bandeja de salida',), 'fr_CA': (u"Boîte d'envoi",), 'nl_NL': ('Postvak UIT',), 'ru_RU': ('Исходящие',), 'sv_SE': ('Utkorgen',), 'zh_CN': ('发件箱',), } class SentItems(Messages): DISTINGUISHED_FOLDER_ID = 'sentitems' LOCALIZED_NAMES = { 'da_DK': ('Sendt post',), 'de_DE': ('Gesendete Elemente',), 'en_US': ('Sent Items',), 'es_ES': ('Elementos enviados',), 'fr_CA': ('Éléments envoyés',), 'nl_NL': ('Verzonden items',), 'ru_RU': ('Отправленные',), 'sv_SE': ('Skickat',), 'zh_CN': ('已发送邮件',), } class JunkEmail(Messages): DISTINGUISHED_FOLDER_ID = 'junkemail' LOCALIZED_NAMES = { 'da_DK': ('Uønsket e-mail',), 'de_DE': ('Junk-E-Mail',), 'en_US': ('Junk E-mail',), 'es_ES': ('Correo no deseado',), 'fr_CA': ('Courrier indésirables',), 'nl_NL': ('Ongewenste e-mail',), 'ru_RU': ('Нежелательная почта',), 'sv_SE': ('Skräppost',), 'zh_CN': ('垃圾邮件',), } class Tasks(Folder): DISTINGUISHED_FOLDER_ID = 'tasks' CONTAINER_CLASS = 'IPF.Task' supported_item_models = (Task,) LOCALIZED_NAMES = { 'da_DK': ('Opgaver',), 'de_DE': ('Aufgaben',), 'en_US': ('Tasks',), 'es_ES': ('Tareas',), 'fr_CA': ('Tâches',), 'nl_NL': ('Taken',), 'ru_RU': ('Задачи',), 'sv_SE': ('Uppgifter',), 'zh_CN': ('任务',), } class Contacts(Folder): DISTINGUISHED_FOLDER_ID = 'contacts' CONTAINER_CLASS = 'IPF.Contact' supported_item_models = (Contact, DistributionList) LOCALIZED_NAMES = { 'da_DK': ('Kontaktpersoner',), 'de_DE': ('Kontakte',), 'en_US': ('Contacts',), 'es_ES': ('Contactos',), 'fr_CA': ('Contacts',), 'nl_NL': ('Contactpersonen',), 'ru_RU': ('Контакты',), 'sv_SE': ('Kontakter',), 'zh_CN': ('联系人',), } class WellknownFolder(Folder): """A base class to use until we have a more specific folder implementation for this folder.""" supported_item_models = ITEM_CLASSES class AdminAuditLogs(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'adminauditlogs' supported_from = EXCHANGE_2013 get_folder_allowed = False class ArchiveDeletedItems(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'archivedeleteditems' supported_from = EXCHANGE_2010_SP1 class ArchiveInbox(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'archiveinbox' supported_from = EXCHANGE_2013_SP1 class ArchiveMsgFolderRoot(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'archivemsgfolderroot' supported_from = EXCHANGE_2010_SP1 class ArchiveRecoverableItemsDeletions(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemsdeletions' supported_from = EXCHANGE_2010_SP1 class ArchiveRecoverableItemsPurges(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemspurges' supported_from = EXCHANGE_2010_SP1 class ArchiveRecoverableItemsRoot(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemsroot' supported_from = EXCHANGE_2010_SP1 class ArchiveRecoverableItemsVersions(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'archiverecoverableitemsversions' supported_from = EXCHANGE_2010_SP1 class Conflicts(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'conflicts' supported_from = EXCHANGE_2013 class ConversationHistory(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'conversationhistory' supported_from = EXCHANGE_2013 class Directory(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'directory' supported_from = EXCHANGE_2013_SP1 class Favorites(WellknownFolder): CONTAINER_CLASS = 'IPF.Note' DISTINGUISHED_FOLDER_ID = 'favorites' supported_from = EXCHANGE_2013 class IMContactList(WellknownFolder): CONTAINER_CLASS = 'IPF.Contact.MOC.ImContactList' DISTINGUISHED_FOLDER_ID = 'imcontactlist' supported_from = EXCHANGE_2013 class Journal(WellknownFolder): CONTAINER_CLASS = 'IPF.Journal' DISTINGUISHED_FOLDER_ID = 'journal' class LocalFailures(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'localfailures' supported_from = EXCHANGE_2013 class MsgFolderRoot(WellknownFolder): """Also known as the 'Top of Information Store' folder.""" DISTINGUISHED_FOLDER_ID = 'msgfolderroot' LOCALIZED_NAMES = { 'zh_CN': ('信息存储顶部',), } class MyContacts(WellknownFolder): CONTAINER_CLASS = 'IPF.Note' DISTINGUISHED_FOLDER_ID = 'mycontacts' supported_from = EXCHANGE_2013 class Notes(WellknownFolder): CONTAINER_CLASS = 'IPF.StickyNote' DISTINGUISHED_FOLDER_ID = 'notes' LOCALIZED_NAMES = { 'da_DK': ('Noter',), } class PeopleConnect(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'peopleconnect' supported_from = EXCHANGE_2013 class QuickContacts(WellknownFolder): CONTAINER_CLASS = 'IPF.Contact.MOC.QuickContacts' DISTINGUISHED_FOLDER_ID = 'quickcontacts' supported_from = EXCHANGE_2013 class RecipientCache(Contacts): DISTINGUISHED_FOLDER_ID = 'recipientcache' CONTAINER_CLASS = 'IPF.Contact.RecipientCache' supported_from = EXCHANGE_2013 LOCALIZED_NAMES = {} class RecoverableItemsDeletions(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'recoverableitemsdeletions' supported_from = EXCHANGE_2010_SP1 class RecoverableItemsPurges(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'recoverableitemspurges' supported_from = EXCHANGE_2010_SP1 class RecoverableItemsRoot(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'recoverableitemsroot' supported_from = EXCHANGE_2010_SP1 class RecoverableItemsVersions(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'recoverableitemsversions' supported_from = EXCHANGE_2010_SP1 class SearchFolders(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'searchfolders' class ServerFailures(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'serverfailures' supported_from = EXCHANGE_2013 class SyncIssues(WellknownFolder): CONTAINER_CLASS = 'IPF.Note' DISTINGUISHED_FOLDER_ID = 'syncissues' supported_from = EXCHANGE_2013 class ToDoSearch(WellknownFolder): CONTAINER_CLASS = 'IPF.Task' DISTINGUISHED_FOLDER_ID = 'todosearch' supported_from = EXCHANGE_2013 LOCALIZED_NAMES = { None: ('To-Do Search',), } class VoiceMail(WellknownFolder): DISTINGUISHED_FOLDER_ID = 'voicemail' CONTAINER_CLASS = 'IPF.Note.Microsoft.Voicemail' LOCALIZED_NAMES = { None: ('Voice Mail',), } class NonDeletableFolderMixin: """A mixin for non-wellknown folders than that are not deletable.""" @property def is_deletable(self): return False class AllContacts(NonDeletableFolderMixin, Contacts): CONTAINER_CLASS = 'IPF.Note' LOCALIZED_NAMES = { None: ('AllContacts',), } class AllItems(NonDeletableFolderMixin, Folder): CONTAINER_CLASS = 'IPF' LOCALIZED_NAMES = { None: ('AllItems',), } class Audits(NonDeletableFolderMixin, Folder): LOCALIZED_NAMES = { None: ('Audits',), } get_folder_allowed = False class CalendarLogging(NonDeletableFolderMixin, Folder): LOCALIZED_NAMES = { None: ('Calendar Logging',), } class CommonViews(NonDeletableFolderMixin, Folder): DEFAULT_ITEM_TRAVERSAL_DEPTH = ASSOCIATED LOCALIZED_NAMES = { None: ('Common Views',), } class Companies(NonDeletableFolderMixin, Contacts): DISTINGUISHED_FOLDER_ID = None CONTAINTER_CLASS = 'IPF.Contact.Company' LOCALIZED_NAMES = { None: ('Companies',), } class ConversationSettings(NonDeletableFolderMixin, Folder): CONTAINER_CLASS = 'IPF.Configuration' LOCALIZED_NAMES = { 'da_DK': ('Indstillinger for samtalehandlinger',), } class DefaultFoldersChangeHistory(NonDeletableFolderMixin, Folder): CONTAINER_CLASS = 'IPM.DefaultFolderHistoryItem' LOCALIZED_NAMES = { None: ('DefaultFoldersChangeHistory',), } class DeferredAction(NonDeletableFolderMixin, Folder): LOCALIZED_NAMES = { None: ('Deferred Action',), } class ExchangeSyncData(NonDeletableFolderMixin, Folder): LOCALIZED_NAMES = { None: ('ExchangeSyncData',), } class Files(NonDeletableFolderMixin, Folder): CONTAINER_CLASS = 'IPF.Files' LOCALIZED_NAMES = { 'da_DK': ('Filer',), } class FreebusyData(NonDeletableFolderMixin, Folder): LOCALIZED_NAMES = { None: ('Freebusy Data',), } class Friends(NonDeletableFolderMixin, Contacts): CONTAINER_CLASS = 'IPF.Note' LOCALIZED_NAMES = { 'de_DE': ('Bekannte',), } class GALContacts(NonDeletableFolderMixin, Contacts): DISTINGUISHED_FOLDER_ID = None CONTAINER_CLASS = 'IPF.Contact.GalContacts' LOCALIZED_NAMES = { None: ('GAL Contacts',), } class GraphAnalytics(NonDeletableFolderMixin, Folder): CONTAINER_CLASS = 'IPF.StoreItem.GraphAnalytics' LOCALIZED_NAMES = { None: ('GraphAnalytics',), } class Location(NonDeletableFolderMixin, Folder): LOCALIZED_NAMES = { None: ('Location',), } class MailboxAssociations(NonDeletableFolderMixin, Folder): LOCALIZED_NAMES = { None: ('MailboxAssociations',), } class MyContactsExtended(NonDeletableFolderMixin, Contacts): CONTAINER_CLASS = 'IPF.Note' LOCALIZED_NAMES = { None: ('MyContactsExtended',), } class OrganizationalContacts(NonDeletableFolderMixin, Contacts): DISTINGUISHED_FOLDER_ID = None CONTAINTER_CLASS = 'IPF.Contact.OrganizationalContacts' LOCALIZED_NAMES = { None: ('Organizational Contacts',), } class ParkedMessages(NonDeletableFolderMixin, Folder): CONTAINER_CLASS = None LOCALIZED_NAMES = { None: ('ParkedMessages',), } class PassThroughSearchResults(NonDeletableFolderMixin, Folder): CONTAINER_CLASS = 'IPF.StoreItem.PassThroughSearchResults' LOCALIZED_NAMES = { None: ('Pass-Through Search Results',), } class PeopleCentricConversationBuddies(NonDeletableFolderMixin, Contacts): DISTINGUISHED_FOLDER_ID = None CONTAINTER_CLASS = 'IPF.Contact.PeopleCentricConversationBuddies' LOCALIZED_NAMES = { None: ('PeopleCentricConversation Buddies',), } class PdpProfileV2Secured(NonDeletableFolderMixin, Folder): CONTAINER_CLASS = 'IPF.StoreItem.PdpProfileSecured' LOCALIZED_NAMES = { None: ('PdpProfileV2Secured',), } class Reminders(NonDeletableFolderMixin, Folder): CONTAINER_CLASS = 'Outlook.Reminder' LOCALIZED_NAMES = { 'da_DK': ('Påmindelser',), } class RSSFeeds(NonDeletableFolderMixin, Folder): CONTAINER_CLASS = 'IPF.Note.OutlookHomepage' LOCALIZED_NAMES = { None: ('RSS Feeds',), } class Schedule(NonDeletableFolderMixin, Folder): LOCALIZED_NAMES = { None: ('Schedule',), } class Sharing(NonDeletableFolderMixin, Folder): CONTAINER_CLASS = 'IPF.Note' LOCALIZED_NAMES = { None: ('Sharing',), } class Shortcuts(NonDeletableFolderMixin, Folder): LOCALIZED_NAMES = { None: ('Shortcuts',), } class Signal(NonDeletableFolderMixin, Folder): CONTAINER_CLASS = 'IPF.StoreItem.Signal' LOCALIZED_NAMES = { None: ('Signal',), } class SmsAndChatsSync(NonDeletableFolderMixin, Folder): CONTAINER_CLASS = 'IPF.SmsAndChatsSync' LOCALIZED_NAMES = { None: ('SmsAndChatsSync',), } class SpoolerQueue(NonDeletableFolderMixin, Folder): LOCALIZED_NAMES = { None: ('Spooler Queue',), } class System(NonDeletableFolderMixin, Folder): LOCALIZED_NAMES = { None: ('System',), } get_folder_allowed = False class System1(NonDeletableFolderMixin, Folder): LOCALIZED_NAMES = { None: ('System1',), } get_folder_allowed = False class TemporarySaves(NonDeletableFolderMixin, Folder): LOCALIZED_NAMES = { None: ('TemporarySaves',), } class Views(NonDeletableFolderMixin, Folder): LOCALIZED_NAMES = { None: ('Views',), } class WorkingSet(NonDeletableFolderMixin, Folder): LOCALIZED_NAMES = { None: ('Working Set',), } # Folders that return 'ErrorDeleteDistinguishedFolder' when we try to delete them. I can't find any official docs # listing these folders. NON_DELETABLE_FOLDERS = [ AllContacts, AllItems, Audits, CalendarLogging, CommonViews, Companies, ConversationSettings, DefaultFoldersChangeHistory, DeferredAction, ExchangeSyncData, FreebusyData, Files, Friends, GALContacts, GraphAnalytics, Location, MailboxAssociations, MyContactsExtended, OrganizationalContacts, ParkedMessages, PassThroughSearchResults, PeopleCentricConversationBuddies, PdpProfileV2Secured, Reminders, RSSFeeds, Schedule, Sharing, Shortcuts, Signal, SmsAndChatsSync, SpoolerQueue, System, System1, TemporarySaves, Views, WorkingSet, ] WELLKNOWN_FOLDERS_IN_ROOT = [ AdminAuditLogs, Calendar, Conflicts, Contacts, ConversationHistory, DeletedItems, Directory, Drafts, Favorites, IMContactList, Inbox, Journal, JunkEmail, LocalFailures, MsgFolderRoot, MyContacts, Notes, Outbox, PeopleConnect, QuickContacts, RecipientCache, RecoverableItemsDeletions, RecoverableItemsPurges, RecoverableItemsRoot, RecoverableItemsVersions, SearchFolders, SentItems, ServerFailures, SyncIssues, Tasks, ToDoSearch, VoiceMail, ] WELLKNOWN_FOLDERS_IN_ARCHIVE_ROOT = [ ArchiveDeletedItems, ArchiveInbox, ArchiveMsgFolderRoot, ArchiveRecoverableItemsDeletions, ArchiveRecoverableItemsPurges, ArchiveRecoverableItemsRoot, ArchiveRecoverableItemsVersions, ] exchangelib-4.6.1/exchangelib/folders/queryset.py000066400000000000000000000152401414601472700221450ustar00rootroot00000000000000import logging from copy import deepcopy from ..properties import InvalidField, FolderId from ..queryset import DoesNotExist, MultipleObjectsReturned from ..restriction import Q # Traversal enums SHALLOW = 'Shallow' SOFT_DELETED = 'SoftDeleted' DEEP = 'Deep' FOLDER_TRAVERSAL_CHOICES = (SHALLOW, DEEP, SOFT_DELETED) log = logging.getLogger(__name__) class FolderQuerySet: """A QuerySet-like class for finding subfolders of a folder collection.""" def __init__(self, folder_collection): from .collections import FolderCollection if not isinstance(folder_collection, FolderCollection): raise ValueError("'folder_collection' %r must be a FolderCollection instance" % folder_collection) self.folder_collection = folder_collection self.q = Q() # Default to no restrictions self.only_fields = None self._depth = None def _copy_cls(self): return self.__class__(folder_collection=self.folder_collection) def _copy_self(self): """Chaining operations must make a copy of self before making any modifications.""" new_qs = self._copy_cls() new_qs.q = deepcopy(self.q) new_qs.only_fields = self.only_fields new_qs._depth = self._depth return new_qs def only(self, *args): """Restrict the fields returned. 'name' and 'folder_class' are always returned.""" from .base import Folder # Subfolders will always be of class Folder all_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=None) all_fields.update(Folder.attribute_fields()) only_fields = [] for arg in args: for field_path in all_fields: if field_path.field.name == arg: only_fields.append(field_path) break else: raise InvalidField("Unknown field %r on folders %s" % (arg, self.folder_collection.folders)) new_qs = self._copy_self() new_qs.only_fields = only_fields return new_qs def depth(self, depth): """Specify the search depth. Possible values are: SHALLOW or DEEP. :param depth: """ new_qs = self._copy_self() new_qs._depth = depth return new_qs def get(self, *args, **kwargs): """Return the query result as exactly one item. Raises DoesNotExist if there are no results, and MultipleObjectsReturned if there are multiple results. """ from .collections import FolderCollection if not args and set(kwargs) in ({'id'}, {'id', 'changekey'}): folders = list(FolderCollection( account=self.folder_collection.account, folders=[FolderId(**kwargs)] ).resolve()) elif args or kwargs: folders = list(self.filter(*args, **kwargs)) else: folders = list(self.all()) if not folders: raise DoesNotExist('Could not find a child folder matching the query') if len(folders) != 1: raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders) f = folders[0] if isinstance(f, Exception): raise f return f def all(self): """ """ new_qs = self._copy_self() return new_qs def filter(self, *args, **kwargs): """Add restrictions to the folder search.""" new_qs = self._copy_self() q = Q(*args, **kwargs) new_qs.q = new_qs.q & q return new_qs def __iter__(self): return self._query() def _query(self): from .base import Folder from .collections import FolderCollection if self.only_fields is None: # Subfolders will always be of class Folder non_complex_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=False) complex_fields = self.folder_collection.get_folder_fields(target_cls=Folder, is_complex=True) else: non_complex_fields = {f for f in self.only_fields if not f.field.is_complex} complex_fields = {f for f in self.only_fields if f.field.is_complex} # First, fetch all non-complex fields using FindFolder. We do this because some folders do not support # GetFolder but we still want to get as much information as possible. folders = self.folder_collection.find_folders(q=self.q, depth=self._depth, additional_fields=non_complex_fields) if not complex_fields: yield from folders return # Fetch all properties for the found folders resolveable_folders = [] for f in folders: if not f.get_folder_allowed: log.debug('GetFolder not allowed on folder %s. Non-complex fields must be fetched with FindFolder', f) yield f else: resolveable_folders.append(f) # Get the complex fields using GetFolder, for the folders that support it, and add the extra field values complex_folders = FolderCollection( account=self.folder_collection.account, folders=resolveable_folders ).get_folders(additional_fields=complex_fields) for f, complex_f in zip(resolveable_folders, complex_folders): if isinstance(f, Exception): yield f continue if isinstance(complex_f, Exception): yield complex_f continue # Add the extra field values to the folders we fetched with find_folders() if f.__class__ != complex_f.__class__: raise ValueError('Type mismatch: %s vs %s' % (f, complex_f)) for complex_field in complex_fields: field_name = complex_field.field.name setattr(f, field_name, getattr(complex_f, field_name)) yield f class SingleFolderQuerySet(FolderQuerySet): """A helper class with simpler argument types.""" def __init__(self, account, folder): from .collections import FolderCollection folder_collection = FolderCollection(account=account, folders=[folder]) super().__init__(folder_collection=folder_collection) def _copy_cls(self): return self.__class__(account=self.folder_collection.account, folder=self.folder_collection.folders[0]) def resolve(self): folders = list(self.folder_collection.resolve()) if not folders: raise DoesNotExist('Could not find a folder matching the query') if len(folders) != 1: raise MultipleObjectsReturned('Expected result length 1, but got %s' % folders) f = folders[0] if isinstance(f, Exception): raise f return f exchangelib-4.6.1/exchangelib/folders/roots.py000066400000000000000000000343201414601472700214320ustar00rootroot00000000000000import logging from threading import Lock from .base import BaseFolder, MISSING_FOLDER_ERRORS from .collections import FolderCollection from .known_folders import MsgFolderRoot, NON_DELETABLE_FOLDERS, WELLKNOWN_FOLDERS_IN_ROOT, \ WELLKNOWN_FOLDERS_IN_ARCHIVE_ROOT from .queryset import SingleFolderQuerySet, SHALLOW from ..errors import ErrorAccessDenied, ErrorFolderNotFound, ErrorInvalidOperation from ..fields import EffectiveRightsField from ..properties import EWSMeta from ..version import EXCHANGE_2007_SP1, EXCHANGE_2010_SP1 log = logging.getLogger(__name__) class RootOfHierarchy(BaseFolder, metaclass=EWSMeta): """Base class for folders that implement the root of a folder hierarchy.""" # A list of wellknown, or "distinguished", folders that are belong in this folder hierarchy. See # https://docs.microsoft.com/en-us/dotnet/api/microsoft.exchange.webservices.data.wellknownfoldername # and https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid # 'RootOfHierarchy' subclasses must not be in this list. WELLKNOWN_FOLDERS = [] _subfolders_lock = Lock() # This folder type also has 'folder:PermissionSet' on some server versions, but requesting it sometimes causes # 'ErrorAccessDenied', as reported by some users. Ignore it entirely for root folders - it's usefulness is # deemed minimal at best. effective_rights = EffectiveRightsField(field_uri='folder:EffectiveRights', is_read_only=True, supported_from=EXCHANGE_2007_SP1) __slots__ = '_account', '_subfolders' # A special folder that acts as the top of a folder hierarchy. Finds and caches subfolders at arbitrary depth. def __init__(self, **kwargs): self._account = kwargs.pop('account', None) # A pointer back to the account holding the folder hierarchy super().__init__(**kwargs) self._subfolders = None # See self._folders_map() @property def account(self): return self._account @property def root(self): return self @property def parent(self): return None @classmethod def register(cls, *args, **kwargs): if cls is not RootOfHierarchy: raise TypeError('For folder roots, custom fields must be registered on the RootOfHierarchy class') return super().register(*args, **kwargs) @classmethod def deregister(cls, *args, **kwargs): if cls is not RootOfHierarchy: raise TypeError('For folder roots, custom fields must be registered on the RootOfHierarchy class') return super().deregister(*args, **kwargs) def get_folder(self, folder): if not folder.id: raise ValueError("'folder' must have an ID") return self._folders_map.get(folder.id, None) def add_folder(self, folder): if not folder.id: raise ValueError("'folder' must have an ID") self._folders_map[folder.id] = folder def update_folder(self, folder): if not folder.id: raise ValueError("'folder' must have an ID") self._folders_map[folder.id] = folder def remove_folder(self, folder): if not folder.id: raise ValueError("'folder' must have an ID") try: del self._folders_map[folder.id] except KeyError: pass def clear_cache(self): with self._subfolders_lock: self._subfolders = None def get_children(self, folder): for f in self._folders_map.values(): if not f.parent: continue if f.parent.id == folder.id: yield f @classmethod def get_distinguished(cls, account): """Get the distinguished folder for this folder class. :param account: """ if not cls.DISTINGUISHED_FOLDER_ID: raise ValueError('Class %s must have a DISTINGUISHED_FOLDER_ID value' % cls) try: return cls.resolve( account=account, folder=cls(account=account, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True) ) except MISSING_FOLDER_ERRORS: raise ErrorFolderNotFound('Could not find distinguished folder %s' % cls.DISTINGUISHED_FOLDER_ID) def get_default_folder(self, folder_cls): """Return the distinguished folder instance of type folder_cls belonging to this account. If no distinguished folder was found, try as best we can to return the default folder of type 'folder_cls' """ if not folder_cls.DISTINGUISHED_FOLDER_ID: raise ValueError("'folder_cls' %s must have a DISTINGUISHED_FOLDER_ID value" % folder_cls) # Use cached distinguished folder instance, but only if cache has already been prepped. This is an optimization # for accessing e.g. 'account.contacts' without fetching all folders of the account. if self._subfolders is not None: for f in self._folders_map.values(): # Require exact class, to not match subclasses, e.g. RecipientCache instead of Contacts if f.__class__ == folder_cls and f.is_distinguished: log.debug('Found cached distinguished %s folder', folder_cls) return f try: log.debug('Requesting distinguished %s folder explicitly', folder_cls) return folder_cls.get_distinguished(root=self) except ErrorAccessDenied: # Maybe we just don't have GetFolder access? Try FindItems instead log.debug('Testing default %s folder with FindItem', folder_cls) fld = folder_cls(root=self, name=folder_cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True) fld.test_access() return self._folders_map.get(fld.id, fld) # Use cached instance if available except MISSING_FOLDER_ERRORS: # The Exchange server does not return a distinguished folder of this type pass raise ErrorFolderNotFound('No usable default %s folders' % folder_cls) @property def _folders_map(self): if self._subfolders is not None: return self._subfolders with self._subfolders_lock: # Map root, and all subfolders of root, at arbitrary depth by folder ID. First get distinguished folders, # so we are sure to apply the correct Folder class, then fetch all subfolders of this root. folders_map = {self.id: self} distinguished_folders = [ cls(root=self, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True) for cls in self.WELLKNOWN_FOLDERS if cls.get_folder_allowed and cls.supports_version(self.account.version) ] for f in FolderCollection(account=self.account, folders=distinguished_folders).resolve(): if isinstance(f, MISSING_FOLDER_ERRORS): # This is just a distinguished folder the server does not have continue if isinstance(f, ErrorInvalidOperation): # This is probably a distinguished folder the server does not have. We previously tested the exact # error message (f.value), but some Exchange servers return localized error messages, so that's not # possible to do reliably. continue if isinstance(f, ErrorAccessDenied): # We may not have GetFolder access, either to this folder or at all continue if isinstance(f, Exception): raise f folders_map[f.id] = f for f in SingleFolderQuerySet(account=self.account, folder=self).depth( self.DEFAULT_FOLDER_TRAVERSAL_DEPTH ).all(): if isinstance(f, ErrorAccessDenied): # We may not have FindFolder access, or GetFolder access, either to this folder or at all continue if isinstance(f, Exception): raise f if f.id in folders_map: # Already exists. Probably a distinguished folder continue folders_map[f.id] = f self._subfolders = folders_map return folders_map @classmethod def from_xml(cls, elem, account): kwargs = cls._kwargs_from_elem(elem=elem, account=account) cls._clear(elem) return cls(account=account, **kwargs) @classmethod def folder_cls_from_folder_name(cls, folder_name, locale): """Return the folder class that matches a localized folder name. :param folder_name: :param locale: a string, e.g. 'da_DK' """ for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS: if folder_name.lower() in folder_cls.localized_names(locale): return folder_cls raise KeyError() def __repr__(self): # Let's not create an infinite loop when printing self.root return self.__class__.__name__ + \ repr((self.account, '[self]', self.name, self.total_count, self.unread_count, self.child_folder_count, self.folder_class, self.id, self.changekey)) class Root(RootOfHierarchy): """The root of the standard folder hierarchy.""" DISTINGUISHED_FOLDER_ID = 'root' WELLKNOWN_FOLDERS = WELLKNOWN_FOLDERS_IN_ROOT @property def tois(self): # 'Top of Information Store' is a folder available in some Exchange accounts. It usually contains the # distinguished folders belonging to the account (inbox, calendar, trash etc.). return self.get_default_folder(MsgFolderRoot) def get_default_folder(self, folder_cls): try: return super().get_default_folder(folder_cls) except MISSING_FOLDER_ERRORS: pass # Try to pick a suitable default folder. we do this by: # 1. Searching the full folder list for a folder with the distinguished folder name # 2. Searching TOIS for a direct child folder of the same type that is marked as distinguished # 3. Searching TOIS for a direct child folder of the same type that is has a localized name # 4. Searching root for a direct child folder of the same type that is marked as distinguished # 5. Searching root for a direct child folder of the same type that is has a localized name log.debug('Searching default %s folder in full folder list', folder_cls) for f in self._folders_map.values(): # Require exact class to not match e.g. RecipientCache instead of Contacts if f.__class__ == folder_cls and f.has_distinguished_name: log.debug('Found cached %s folder with default distinguished name', folder_cls) return f # Try direct children of TOIS first, unless we're trying to get the TOIS folder. TOIS might not exist. if folder_cls != MsgFolderRoot: try: return self._get_candidate(folder_cls=folder_cls, folder_coll=self.tois.children) except MISSING_FOLDER_ERRORS: # No candidates, or TOIS does not exist, or we don't have access pass # No candidates in TOIS. Try direct children of root. return self._get_candidate(folder_cls=folder_cls, folder_coll=self.children) def _get_candidate(self, folder_cls, folder_coll): # Get a single the folder of the same type in folder_coll same_type = [f for f in folder_coll if f.__class__ == folder_cls] are_distinguished = [f for f in same_type if f.is_distinguished] if are_distinguished: candidates = are_distinguished else: candidates = [f for f in same_type if f.name.lower() in folder_cls.localized_names(self.account.locale)] if candidates: if len(candidates) > 1: raise ValueError( 'Multiple possible default %s folders: %s' % (folder_cls, [f.name for f in candidates]) ) if candidates[0].is_distinguished: log.debug('Found cached distinguished %s folder', folder_cls) else: log.debug('Found cached %s folder with localized name', folder_cls) return candidates[0] raise ErrorFolderNotFound('No usable default %s folders' % folder_cls) class PublicFoldersRoot(RootOfHierarchy): """The root of the public folders hierarchy. Not available on all mailboxes.""" DISTINGUISHED_FOLDER_ID = 'publicfoldersroot' DEFAULT_FOLDER_TRAVERSAL_DEPTH = SHALLOW supported_from = EXCHANGE_2007_SP1 def get_children(self, folder): # EWS does not allow deep traversal of public folders, so self._folders_map will only populate the top-level # subfolders. To traverse public folders at arbitrary depth, we need to get child folders on demand. # Let's check if this folder already has any cached children. If so, assume we can just return those. children = list(super().get_children(folder=folder)) if children: # Return a generator like our parent does yield from children return # Also return early if the server told us that there are no child folders. if folder.child_folder_count == 0: return children_map = {} try: for f in SingleFolderQuerySet(account=self.account, folder=folder).depth( self.DEFAULT_FOLDER_TRAVERSAL_DEPTH ).all(): if isinstance(f, Exception): raise f children_map[f.id] = f except ErrorAccessDenied: # No access to this folder pass # Let's update the cache atomically, to avoid partial reads of the cache. with self._subfolders_lock: self._subfolders.update(children_map) # Child folders have been cached now. Try super().get_children() again. yield from super().get_children(folder=folder) class ArchiveRoot(RootOfHierarchy): """The root of the archive folders hierarchy. Not available on all mailboxes.""" DISTINGUISHED_FOLDER_ID = 'archiveroot' supported_from = EXCHANGE_2010_SP1 WELLKNOWN_FOLDERS = WELLKNOWN_FOLDERS_IN_ARCHIVE_ROOT exchangelib-4.6.1/exchangelib/indexed_properties.py000066400000000000000000000053411414601472700225230ustar00rootroot00000000000000import logging from .fields import EmailSubField, LabelField, SubField, NamedSubField, Choice from .properties import EWSElement, EWSMeta log = logging.getLogger(__name__) class IndexedElement(EWSElement, metaclass=EWSMeta): """Base class for all classes that implement an indexed element.""" LABEL_CHOICES = () class SingleFieldIndexedElement(IndexedElement, metaclass=EWSMeta): """Base class for all classes that implement an indexed element with a single field.""" @classmethod def value_field(cls, version=None): fields = cls.supported_fields(version=version) if len(fields) != 1: raise ValueError('This class must have only one field (found %s)' % (fields,)) return fields[0] class EmailAddress(SingleFieldIndexedElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/entry-emailaddress""" ELEMENT_NAME = 'Entry' LABEL_CHOICES = ('EmailAddress1', 'EmailAddress2', 'EmailAddress3') label = LabelField(field_uri='Key', choices={Choice(c) for c in LABEL_CHOICES}, default=LABEL_CHOICES[0]) email = EmailSubField(is_required=True) class PhoneNumber(SingleFieldIndexedElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/entry-phonenumber""" ELEMENT_NAME = 'Entry' LABEL_CHOICES = ( 'AssistantPhone', 'BusinessFax', 'BusinessPhone', 'BusinessPhone2', 'Callback', 'CarPhone', 'CompanyMainPhone', 'HomeFax', 'HomePhone', 'HomePhone2', 'Isdn', 'MobilePhone', 'OtherFax', 'OtherTelephone', 'Pager', 'PrimaryPhone', 'RadioPhone', 'Telex', 'TtyTddPhone' ) label = LabelField(field_uri='Key', choices={Choice(c) for c in LABEL_CHOICES}, default='PrimaryPhone') phone_number = SubField(is_required=True) class MultiFieldIndexedElement(IndexedElement, metaclass=EWSMeta): """Base class for all classes that implement an indexed element with multiple fields.""" class PhysicalAddress(MultiFieldIndexedElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/entry-physicaladdress""" ELEMENT_NAME = 'Entry' LABEL_CHOICES = ('Business', 'Home', 'Other') label = LabelField(field_uri='Key', choices={Choice(c) for c in LABEL_CHOICES}, default=LABEL_CHOICES[0]) street = NamedSubField(field_uri='Street') # Street, house number, etc. city = NamedSubField(field_uri='City') state = NamedSubField(field_uri='State') country = NamedSubField(field_uri='CountryOrRegion') zipcode = NamedSubField(field_uri='PostalCode') def clean(self, version=None): if isinstance(self.zipcode, int): self.zipcode = str(self.zipcode) super().clean(version=version) exchangelib-4.6.1/exchangelib/items/000077500000000000000000000000001414601472700173735ustar00rootroot00000000000000exchangelib-4.6.1/exchangelib/items/__init__.py000066400000000000000000000057241414601472700215140ustar00rootroot00000000000000from .base import RegisterMixIn, BulkCreateResult, MESSAGE_DISPOSITION_CHOICES, SAVE_ONLY, SEND_ONLY, \ ID_ONLY, DEFAULT, ALL_PROPERTIES, SEND_MEETING_INVITATIONS_CHOICES, SEND_TO_NONE, SEND_ONLY_TO_ALL, \ SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES, SEND_ONLY_TO_CHANGED, SEND_TO_CHANGED_AND_SAVE_COPY, \ SEND_MEETING_CANCELLATIONS_CHOICES, AFFECTED_TASK_OCCURRENCES_CHOICES, ALL_OCCURRENCIES, \ SPECIFIED_OCCURRENCE_ONLY, CONFLICT_RESOLUTION_CHOICES, NEVER_OVERWRITE, AUTO_RESOLVE, ALWAYS_OVERWRITE, \ DELETE_TYPE_CHOICES, HARD_DELETE, SOFT_DELETE, MOVE_TO_DELETED_ITEMS, SEND_TO_ALL_AND_SAVE_COPY, \ SEND_AND_SAVE_COPY, SHAPE_CHOICES from .calendar_item import CalendarItem, AcceptItem, TentativelyAcceptItem, DeclineItem, CancelCalendarItem, \ MeetingMessage, MeetingRequest, MeetingResponse, MeetingCancellation, CONFERENCE_TYPES from .contact import Contact, Persona, DistributionList from .item import BaseItem, Item from .message import Message, ReplyToItem, ReplyAllToItem, ForwardItem from .post import PostItem, PostReplyItem from .task import Task # Traversal enums SHALLOW = 'Shallow' SOFT_DELETED = 'SoftDeleted' ASSOCIATED = 'Associated' ITEM_TRAVERSAL_CHOICES = (SHALLOW, SOFT_DELETED, ASSOCIATED) # Contacts search (ResolveNames) scope enums ACTIVE_DIRECTORY = 'ActiveDirectory' ACTIVE_DIRECTORY_CONTACTS = 'ActiveDirectoryContacts' CONTACTS = 'Contacts' CONTACTS_ACTIVE_DIRECTORY = 'ContactsActiveDirectory' SEARCH_SCOPE_CHOICES = (ACTIVE_DIRECTORY, ACTIVE_DIRECTORY_CONTACTS, CONTACTS, CONTACTS_ACTIVE_DIRECTORY) ITEM_CLASSES = (CalendarItem, Contact, DistributionList, Item, Message, MeetingMessage, MeetingRequest, MeetingResponse, MeetingCancellation, PostItem, Task) __all__ = [ 'RegisterMixIn', 'MESSAGE_DISPOSITION_CHOICES', 'SAVE_ONLY', 'SEND_ONLY', 'SEND_AND_SAVE_COPY', 'CalendarItem', 'AcceptItem', 'TentativelyAcceptItem', 'DeclineItem', 'CancelCalendarItem', 'MeetingRequest', 'MeetingResponse', 'MeetingCancellation', 'CONFERENCE_TYPES', 'Contact', 'Persona', 'DistributionList', 'SEND_MEETING_INVITATIONS_CHOICES', 'SEND_TO_NONE', 'SEND_ONLY_TO_ALL', 'SEND_TO_ALL_AND_SAVE_COPY', 'SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES', 'SEND_ONLY_TO_CHANGED', 'SEND_TO_CHANGED_AND_SAVE_COPY', 'SEND_MEETING_CANCELLATIONS_CHOICES', 'AFFECTED_TASK_OCCURRENCES_CHOICES', 'ALL_OCCURRENCIES', 'SPECIFIED_OCCURRENCE_ONLY', 'CONFLICT_RESOLUTION_CHOICES', 'NEVER_OVERWRITE', 'AUTO_RESOLVE', 'ALWAYS_OVERWRITE', 'DELETE_TYPE_CHOICES', 'HARD_DELETE', 'SOFT_DELETE', 'MOVE_TO_DELETED_ITEMS', 'BaseItem', 'Item', 'BulkCreateResult', 'Message', 'ReplyToItem', 'ReplyAllToItem', 'ForwardItem', 'PostItem', 'PostReplyItem', 'Task', 'ITEM_TRAVERSAL_CHOICES', 'SHALLOW', 'SOFT_DELETED', 'ASSOCIATED', 'SHAPE_CHOICES', 'ID_ONLY', 'DEFAULT', 'ALL_PROPERTIES', 'SEARCH_SCOPE_CHOICES', 'ACTIVE_DIRECTORY', 'ACTIVE_DIRECTORY_CONTACTS', 'CONTACTS', 'CONTACTS_ACTIVE_DIRECTORY', 'ITEM_CLASSES', ] exchangelib-4.6.1/exchangelib/items/base.py000066400000000000000000000227111414601472700206620ustar00rootroot00000000000000import logging from ..extended_properties import ExtendedProperty from ..fields import BooleanField, ExtendedPropertyField, BodyField, MailboxField, MailboxListField, EWSElementField, \ CharField, IdElementField, AttachmentField from ..properties import InvalidField, IdChangeKeyMixIn, EWSElement, ReferenceItemId, ItemId, EWSMeta from ..services import CreateItem from ..util import require_account from ..version import EXCHANGE_2007_SP1 log = logging.getLogger(__name__) # Shape enums ID_ONLY = 'IdOnly' DEFAULT = 'Default' # AllProperties doesn't actually get all properties in FindItem, just the "first-class" ones. See # https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/email-properties-and-elements-in-ews-in-exchange ALL_PROPERTIES = 'AllProperties' SHAPE_CHOICES = (ID_ONLY, DEFAULT, ALL_PROPERTIES) # MessageDisposition values. See # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem SAVE_ONLY = 'SaveOnly' SEND_ONLY = 'SendOnly' SEND_AND_SAVE_COPY = 'SendAndSaveCopy' MESSAGE_DISPOSITION_CHOICES = (SAVE_ONLY, SEND_ONLY, SEND_AND_SAVE_COPY) # SendMeetingInvitations values. See # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem # SendMeetingInvitationsOrCancellations. See # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem # SendMeetingCancellations values. See # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteitem SEND_TO_NONE = 'SendToNone' SEND_ONLY_TO_ALL = 'SendOnlyToAll' SEND_ONLY_TO_CHANGED = 'SendOnlyToChanged' SEND_TO_ALL_AND_SAVE_COPY = 'SendToAllAndSaveCopy' SEND_TO_CHANGED_AND_SAVE_COPY = 'SendToChangedAndSaveCopy' SEND_MEETING_INVITATIONS_CHOICES = (SEND_TO_NONE, SEND_ONLY_TO_ALL, SEND_TO_ALL_AND_SAVE_COPY) SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES = (SEND_TO_NONE, SEND_ONLY_TO_ALL, SEND_ONLY_TO_CHANGED, SEND_TO_ALL_AND_SAVE_COPY, SEND_TO_CHANGED_AND_SAVE_COPY) SEND_MEETING_CANCELLATIONS_CHOICES = (SEND_TO_NONE, SEND_ONLY_TO_ALL, SEND_TO_ALL_AND_SAVE_COPY) # AffectedTaskOccurrences values. See # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteitem ALL_OCCURRENCIES = 'AllOccurrences' SPECIFIED_OCCURRENCE_ONLY = 'SpecifiedOccurrenceOnly' AFFECTED_TASK_OCCURRENCES_CHOICES = (ALL_OCCURRENCIES, SPECIFIED_OCCURRENCE_ONLY) # ConflictResolution values. See # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem NEVER_OVERWRITE = 'NeverOverwrite' AUTO_RESOLVE = 'AutoResolve' ALWAYS_OVERWRITE = 'AlwaysOverwrite' CONFLICT_RESOLUTION_CHOICES = (NEVER_OVERWRITE, AUTO_RESOLVE, ALWAYS_OVERWRITE) # DeleteType values. See # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteitem HARD_DELETE = 'HardDelete' SOFT_DELETE = 'SoftDelete' MOVE_TO_DELETED_ITEMS = 'MoveToDeletedItems' DELETE_TYPE_CHOICES = (HARD_DELETE, SOFT_DELETE, MOVE_TO_DELETED_ITEMS) class RegisterMixIn(IdChangeKeyMixIn, metaclass=EWSMeta): """Base class for classes that can change their list of supported fields dynamically.""" # This class implements dynamic fields on an element class, so we need to include __dict__ in __slots__ __slots__ = '__dict__', INSERT_AFTER_FIELD = None @classmethod def register(cls, attr_name, attr_cls): """Register a custom extended property in this item class so they can be accessed just like any other attribute :param attr_name: :param attr_cls: :return: """ if not cls.INSERT_AFTER_FIELD: raise ValueError('Class %s is missing INSERT_AFTER_FIELD value' % cls) try: cls.get_field_by_fieldname(attr_name) except InvalidField: pass else: raise ValueError("'%s' is already registered" % attr_name) if not issubclass(attr_cls, ExtendedProperty): raise ValueError("%r must be a subclass of ExtendedProperty" % attr_cls) # Check if class attributes are properly defined attr_cls.validate_cls() # ExtendedProperty is not a real field, but a placeholder in the fields list. See # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item # # Find the correct index for the new extended property, and insert. field = ExtendedPropertyField(attr_name, value_cls=attr_cls) cls.add_field(field, insert_after=cls.INSERT_AFTER_FIELD) @classmethod def deregister(cls, attr_name): """De-register an extended property that has been registered with register(). :param attr_name: :return: """ try: field = cls.get_field_by_fieldname(attr_name) except InvalidField: raise ValueError("'%s' is not registered" % attr_name) if not isinstance(field, ExtendedPropertyField): raise ValueError("'%s' is not registered as an ExtendedProperty" % attr_name) cls.remove_field(field) class BaseItem(RegisterMixIn, metaclass=EWSMeta): """Base class for all other classes that implement EWS items.""" ID_ELEMENT_CLS = ItemId _id = IdElementField(field_uri='item:ItemId', value_cls=ID_ELEMENT_CLS) __slots__ = 'account', 'folder' def __init__(self, **kwargs): """Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class. :param kwargs: 'account' is optional but allows calling 'send()' and 'delete()' 'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set, we use folder.account. """ from ..folders import BaseFolder from ..account import Account self.account = kwargs.pop('account', None) if self.account is not None and not isinstance(self.account, Account): raise ValueError("'account' %r must be an Account instance" % self.account) self.folder = kwargs.pop('folder', None) if self.folder is not None: if not isinstance(self.folder, BaseFolder): raise ValueError("'folder' %r must be a Folder instance" % self.folder) if self.folder.account is not None: if self.account is not None: # Make sure the account from kwargs matches the folder account if self.account != self.folder.account: raise ValueError("'account' does not match 'folder.account'") self.account = self.folder.account super().__init__(**kwargs) @classmethod def from_xml(cls, elem, account): item = super().from_xml(elem=elem, account=account) item.account = account return item class BaseReplyItem(EWSElement, metaclass=EWSMeta): """Base class for reply/forward elements that share the same fields.""" subject = CharField(field_uri='Subject') body = BodyField(field_uri='Body') # Accepts and returns Body or HTMLBody instances to_recipients = MailboxListField(field_uri='ToRecipients') cc_recipients = MailboxListField(field_uri='CcRecipients') bcc_recipients = MailboxListField(field_uri='BccRecipients') is_read_receipt_requested = BooleanField(field_uri='IsReadReceiptRequested') is_delivery_receipt_requested = BooleanField(field_uri='IsDeliveryReceiptRequested') author = MailboxField(field_uri='From') reference_item_id = EWSElementField(value_cls=ReferenceItemId) new_body = BodyField(field_uri='NewBodyContent') # Accepts and returns Body or HTMLBody instances received_by = MailboxField(field_uri='ReceivedBy', supported_from=EXCHANGE_2007_SP1) received_by_representing = MailboxField(field_uri='ReceivedRepresenting', supported_from=EXCHANGE_2007_SP1) __slots__ = 'account', def __init__(self, **kwargs): # 'account' is optional but allows calling 'send()' and 'save()' from ..account import Account self.account = kwargs.pop('account', None) if self.account is not None and not isinstance(self.account, Account): raise ValueError("'account' %r must be an Account instance" % self.account) super().__init__(**kwargs) @require_account def send(self, save_copy=True, copy_to_folder=None): if copy_to_folder and not save_copy: raise AttributeError("'save_copy' must be True when 'copy_to_folder' is set") message_disposition = SEND_AND_SAVE_COPY if save_copy else SEND_ONLY return CreateItem(account=self.account).get( items=[self], folder=copy_to_folder, message_disposition=message_disposition, send_meeting_invitations=SEND_TO_NONE, ) @require_account def save(self, folder): """Save the item for later modification. You may want to use account.drafts as the folder. :param folder: :return: """ return CreateItem(account=self.account).get( items=[self], folder=folder, message_disposition=SAVE_ONLY, send_meeting_invitations=SEND_TO_NONE, ) class BulkCreateResult(BaseItem): """A dummy class to store return values from a CreateItem service call.""" attachments = AttachmentField(field_uri='item:Attachments') # ItemAttachment or FileAttachment def __init__(self, **kwargs): super().__init__(**kwargs) if self.attachments is None: self.attachments = [] exchangelib-4.6.1/exchangelib/items/calendar_item.py000066400000000000000000000502751414601472700225450ustar00rootroot00000000000000import datetime import logging from .base import BaseItem, BaseReplyItem, SEND_AND_SAVE_COPY, SEND_TO_NONE from .item import Item from .message import Message from ..ewsdatetime import EWSDate, EWSDateTime from ..fields import BooleanField, IntegerField, TextField, ChoiceField, URIField, BodyField, DateTimeField, \ MessageHeaderField, AttachmentField, RecurrenceField, MailboxField, AttendeesField, Choice, OccurrenceField, \ OccurrenceListField, TimeZoneField, CharField, EnumAsIntField, FreeBusyStatusField, ReferenceItemIdField, \ AssociatedCalendarItemIdField, DateOrDateTimeField, EWSElementListField, AppointmentStateField from ..properties import Attendee, ReferenceItemId, OccurrenceItemId, RecurringMasterItemId, EWSMeta from ..recurrence import FirstOccurrence, LastOccurrence, Occurrence, DeletedOccurrence from ..services import CreateItem from ..util import set_xml_value, require_account from ..version import EXCHANGE_2010, EXCHANGE_2013 log = logging.getLogger(__name__) # Conference Type values. See # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/conferencetype CONFERENCE_TYPES = ('NetMeeting', 'NetShow', 'Chat') # CalendarItemType enums SINGLE = 'Single' OCCURRENCE = 'Occurrence' EXCEPTION = 'Exception' RECURRING_MASTER = 'RecurringMaster' CALENDAR_ITEM_CHOICES = (SINGLE, OCCURRENCE, EXCEPTION, RECURRING_MASTER) class AcceptDeclineMixIn: """A mixin for items that can be declined or accepted.""" def accept(self, message_disposition=SEND_AND_SAVE_COPY, **kwargs): return AcceptItem( account=self.account, reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey), **kwargs ).send(message_disposition) def decline(self, message_disposition=SEND_AND_SAVE_COPY, **kwargs): return DeclineItem( account=self.account, reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey), **kwargs ).send(message_disposition) def tentatively_accept(self, message_disposition=SEND_AND_SAVE_COPY, **kwargs): return TentativelyAcceptItem( account=self.account, reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey), **kwargs ).send(message_disposition) class CalendarItem(Item, AcceptDeclineMixIn): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendaritem""" ELEMENT_NAME = 'CalendarItem' uid = TextField(field_uri='calendar:UID', is_required_after_save=True, is_searchable=False) recurrence_id = DateTimeField(field_uri='calendar:RecurrenceId', is_read_only=True) start = DateOrDateTimeField(field_uri='calendar:Start', is_required=True) end = DateOrDateTimeField(field_uri='calendar:End', is_required=True) original_start = DateTimeField(field_uri='calendar:OriginalStart', is_read_only=True) is_all_day = BooleanField(field_uri='calendar:IsAllDayEvent', is_required=True, default=False) legacy_free_busy_status = FreeBusyStatusField(field_uri='calendar:LegacyFreeBusyStatus', is_required=True, default='Busy') location = TextField(field_uri='calendar:Location') when = TextField(field_uri='calendar:When') is_meeting = BooleanField(field_uri='calendar:IsMeeting', is_read_only=True) is_cancelled = BooleanField(field_uri='calendar:IsCancelled', is_read_only=True) is_recurring = BooleanField(field_uri='calendar:IsRecurring', is_read_only=True) meeting_request_was_sent = BooleanField(field_uri='calendar:MeetingRequestWasSent', is_read_only=True) is_response_requested = BooleanField(field_uri='calendar:IsResponseRequested', default=None, is_required_after_save=True, is_searchable=False) type = ChoiceField(field_uri='calendar:CalendarItemType', choices={Choice(c) for c in CALENDAR_ITEM_CHOICES}, is_read_only=True) my_response_type = ChoiceField(field_uri='calendar:MyResponseType', choices={ Choice(c) for c in Attendee.RESPONSE_TYPES }, is_read_only=True) organizer = MailboxField(field_uri='calendar:Organizer', is_read_only=True) required_attendees = AttendeesField(field_uri='calendar:RequiredAttendees', is_searchable=False) optional_attendees = AttendeesField(field_uri='calendar:OptionalAttendees', is_searchable=False) resources = AttendeesField(field_uri='calendar:Resources', is_searchable=False) conflicting_meeting_count = IntegerField(field_uri='calendar:ConflictingMeetingCount', is_read_only=True) adjacent_meeting_count = IntegerField(field_uri='calendar:AdjacentMeetingCount', is_read_only=True) conflicting_meetings = EWSElementListField(field_uri='calendar:ConflictingMeetings', value_cls='CalendarItem', namespace=Item.NAMESPACE, is_read_only=True) adjacent_meetings = EWSElementListField(field_uri='calendar:AdjacentMeetings', value_cls='CalendarItem', namespace=Item.NAMESPACE, is_read_only=True) duration = CharField(field_uri='calendar:Duration', is_read_only=True) appointment_reply_time = DateTimeField(field_uri='calendar:AppointmentReplyTime', is_read_only=True) appointment_sequence_number = IntegerField(field_uri='calendar:AppointmentSequenceNumber', is_read_only=True) appointment_state = AppointmentStateField(field_uri='calendar:AppointmentState', is_read_only=True) recurrence = RecurrenceField(field_uri='calendar:Recurrence', is_searchable=False) first_occurrence = OccurrenceField(field_uri='calendar:FirstOccurrence', value_cls=FirstOccurrence, is_read_only=True) last_occurrence = OccurrenceField(field_uri='calendar:LastOccurrence', value_cls=LastOccurrence, is_read_only=True) modified_occurrences = OccurrenceListField(field_uri='calendar:ModifiedOccurrences', value_cls=Occurrence, is_read_only=True) deleted_occurrences = OccurrenceListField(field_uri='calendar:DeletedOccurrences', value_cls=DeletedOccurrence, is_read_only=True) _meeting_timezone = TimeZoneField(field_uri='calendar:MeetingTimeZone', deprecated_from=EXCHANGE_2010, is_searchable=False) _start_timezone = TimeZoneField(field_uri='calendar:StartTimeZone', supported_from=EXCHANGE_2010, is_searchable=False) _end_timezone = TimeZoneField(field_uri='calendar:EndTimeZone', supported_from=EXCHANGE_2010, is_searchable=False) conference_type = EnumAsIntField(field_uri='calendar:ConferenceType', enum=CONFERENCE_TYPES, min=0, default=None, is_required_after_save=True) allow_new_time_proposal = BooleanField(field_uri='calendar:AllowNewTimeProposal', default=None, is_required_after_save=True, is_searchable=False) is_online_meeting = BooleanField(field_uri='calendar:IsOnlineMeeting', default=None, is_read_only=True) meeting_workspace_url = URIField(field_uri='calendar:MeetingWorkspaceUrl') net_show_url = URIField(field_uri='calendar:NetShowUrl') def occurrence(self, index): """Get an occurrence of a recurring master by index. No query is sent to the server to actually fetch the item. Call refresh() on the item do do so. Only call this method on a recurring master. :param index: The index, which is 1-based :return The occurrence """ return self.__class__( account=self.account, folder=self.folder, _id=OccurrenceItemId(id=self.id, changekey=self.changekey, instance_index=index), ) def recurring_master(self): """Get the recurring master of an occurrence. No query is sent to the server to actually fetch the item. Call refresh() on the item do do so. Only call this method on an occurrence of a recurring master. :return: The master occurrence """ return self.__class__( account=self.account, folder=self.folder, _id=RecurringMasterItemId(id=self.id, changekey=self.changekey), ) @classmethod def timezone_fields(cls): return [f for f in cls.FIELDS if isinstance(f, TimeZoneField)] def clean_timezone_fields(self, version): # Sets proper values on the timezone fields if they are not already set if self.start is None: start_tz = None elif type(self.start) in (EWSDate, datetime.date): start_tz = self.account.default_timezone else: start_tz = self.start.tzinfo if self.end is None: end_tz = None elif type(self.end) in (EWSDate, datetime.date): end_tz = self.account.default_timezone else: end_tz = self.end.tzinfo if version.build < EXCHANGE_2010: if self._meeting_timezone is None: self._meeting_timezone = start_tz self._start_timezone = None self._end_timezone = None else: self._meeting_timezone = None if self._start_timezone is None: self._start_timezone = start_tz if self._end_timezone is None: self._end_timezone = end_tz def clean(self, version=None): super().clean(version=version) if self.start and self.end and self.end < self.start: raise ValueError("'end' must be greater than 'start' (%s -> %s)" % (self.start, self.end)) if version: self.clean_timezone_fields(version=version) def cancel(self, **kwargs): return CancelCalendarItem( account=self.account, reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey), **kwargs ).send() def _update_fieldnames(self): update_fields = super()._update_fieldnames() if self.type == OCCURRENCE: # Some CalendarItem fields cannot be updated when the item is an occurrence. The values are empty when we # receive them so would have been updated because they are set to None. update_fields.remove('recurrence') update_fields.remove('uid') return update_fields @classmethod def from_xml(cls, elem, account): item = super().from_xml(elem=elem, account=account) # EWS returns the start and end values as a datetime regardless of the is_all_day status. Convert to date if # applicable. if not item.is_all_day: return item for field_name in ('start', 'end'): val = getattr(item, field_name) if val is None: continue # Return just the date part of the value. Subtract 1 day from the date if this is the end field. This is # the inverse of what we do in .to_xml(). Convert to the local timezone before getting the date. if field_name == 'end': val -= datetime.timedelta(days=1) tz = getattr(item, '_%s_timezone' % field_name) setattr(item, field_name, val.astimezone(tz).date()) return item def tz_field_for_field_name(self, field_name): meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields() if self.account.version.build < EXCHANGE_2010: return meeting_tz_field if field_name == 'start': return start_tz_field if field_name == 'end': return end_tz_field raise ValueError('Unsupported field_name') def date_to_datetime(self, field_name): # EWS always expects a datetime. If we have a date value, then convert it to datetime in the local # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both # start and end values and let EWS apply its logic, but that seems hacky. value = getattr(self, field_name) tz = getattr(self, self.tz_field_for_field_name(field_name).name) value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz) if field_name == 'end': value += datetime.timedelta(days=1) return value def to_xml(self, version): # EWS has some special logic related to all-day start and end values. Non-midnight start values are pushed to # the previous midnight. Non-midnight end values are pushed to the following midnight. Midnight in this context # refers to midnight in the local timezone. See # # https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-create-all-day-events-by-using-ews-in-exchange # elem = super().to_xml(version=version) if not self.is_all_day: return elem for field_name in ('start', 'end'): value = getattr(self, field_name) if value is None: continue if type(value) in (EWSDate, datetime.date): # EWS always expects a datetime value = self.date_to_datetime(field_name=field_name) # We already generated an XML element for this field, but it contains a plain date at this point, which # is invalid. Replace the value. field = self.get_field_by_fieldname(field_name) set_xml_value(elem=elem.find(field.response_tag()), value=value, version=version) return elem class BaseMeetingItem(Item): """A base class for meeting requests that share the same fields (Message, Request, Response, Cancellation) MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/responsecode Certain types are created as a side effect of doing something else. Meeting messages, for example, are created when you send a calendar item to attendees; they are not explicitly created. Therefore BaseMeetingItem inherits from EWSElement has no save() or send() method """ associated_calendar_item_id = AssociatedCalendarItemIdField(field_uri='meeting:AssociatedCalendarItemId') is_delegated = BooleanField(field_uri='meeting:IsDelegated', is_read_only=True, default=False) is_out_of_date = BooleanField(field_uri='meeting:IsOutOfDate', is_read_only=True, default=False) has_been_processed = BooleanField(field_uri='meeting:HasBeenProcessed', is_read_only=True, default=False) response_type = ChoiceField(field_uri='meeting:ResponseType', choices={ Choice('Unknown'), Choice('Organizer'), Choice('Tentative'), Choice('Accept'), Choice('Decline'), Choice('NoResponseReceived') }, is_required=True, default='Unknown') effective_rights_idx = Item.FIELDS.index_by_name('effective_rights') sender_idx = Message.FIELDS.index_by_name('sender') reply_to_idx = Message.FIELDS.index_by_name('reply_to') FIELDS = Item.FIELDS[:effective_rights_idx] \ + Message.FIELDS[sender_idx:reply_to_idx + 1] \ + Item.FIELDS[effective_rights_idx:] class MeetingRequest(BaseMeetingItem, AcceptDeclineMixIn): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingrequest""" ELEMENT_NAME = 'MeetingRequest' meeting_request_type = ChoiceField(field_uri='meetingRequest:MeetingRequestType', choices={ Choice('FullUpdate'), Choice('InformationalUpdate'), Choice('NewMeetingRequest'), Choice('None'), Choice('Outdated'), Choice('PrincipalWantsCopy'), Choice('SilentUpdate') }, default='None') intended_free_busy_status = ChoiceField(field_uri='meetingRequest:IntendedFreeBusyStatus', choices={ Choice('Free'), Choice('Tentative'), Choice('Busy'), Choice('OOF'), Choice('NoData') }, is_required=True, default='Busy') # This element also has some fields from CalendarItem start_idx = CalendarItem.FIELDS.index_by_name('start') is_response_requested_idx = CalendarItem.FIELDS.index_by_name('is_response_requested') FIELDS = BaseMeetingItem.FIELDS \ + CalendarItem.FIELDS[start_idx:is_response_requested_idx]\ + CalendarItem.FIELDS[is_response_requested_idx + 1:] class MeetingMessage(BaseMeetingItem): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingmessage""" ELEMENT_NAME = 'MeetingMessage' class MeetingResponse(BaseMeetingItem): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingresponse""" ELEMENT_NAME = 'MeetingResponse' received_by = MailboxField(field_uri='message:ReceivedBy', is_read_only=True) received_representing = MailboxField(field_uri='message:ReceivedRepresenting', is_read_only=True) proposed_start = DateTimeField(field_uri='meeting:ProposedStart', supported_from=EXCHANGE_2013) proposed_end = DateTimeField(field_uri='meeting:ProposedEnd', supported_from=EXCHANGE_2013) class MeetingCancellation(BaseMeetingItem): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingcancellation""" ELEMENT_NAME = 'MeetingCancellation' class BaseMeetingReplyItem(BaseItem, metaclass=EWSMeta): """Base class for meeting request reply items that share the same fields (Accept, TentativelyAccept, Decline).""" item_class = CharField(field_uri='item:ItemClass', is_read_only=True) sensitivity = ChoiceField(field_uri='item:Sensitivity', choices={ Choice('Normal'), Choice('Personal'), Choice('Private'), Choice('Confidential') }, is_required=True, default='Normal') body = BodyField(field_uri='item:Body') # Accepts and returns Body or HTMLBody instances attachments = AttachmentField(field_uri='item:Attachments') # ItemAttachment or FileAttachment headers = MessageHeaderField(field_uri='item:InternetMessageHeaders', is_read_only=True) sender = Message.FIELDS['sender'] to_recipients = Message.FIELDS['to_recipients'] cc_recipients = Message.FIELDS['cc_recipients'] bcc_recipients = Message.FIELDS['bcc_recipients'] is_read_receipt_requested = Message.FIELDS['is_read_receipt_requested'] is_delivery_receipt_requested = Message.FIELDS['is_delivery_receipt_requested'] reference_item_id = ReferenceItemIdField(field_uri='item:ReferenceItemId') received_by = MailboxField(field_uri='message:ReceivedBy', is_read_only=True) received_representing = MailboxField(field_uri='message:ReceivedRepresenting', is_read_only=True) proposed_start = DateTimeField(field_uri='meeting:ProposedStart', supported_from=EXCHANGE_2013) proposed_end = DateTimeField(field_uri='meeting:ProposedEnd', supported_from=EXCHANGE_2013) @require_account def send(self, message_disposition=SEND_AND_SAVE_COPY): # Some responses contain multiple response IDs, e.g. MeetingRequest.accept(). Return either the single ID or # the list of IDs. res = list(CreateItem(account=self.account).call( items=[self], folder=self.folder, message_disposition=message_disposition, send_meeting_invitations=SEND_TO_NONE, )) for r in res: if isinstance(r, Exception): raise r if len(res) == 1: return res[0] return res class AcceptItem(BaseMeetingReplyItem): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/acceptitem""" ELEMENT_NAME = 'AcceptItem' class TentativelyAcceptItem(BaseMeetingReplyItem): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/tentativelyacceptitem""" ELEMENT_NAME = 'TentativelyAcceptItem' class DeclineItem(BaseMeetingReplyItem): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/declineitem""" ELEMENT_NAME = 'DeclineItem' class CancelCalendarItem(BaseReplyItem): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/cancelcalendaritem""" ELEMENT_NAME = 'CancelCalendarItem' author_idx = BaseReplyItem.FIELDS.index_by_name('author') FIELDS = BaseReplyItem.FIELDS[:author_idx] + BaseReplyItem.FIELDS[author_idx + 1:] exchangelib-4.6.1/exchangelib/items/contact.py000066400000000000000000000343021414601472700214020ustar00rootroot00000000000000import datetime import logging from .item import Item from ..fields import BooleanField, Base64Field, TextField, ChoiceField, URIField, DateTimeBackedDateField, \ PhoneNumberField, EmailAddressesField, PhysicalAddressField, Choice, MemberListField, CharField, TextListField, \ EmailAddressField, IdElementField, EWSElementField, DateTimeField, EWSElementListField, \ BodyContentAttributedValueField, StringAttributedValueField, PhoneNumberAttributedValueField, \ PersonaPhoneNumberField, EmailAddressAttributedValueField, PostalAddressAttributedValueField, MailboxField, \ MailboxListField from ..properties import PersonaId, IdChangeKeyMixIn, CompleteName, Attribution, EmailAddress, Address, FolderId from ..util import TNS from ..version import EXCHANGE_2010, EXCHANGE_2010_SP2 log = logging.getLogger(__name__) class Contact(Item): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/contact""" ELEMENT_NAME = 'Contact' file_as = TextField(field_uri='contacts:FileAs') file_as_mapping = ChoiceField(field_uri='contacts:FileAsMapping', choices={ Choice('None'), Choice('LastCommaFirst'), Choice('FirstSpaceLast'), Choice('Company'), Choice('LastCommaFirstCompany'), Choice('CompanyLastFirst'), Choice('LastFirst'), Choice('LastFirstCompany'), Choice('CompanyLastCommaFirst'), Choice('LastFirstSuffix'), Choice('LastSpaceFirstCompany'), Choice('CompanyLastSpaceFirst'), Choice('LastSpaceFirst'), Choice('DisplayName'), Choice('FirstName'), Choice('LastFirstMiddleSuffix'), Choice('LastName'), Choice('Empty'), }) display_name = TextField(field_uri='contacts:DisplayName', is_required=True) given_name = CharField(field_uri='contacts:GivenName') initials = TextField(field_uri='contacts:Initials') middle_name = CharField(field_uri='contacts:MiddleName') nickname = TextField(field_uri='contacts:Nickname') complete_name = EWSElementField(field_uri='contacts:CompleteName', value_cls=CompleteName, is_read_only=True) company_name = TextField(field_uri='contacts:CompanyName') email_addresses = EmailAddressesField(field_uri='contacts:EmailAddress') physical_addresses = PhysicalAddressField(field_uri='contacts:PhysicalAddress') phone_numbers = PhoneNumberField(field_uri='contacts:PhoneNumber') assistant_name = TextField(field_uri='contacts:AssistantName') birthday = DateTimeBackedDateField(field_uri='contacts:Birthday', default_time=datetime.time(11, 59)) business_homepage = URIField(field_uri='contacts:BusinessHomePage') children = TextListField(field_uri='contacts:Children') companies = TextListField(field_uri='contacts:Companies', is_searchable=False) contact_source = ChoiceField(field_uri='contacts:ContactSource', choices={ Choice('Store'), Choice('ActiveDirectory') }, is_read_only=True) department = TextField(field_uri='contacts:Department') generation = TextField(field_uri='contacts:Generation') im_addresses = CharField(field_uri='contacts:ImAddresses', is_read_only=True) job_title = TextField(field_uri='contacts:JobTitle') manager = TextField(field_uri='contacts:Manager') mileage = TextField(field_uri='contacts:Mileage') office = TextField(field_uri='contacts:OfficeLocation') postal_address_index = ChoiceField(field_uri='contacts:PostalAddressIndex', choices={ Choice('Business'), Choice('Home'), Choice('Other'), Choice('None') }, default='None', is_required_after_save=True) profession = TextField(field_uri='contacts:Profession') spouse_name = TextField(field_uri='contacts:SpouseName') surname = CharField(field_uri='contacts:Surname') wedding_anniversary = DateTimeBackedDateField(field_uri='contacts:WeddingAnniversary', default_time=datetime.time(11, 59)) has_picture = BooleanField(field_uri='contacts:HasPicture', supported_from=EXCHANGE_2010, is_read_only=True) phonetic_full_name = TextField(field_uri='contacts:PhoneticFullName', supported_from=EXCHANGE_2010_SP2, is_read_only=True) phonetic_first_name = TextField(field_uri='contacts:PhoneticFirstName', supported_from=EXCHANGE_2010_SP2, is_read_only=True) phonetic_last_name = TextField(field_uri='contacts:PhoneticLastName', supported_from=EXCHANGE_2010_SP2, is_read_only=True) email_alias = EmailAddressField(field_uri='contacts:Alias', is_read_only=True, supported_from=EXCHANGE_2010_SP2) # 'notes' is documented in MSDN but apparently unused. Writing to it raises ErrorInvalidPropertyRequest. OWA # put entries into the 'notes' form field into the 'body' field. notes = CharField(field_uri='contacts:Notes', supported_from=EXCHANGE_2010_SP2, is_read_only=True) # 'photo' is documented in MSDN but apparently unused. Writing to it raises ErrorInvalidPropertyRequest. OWA # adds photos as FileAttachments on the contact item (with 'is_contact_photo=True'), which automatically flips # the 'has_picture' field. photo = Base64Field(field_uri='contacts:Photo', supported_from=EXCHANGE_2010_SP2, is_read_only=True) user_smime_certificate = Base64Field(field_uri='contacts:UserSMIMECertificate', supported_from=EXCHANGE_2010_SP2, is_read_only=True) ms_exchange_certificate = Base64Field(field_uri='contacts:MSExchangeCertificate', supported_from=EXCHANGE_2010_SP2, is_read_only=True) directory_id = TextField(field_uri='contacts:DirectoryId', supported_from=EXCHANGE_2010_SP2, is_read_only=True) manager_mailbox = MailboxField(field_uri='contacts:ManagerMailbox', supported_from=EXCHANGE_2010_SP2, is_read_only=True) direct_reports = MailboxListField(field_uri='contacts:DirectReports', supported_from=EXCHANGE_2010_SP2, is_read_only=True) class Persona(IdChangeKeyMixIn): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/persona""" ELEMENT_NAME = 'Persona' ID_ELEMENT_CLS = PersonaId _id = IdElementField(field_uri='persona:PersonaId', value_cls=ID_ELEMENT_CLS, namespace=TNS) persona_type = CharField(field_uri='persona:PersonaType') persona_object_type = TextField(field_uri='persona:PersonaObjectStatus') creation_time = DateTimeField(field_uri='persona:CreationTime') bodies = BodyContentAttributedValueField(field_uri='persona:Bodies') display_name_first_last_sort_key = TextField(field_uri='persona:DisplayNameFirstLastSortKey') display_name_last_first_sort_key = TextField(field_uri='persona:DisplayNameLastFirstSortKey') company_sort_key = TextField(field_uri='persona:CompanyNameSortKey') home_sort_key = TextField(field_uri='persona:HomeCitySortKey') work_city_sort_key = TextField(field_uri='persona:WorkCitySortKey') display_name_first_last_header = CharField(field_uri='persona:DisplayNameFirstLastHeader') display_name_last_first_header = CharField(field_uri='persona:DisplayNameLastFirstHeader') file_as_header = TextField(field_uri='persona:FileAsHeader') display_name = CharField(field_uri='persona:DisplayName') display_name_first_last = CharField(field_uri='persona:DisplayNameFirstLast') display_name_last_first = CharField(field_uri='persona:DisplayNameLastFirst') file_as = CharField(field_uri='persona:FileAs') file_as_id = TextField(field_uri='persona:FileAsId') display_name_prefix = CharField(field_uri='persona:DisplayNamePrefix') given_name = CharField(field_uri='persona:GivenName') middle_name = CharField(field_uri='persona:MiddleName') surname = CharField(field_uri='persona:Surname') generation = CharField(field_uri='persona:Generation') nickname = TextField(field_uri='persona:Nickname') yomi_company_name = TextField(field_uri='persona:YomiCompanyName') yomi_first_name = TextField(field_uri='persona:YomiFirstName') yomi_last_name = TextField(field_uri='persona:YomiLastName') title = CharField(field_uri='persona:Title') department = TextField(field_uri='persona:Department') company_name = CharField(field_uri='persona:CompanyName') email_address = EWSElementField(field_uri='persona:EmailAddress', value_cls=EmailAddress) email_addresses = EWSElementListField(field_uri='persona:EmailAddresses', value_cls=Address) PhoneNumber = PersonaPhoneNumberField(field_uri='persona:PhoneNumber') im_address = CharField(field_uri='persona:ImAddress') home_city = CharField(field_uri='persona:HomeCity') work_city = CharField(field_uri='persona:WorkCity') relevance_score = CharField(field_uri='persona:RelevanceScore') folder_ids = EWSElementListField(field_uri='persona:FolderIds', value_cls=FolderId) attributions = EWSElementListField(field_uri='persona:Attributions', value_cls=Attribution) display_names = StringAttributedValueField(field_uri='persona:DisplayNames') file_ases = StringAttributedValueField(field_uri='persona:FileAses') file_as_ids = StringAttributedValueField(field_uri='persona:FileAsIds') display_name_prefixes = StringAttributedValueField(field_uri='persona:DisplayNamePrefixes') given_names = StringAttributedValueField(field_uri='persona:GivenNames') middle_names = StringAttributedValueField(field_uri='persona:MiddleNames') surnames = StringAttributedValueField(field_uri='persona:Surnames') generations = StringAttributedValueField(field_uri='persona:Generations') nicknames = StringAttributedValueField(field_uri='persona:Nicknames') initials = StringAttributedValueField(field_uri='persona:Initials') yomi_company_names = StringAttributedValueField(field_uri='persona:YomiCompanyNames') yomi_first_names = StringAttributedValueField(field_uri='persona:YomiFirstNames') yomi_last_names = StringAttributedValueField(field_uri='persona:YomiLastNames') business_phone_numbers = PhoneNumberAttributedValueField(field_uri='persona:BusinessPhoneNumbers') business_phone_numbers2 = PhoneNumberAttributedValueField(field_uri='persona:BusinessPhoneNumbers2') home_phones = PhoneNumberAttributedValueField(field_uri='persona:HomePhones') home_phones2 = PhoneNumberAttributedValueField(field_uri='persona:HomePhones2') mobile_phones = PhoneNumberAttributedValueField(field_uri='persona:MobilePhones') mobile_phones2 = PhoneNumberAttributedValueField(field_uri='persona:MobilePhones2') assistant_phone_numbers = PhoneNumberAttributedValueField(field_uri='persona:AssistantPhoneNumbers') callback_phones = PhoneNumberAttributedValueField(field_uri='persona:CallbackPhones') car_phones = PhoneNumberAttributedValueField(field_uri='persona:CarPhones') home_faxes = PhoneNumberAttributedValueField(field_uri='persona:HomeFaxes') orgnaization_main_phones = PhoneNumberAttributedValueField(field_uri='persona:OrganizationMainPhones') other_faxes = PhoneNumberAttributedValueField(field_uri='persona:OtherFaxes') other_telephones = PhoneNumberAttributedValueField(field_uri='persona:OtherTelephones') other_phones2 = PhoneNumberAttributedValueField(field_uri='persona:OtherPhones2') pagers = PhoneNumberAttributedValueField(field_uri='persona:Pagers') radio_phones = PhoneNumberAttributedValueField(field_uri='persona:RadioPhones') telex_numbers = PhoneNumberAttributedValueField(field_uri='persona:TelexNumbers') tty_tdd_phone_numbers = PhoneNumberAttributedValueField(field_uri='persona:TTYTDDPhoneNumbers') work_faxes = PhoneNumberAttributedValueField(field_uri='persona:WorkFaxes') emails1 = EmailAddressAttributedValueField(field_uri='persona:Emails1') emails2 = EmailAddressAttributedValueField(field_uri='persona:Emails2') emails3 = EmailAddressAttributedValueField(field_uri='persona:Emails3') business_home_pages = StringAttributedValueField(field_uri='persona:BusinessHomePages') personal_home_pages = StringAttributedValueField(field_uri='persona:PersonalHomePages') office_locations = StringAttributedValueField(field_uri='persona:OfficeLocations') im_addresses = StringAttributedValueField(field_uri='persona:ImAddresses') im_addresses2 = StringAttributedValueField(field_uri='persona:ImAddresses2') im_addresses3 = StringAttributedValueField(field_uri='persona:ImAddresses3') business_addresses = PostalAddressAttributedValueField(field_uri='persona:BusinessAddresses') home_addresses = PostalAddressAttributedValueField(field_uri='persona:HomeAddresses') other_addresses = PostalAddressAttributedValueField(field_uri='persona:OtherAddresses') titles = StringAttributedValueField(field_uri='persona:Titles') departments = StringAttributedValueField(field_uri='persona:Departments') company_names = StringAttributedValueField(field_uri='persona:CompanyNames') managers = StringAttributedValueField(field_uri='persona:Managers') assistant_names = StringAttributedValueField(field_uri='persona:AssistantNames') professions = StringAttributedValueField(field_uri='persona:Professions') spouse_names = StringAttributedValueField(field_uri='persona:SpouseNames') children = StringAttributedValueField(field_uri='persona:Children') schools = StringAttributedValueField(field_uri='persona:Schools') hobbies = StringAttributedValueField(field_uri='persona:Hobbies') wedding_anniversaries = StringAttributedValueField(field_uri='persona:WeddingAnniversaries') birthdays = StringAttributedValueField(field_uri='persona:Birthdays') locations = StringAttributedValueField(field_uri='persona:Locations') # ExtendedPropertyAttributedValueField('extended_properties', field_uri='persona:ExtendedProperties') class DistributionList(Item): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distributionlist""" ELEMENT_NAME = 'DistributionList' display_name = CharField(field_uri='contacts:DisplayName', is_required=True) file_as = CharField(field_uri='contacts:FileAs', is_read_only=True) contact_source = ChoiceField(field_uri='contacts:ContactSource', choices={ Choice('Store'), Choice('ActiveDirectory') }, is_read_only=True) members = MemberListField(field_uri='distributionlist:Members') exchangelib-4.6.1/exchangelib/items/item.py000066400000000000000000000423141414601472700207070ustar00rootroot00000000000000import logging from .base import BaseItem, SAVE_ONLY, SEND_AND_SAVE_COPY, ID_ONLY, SEND_TO_NONE, \ AUTO_RESOLVE, SOFT_DELETE, HARD_DELETE, ALL_OCCURRENCIES, MOVE_TO_DELETED_ITEMS from ..fields import BooleanField, IntegerField, TextField, CharListField, ChoiceField, URIField, BodyField, \ DateTimeField, MessageHeaderField, AttachmentField, Choice, EWSElementField, EffectiveRightsField, CultureField, \ CharField, MimeContentField, FieldPath from ..properties import ConversationId, ParentFolderId, ReferenceItemId, OccurrenceItemId, RecurringMasterItemId, \ ResponseObjects, Fields from ..services import GetItem, CreateItem, UpdateItem, DeleteItem, MoveItem, CopyItem, ArchiveItem from ..util import is_iterable, require_account, require_id from ..version import EXCHANGE_2010, EXCHANGE_2013 log = logging.getLogger(__name__) class Item(BaseItem): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item""" ELEMENT_NAME = 'Item' mime_content = MimeContentField(field_uri='item:MimeContent', is_read_only_after_send=True) _id = BaseItem.FIELDS['_id'] parent_folder_id = EWSElementField(field_uri='item:ParentFolderId', value_cls=ParentFolderId, is_read_only=True) item_class = CharField(field_uri='item:ItemClass', is_read_only=True) subject = CharField(field_uri='item:Subject') sensitivity = ChoiceField(field_uri='item:Sensitivity', choices={ Choice('Normal'), Choice('Personal'), Choice('Private'), Choice('Confidential') }, is_required=True, default='Normal') text_body = TextField(field_uri='item:TextBody', is_read_only=True, supported_from=EXCHANGE_2013) body = BodyField(field_uri='item:Body') # Accepts and returns Body or HTMLBody instances attachments = AttachmentField(field_uri='item:Attachments') # ItemAttachment or FileAttachment datetime_received = DateTimeField(field_uri='item:DateTimeReceived', is_read_only=True) size = IntegerField(field_uri='item:Size', is_read_only=True) # Item size in bytes categories = CharListField(field_uri='item:Categories') importance = ChoiceField(field_uri='item:Importance', choices={ Choice('Low'), Choice('Normal'), Choice('High') }, is_required=True, default='Normal') in_reply_to = TextField(field_uri='item:InReplyTo') is_submitted = BooleanField(field_uri='item:IsSubmitted', is_read_only=True) is_draft = BooleanField(field_uri='item:IsDraft', is_read_only=True) is_from_me = BooleanField(field_uri='item:IsFromMe', is_read_only=True) is_resend = BooleanField(field_uri='item:IsResend', is_read_only=True) is_unmodified = BooleanField(field_uri='item:IsUnmodified', is_read_only=True) headers = MessageHeaderField(field_uri='item:InternetMessageHeaders', is_read_only=True) datetime_sent = DateTimeField(field_uri='item:DateTimeSent', is_read_only=True) datetime_created = DateTimeField(field_uri='item:DateTimeCreated', is_read_only=True) response_objects = EWSElementField(field_uri='item:ResponseObjects', value_cls=ResponseObjects, is_read_only=True,) # Placeholder for ResponseObjects reminder_due_by = DateTimeField(field_uri='item:ReminderDueBy', is_required_after_save=True, is_searchable=False) reminder_is_set = BooleanField(field_uri='item:ReminderIsSet', is_required=True, default=False) reminder_minutes_before_start = IntegerField(field_uri='item:ReminderMinutesBeforeStart', is_required_after_save=True, min=0, default=0) display_cc = TextField(field_uri='item:DisplayCc', is_read_only=True) display_to = TextField(field_uri='item:DisplayTo', is_read_only=True) has_attachments = BooleanField(field_uri='item:HasAttachments', is_read_only=True) # ExtendedProperty fields go here culture = CultureField(field_uri='item:Culture', is_required_after_save=True, is_searchable=False) effective_rights = EffectiveRightsField(field_uri='item:EffectiveRights', is_read_only=True) last_modified_name = CharField(field_uri='item:LastModifiedName', is_read_only=True) last_modified_time = DateTimeField(field_uri='item:LastModifiedTime', is_read_only=True) is_associated = BooleanField(field_uri='item:IsAssociated', is_read_only=True, supported_from=EXCHANGE_2010) web_client_read_form_query_string = URIField(field_uri='item:WebClientReadFormQueryString', is_read_only=True, supported_from=EXCHANGE_2010) web_client_edit_form_query_string = URIField(field_uri='item:WebClientEditFormQueryString', is_read_only=True, supported_from=EXCHANGE_2010) conversation_id = EWSElementField(field_uri='item:ConversationId', value_cls=ConversationId, is_read_only=True, supported_from=EXCHANGE_2010) unique_body = BodyField(field_uri='item:UniqueBody', is_read_only=True, supported_from=EXCHANGE_2010) FIELDS = Fields() # Used to register extended properties INSERT_AFTER_FIELD = 'has_attachments' def __init__(self, **kwargs): super().__init__(**kwargs) if self.attachments: for a in self.attachments: if a.parent_item: if a.parent_item is not self: raise ValueError("'parent_item' of attachment %s must point to this item" % a) else: a.parent_item = self self.attach(self.attachments) else: self.attachments = [] def save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE): from .task import Task if self.id: item_id, changekey = self._update( update_fieldnames=update_fields, message_disposition=SAVE_ONLY, conflict_resolution=conflict_resolution, send_meeting_invitations=send_meeting_invitations ) if self.id != item_id \ and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)) \ and not isinstance(self, Task): # When we update an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so # the ID of this item changes. # # When we update certain fields on a task, the ID may change. A full description is available at # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation-task raise ValueError("'id' mismatch in returned update response") # Don't check that changekeys are different. No-op saves will sometimes leave the changekey intact self._id = self.ID_ELEMENT_CLS(item_id, changekey) else: if update_fields: raise ValueError("'update_fields' is only valid for updates") tmp_attachments = None if self.account and self.account.version.build < EXCHANGE_2013 and self.attachments: # At least some versions prior to Exchange 2013 can't save attachments immediately. You need to first # save, then attach. Store the attachment of this item temporarily and attach later. tmp_attachments, self.attachments = self.attachments, [] item = self._create(message_disposition=SAVE_ONLY, send_meeting_invitations=send_meeting_invitations) self._id = self.ID_ELEMENT_CLS(item.id, item.changekey) for old_att, new_att in zip(self.attachments, item.attachments): if old_att.attachment_id is not None: raise ValueError("Old 'attachment_id' is not empty") if new_att.attachment_id is None: raise ValueError("New 'attachment_id' is empty") old_att.attachment_id = new_att.attachment_id if tmp_attachments: # Exchange 2007 workaround. See above self.attach(tmp_attachments) return self @require_account def _create(self, message_disposition, send_meeting_invitations): # Return a BulkCreateResult because we want to return the ID of both the main item *and* attachments. In send # and send-and-save-copy mode, the server does not return an ID, so we just return True. return CreateItem(account=self.account).get( items=[self], folder=self.folder, message_disposition=message_disposition, send_meeting_invitations=send_meeting_invitations, ) def _update_fieldnames(self): from .contact import Contact, DistributionList # Return the list of fields we are allowed to update update_fieldnames = [] for f in self.supported_fields(version=self.account.version): if f.name == 'attachments': # Attachments are handled separately after item creation continue if f.is_read_only: # These cannot be changed continue if (f.is_required or f.is_required_after_save) and ( getattr(self, f.name) is None or (f.is_list and not getattr(self, f.name)) ): # These are required and cannot be deleted continue if not self.is_draft and f.is_read_only_after_send: # These cannot be changed when the item is no longer a draft continue if f.name == 'message_id' and f.is_read_only_after_send: # 'message_id' doesn't support updating, no matter the draft status continue if f.name == 'mime_content' and isinstance(self, (Contact, DistributionList)): # Contact and DistributionList don't support updating mime_content, no matter the draft status continue update_fieldnames.append(f.name) return update_fieldnames @require_account def _update(self, update_fieldnames, message_disposition, conflict_resolution, send_meeting_invitations): if not self.changekey: raise ValueError('%s must have changekey' % self.__class__.__name__) if not update_fieldnames: # The fields to update was not specified explicitly. Update all fields where update is possible update_fieldnames = self._update_fieldnames() return UpdateItem(account=self.account).get( items=[(self, update_fieldnames)], message_disposition=message_disposition, conflict_resolution=conflict_resolution, send_meeting_invitations_or_cancellations=send_meeting_invitations, suppress_read_receipts=True, expect_result=message_disposition != SEND_AND_SAVE_COPY, ) @require_id def refresh(self): # Updates the item based on fresh data from EWS from ..folders import Folder additional_fields = { FieldPath(field=f) for f in Folder(root=self.account.root).allowed_item_fields(version=self.account.version) } res = GetItem(account=self.account).get(items=[self], additional_fields=additional_fields, shape=ID_ONLY) if self.id != res.id and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)): # When we refresh an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so # the ID of this item changes. raise ValueError("'id' mismatch in returned update response") for f in self.FIELDS: setattr(self, f.name, getattr(res, f.name)) # 'parent_item' should point to 'self', not 'fresh_item'. That way, 'fresh_item' can be garbage collected. for a in self.attachments: a.parent_item = self return self @require_id def copy(self, to_folder): # If 'to_folder' is a public folder or a folder in a different mailbox then None is returned return CopyItem(account=self.account).get( items=[self], to_folder=to_folder, expect_result=None, ) @require_id def move(self, to_folder): res = MoveItem(account=self.account).get( items=[self], to_folder=to_folder, expect_result=None, ) if res is None: # Assume 'to_folder' is a public folder or a folder in a different mailbox self._id = None return self._id = self.ID_ELEMENT_CLS(*res) self.folder = to_folder def move_to_trash(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES, suppress_read_receipts=True): # Delete and move to the trash folder. self._delete(delete_type=MOVE_TO_DELETED_ITEMS, send_meeting_cancellations=send_meeting_cancellations, affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts) self._id = None self.folder = self.account.trash def soft_delete(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES, suppress_read_receipts=True): # Delete and move to the dumpster, if it is enabled. self._delete(delete_type=SOFT_DELETE, send_meeting_cancellations=send_meeting_cancellations, affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts) self._id = None self.folder = self.account.recoverable_items_deletions def delete(self, send_meeting_cancellations=SEND_TO_NONE, affected_task_occurrences=ALL_OCCURRENCIES, suppress_read_receipts=True): # Remove the item permanently. No copies are stored anywhere. self._delete(delete_type=HARD_DELETE, send_meeting_cancellations=send_meeting_cancellations, affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts) self._id, self.folder = None, None @require_id def _delete(self, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts): DeleteItem(account=self.account).get( items=[self], delete_type=delete_type, send_meeting_cancellations=send_meeting_cancellations, affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts, ) @require_id def archive(self, to_folder): return ArchiveItem(account=self.account).get(items=[self], to_folder=to_folder, expect_result=True) def attach(self, attachments): """Add an attachment, or a list of attachments, to this item. If the item has already been saved, the attachments will be created on the server immediately. If the item has not yet been saved, the attachments will be created on the server when the item is saved. Adding attachments to an existing item will update the changekey of the item. :param attachments: """ if not is_iterable(attachments, generators_allowed=True): attachments = [attachments] for a in attachments: if not a.parent_item: a.parent_item = self if self.id and not a.attachment_id: # Already saved object. Attach the attachment server-side now a.attach() if a not in self.attachments: self.attachments.append(a) def detach(self, attachments): """Remove an attachment, or a list of attachments, from this item. If the item has already been saved, the attachments will be deleted on the server immediately. If the item has not yet been saved, the attachments will simply not be created on the server the item is saved. Removing attachments from an existing item will update the changekey of the item. :param attachments: """ if not is_iterable(attachments, generators_allowed=True): attachments = [attachments] if attachments is self.attachments: # Don't remove from the same list we are iterating attachments = list(attachments) for a in attachments: if a.parent_item is not self: raise ValueError('Attachment does not belong to this item') if self.id: # Item is already created. Detach the attachment server-side now a.detach() if a in self.attachments: self.attachments.remove(a) @require_id def create_forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None): from .message import ForwardItem return ForwardItem( account=self.account, reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey), subject=subject, new_body=body, to_recipients=to_recipients, cc_recipients=cc_recipients, bcc_recipients=bcc_recipients, ) def forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None): self.create_forward( subject, body, to_recipients, cc_recipients, bcc_recipients, ).send() exchangelib-4.6.1/exchangelib/items/message.py000066400000000000000000000213631414601472700213760ustar00rootroot00000000000000import logging from .base import BaseReplyItem, AUTO_RESOLVE, SEND_TO_NONE, SEND_ONLY, SEND_AND_SAVE_COPY from .item import Item from ..fields import BooleanField, Base64Field, TextField, MailboxField, MailboxListField, CharField, EWSElementField from ..properties import ReferenceItemId, ReminderMessageData from ..services import SendItem, MarkAsJunk from ..util import require_account, require_id from ..version import EXCHANGE_2013, EXCHANGE_2013_SP1 log = logging.getLogger(__name__) class Message(Item): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/message-ex15websvcsotherref """ ELEMENT_NAME = 'Message' sender = MailboxField(field_uri='message:Sender', is_read_only=True, is_read_only_after_send=True) to_recipients = MailboxListField(field_uri='message:ToRecipients', is_read_only_after_send=True, is_searchable=False) cc_recipients = MailboxListField(field_uri='message:CcRecipients', is_read_only_after_send=True, is_searchable=False) bcc_recipients = MailboxListField(field_uri='message:BccRecipients', is_read_only_after_send=True, is_searchable=False) is_read_receipt_requested = BooleanField(field_uri='message:IsReadReceiptRequested', is_required=True, default=False, is_read_only_after_send=True) is_delivery_receipt_requested = BooleanField(field_uri='message:IsDeliveryReceiptRequested', is_required=True, default=False, is_read_only_after_send=True) conversation_index = Base64Field(field_uri='message:ConversationIndex', is_read_only=True) conversation_topic = CharField(field_uri='message:ConversationTopic', is_read_only=True) # Rename 'From' to 'author'. We can't use fieldname 'from' since it's a Python keyword. author = MailboxField(field_uri='message:From', is_read_only_after_send=True) message_id = CharField(field_uri='message:InternetMessageId', is_read_only_after_send=True) is_read = BooleanField(field_uri='message:IsRead', is_required=True, default=False) is_response_requested = BooleanField(field_uri='message:IsResponseRequested', default=False, is_required=True) references = TextField(field_uri='message:References') reply_to = MailboxListField(field_uri='message:ReplyTo', is_read_only_after_send=True, is_searchable=False) received_by = MailboxField(field_uri='message:ReceivedBy', is_read_only=True) received_representing = MailboxField(field_uri='message:ReceivedRepresenting', is_read_only=True) reminder_message_data = EWSElementField(field_uri='message:ReminderMessageData', value_cls=ReminderMessageData, supported_from=EXCHANGE_2013_SP1, is_read_only=True) @require_account def send(self, save_copy=True, copy_to_folder=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE): # Only sends a message. The message can either be an existing draft stored in EWS or a new message that does # not yet exist in EWS. 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.account.sent # 'Sent' is default EWS behaviour if self.id: SendItem(account=self.account).get(items=[self], saved_item_folder=copy_to_folder) # The item will be deleted from the original folder self._id = None self.folder = copy_to_folder return None # New message if copy_to_folder: # This would better be done via send_and_save() but lets just support it here self.folder = copy_to_folder return self.send_and_save(conflict_resolution=conflict_resolution, send_meeting_invitations=send_meeting_invitations) if self.account.version.build < EXCHANGE_2013 and self.attachments: # At least some versions prior to Exchange 2013 can't send attachments immediately. You need to first save, # then attach, then send. This is done in send_and_save(). send() will delete the item again. self.send_and_save(conflict_resolution=conflict_resolution, send_meeting_invitations=send_meeting_invitations) return None self._create(message_disposition=SEND_ONLY, send_meeting_invitations=send_meeting_invitations) return None def send_and_save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE): # Sends Message and saves a copy in the parent folder. Does not return an ItemId. if self.id: self._update( update_fieldnames=update_fields, message_disposition=SEND_AND_SAVE_COPY, conflict_resolution=conflict_resolution, send_meeting_invitations=send_meeting_invitations ) else: if self.account.version.build < EXCHANGE_2013 and self.attachments: # At least some versions prior to Exchange 2013 can't send-and-save attachments immediately. You need # to first save, then attach, then send. This is done in save(). self.save(update_fields=update_fields, conflict_resolution=conflict_resolution, send_meeting_invitations=send_meeting_invitations) self.send(save_copy=False, conflict_resolution=conflict_resolution, send_meeting_invitations=send_meeting_invitations) else: res = self._create( message_disposition=SEND_AND_SAVE_COPY, send_meeting_invitations=send_meeting_invitations ) if res is not True: raise ValueError('Unexpected response in send-only mode') @require_id def create_reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None): if to_recipients is None: if not self.author: raise ValueError("'to_recipients' must be set when message has no 'author'") to_recipients = [self.author] return ReplyToItem( account=self.account, reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey), subject=subject, new_body=body, to_recipients=to_recipients, cc_recipients=cc_recipients, bcc_recipients=bcc_recipients, ) def reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None): self.create_reply( subject, body, to_recipients, cc_recipients, bcc_recipients ).send() @require_id def create_reply_all(self, subject, body): to_recipients = list(self.to_recipients) if self.to_recipients else [] if self.author: to_recipients.append(self.author) return ReplyAllToItem( account=self.account, reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey), subject=subject, new_body=body, to_recipients=to_recipients, cc_recipients=self.cc_recipients, bcc_recipients=self.bcc_recipients, ) def reply_all(self, subject, body): self.create_reply_all(subject, body).send() def mark_as_junk(self, is_junk=True, move_item=True): """Mark or un-marks items as junk email. :param is_junk: If True, the sender will be added from the blocked sender list. Otherwise, the sender will be removed. :param move_item: If true, the item will be moved to the junk folder. :return: """ res = MarkAsJunk(account=self.account).get( items=[self], is_junk=is_junk, move_item=move_item, expect_result=move_item ) if res is None: return self.folder = self.account.junk if is_junk else self.account.inbox self.id, self.changekey = res class ReplyToItem(BaseReplyItem): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/replytoitem""" ELEMENT_NAME = 'ReplyToItem' class ReplyAllToItem(BaseReplyItem): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/replyalltoitem""" ELEMENT_NAME = 'ReplyAllToItem' class ForwardItem(BaseReplyItem): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/forwarditem""" ELEMENT_NAME = 'ForwardItem' exchangelib-4.6.1/exchangelib/items/post.py000066400000000000000000000025741414601472700207420ustar00rootroot00000000000000import logging from .item import Item from .message import Message from ..fields import TextField, BodyField, DateTimeField, MailboxField log = logging.getLogger(__name__) class PostItem(Item): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postitem""" ELEMENT_NAME = 'PostItem' conversation_index = Message.FIELDS['conversation_index'] conversation_topic = Message.FIELDS['conversation_topic'] author = Message.FIELDS['author'] message_id = Message.FIELDS['message_id'] is_read = Message.FIELDS['is_read'] posted_time = DateTimeField(field_uri='postitem:PostedTime', is_read_only=True) references = TextField(field_uri='message:References') sender = MailboxField(field_uri='message:Sender', is_read_only=True, is_read_only_after_send=True) class PostReplyItem(Item): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postreplyitem""" ELEMENT_NAME = 'PostReplyItem' # This element only has Item fields up to, and including, 'culture' # TDO: Plus all message fields new_body = BodyField(field_uri='NewBodyContent') # Accepts and returns Body or HTMLBody instances culture_idx = Item.FIELDS.index_by_name('culture') sender_idx = Message.FIELDS.index_by_name('sender') FIELDS = Item.FIELDS[:culture_idx + 1] + Message.FIELDS[sender_idx:] exchangelib-4.6.1/exchangelib/items/task.py000066400000000000000000000120531414601472700207100ustar00rootroot00000000000000import datetime import logging from decimal import Decimal from .item import Item from ..ewsdatetime import EWSDateTime, UTC from ..fields import BooleanField, IntegerField, DecimalField, TextField, ChoiceField, DateTimeField, Choice, \ CharField, TextListField, TaskRecurrenceField, DateTimeBackedDateField log = logging.getLogger(__name__) class Task(Item): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/task""" ELEMENT_NAME = 'Task' NOT_STARTED = 'NotStarted' COMPLETED = 'Completed' actual_work = IntegerField(field_uri='task:ActualWork', min=0) assigned_time = DateTimeField(field_uri='task:AssignedTime', is_read_only=True) billing_information = TextField(field_uri='task:BillingInformation') change_count = IntegerField(field_uri='task:ChangeCount', is_read_only=True, min=0) companies = TextListField(field_uri='task:Companies') # 'complete_date' can be set, but is ignored by the server, which sets it to now() complete_date = DateTimeField(field_uri='task:CompleteDate', is_read_only=True) contacts = TextListField(field_uri='task:Contacts') delegation_state = ChoiceField(field_uri='task:DelegationState', choices={ Choice('NoMatch'), Choice('OwnNew'), Choice('Owned'), Choice('Accepted'), Choice('Declined'), Choice('Max') }, is_read_only=True) delegator = CharField(field_uri='task:Delegator', is_read_only=True) due_date = DateTimeBackedDateField(field_uri='task:DueDate') is_editable = BooleanField(field_uri='task:IsAssignmentEditable', is_read_only=True) is_complete = BooleanField(field_uri='task:IsComplete', is_read_only=True) is_recurring = BooleanField(field_uri='task:IsRecurring', is_read_only=True) is_team_task = BooleanField(field_uri='task:IsTeamTask', is_read_only=True) mileage = TextField(field_uri='task:Mileage') owner = CharField(field_uri='task:Owner', is_read_only=True) percent_complete = DecimalField(field_uri='task:PercentComplete', is_required=True, default=Decimal(0.0), min=Decimal(0), max=Decimal(100), is_searchable=False) recurrence = TaskRecurrenceField(field_uri='task:Recurrence', is_searchable=False) start_date = DateTimeBackedDateField(field_uri='task:StartDate') status = ChoiceField(field_uri='task:Status', choices={ Choice(NOT_STARTED), Choice('InProgress'), Choice(COMPLETED), Choice('WaitingOnOthers'), Choice('Deferred') }, is_required=True, is_searchable=False, default=NOT_STARTED) status_description = CharField(field_uri='task:StatusDescription', is_read_only=True) total_work = IntegerField(field_uri='task:TotalWork', min=0) def clean(self, version=None): super().clean(version=version) if self.due_date and self.start_date and self.due_date < self.start_date: log.warning("'due_date' must be greater than 'start_date' (%s vs %s). Resetting 'due_date'", self.due_date, self.start_date) self.due_date = self.start_date if self.complete_date: if self.status != self.COMPLETED: log.warning("'status' must be '%s' when 'complete_date' is set (%s). Resetting", self.COMPLETED, self.status) self.status = self.COMPLETED now = datetime.datetime.now(tz=UTC) if (self.complete_date - now).total_seconds() > 120: # Reset complete_date values that are in the future # 'complete_date' can be set automatically by the server. Allow some grace between local and server time log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now) self.complete_date = now if self.start_date and self.complete_date.date() < self.start_date: log.warning("'complete_date' must be greater than 'start_date' (%s vs %s). Resetting", self.complete_date, self.start_date) self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC) if self.percent_complete is not None: if self.status == self.COMPLETED and self.percent_complete != Decimal(100): # percent_complete must be 100% if task is complete log.warning("'percent_complete' must be 100 when 'status' is '%s' (%s). Resetting", self.COMPLETED, self.percent_complete) self.percent_complete = Decimal(100) elif self.status == self.NOT_STARTED and self.percent_complete != Decimal(0): # percent_complete must be 0% if task is not started log.warning("'percent_complete' must be 0 when 'status' is '%s' (%s). Resetting", self.NOT_STARTED, self.percent_complete) self.percent_complete = Decimal(0) def complete(self): # A helper method to mark a task as complete on the server self.status = Task.COMPLETED self.percent_complete = Decimal(100) self.save() exchangelib-4.6.1/exchangelib/properties.py000066400000000000000000002105551414601472700210300ustar00rootroot00000000000000import abc import binascii import codecs import datetime import logging import struct from inspect import getmro from threading import Lock from .errors import TimezoneDefinitionInvalidForYear from .fields import SubField, TextField, EmailAddressField, ChoiceField, DateTimeField, EWSElementField, MailboxField, \ Choice, BooleanField, IdField, ExtendedPropertyField, IntegerField, TimeField, EnumField, CharField, EmailField, \ EWSElementListField, EnumListField, FreeBusyStatusField, UnknownEntriesField, MessageField, RecipientAddressField, \ RoutingTypeField, WEEKDAY_NAMES, FieldPath, Field, AssociatedCalendarItemIdField, ReferenceItemIdField, \ Base64Field, TypeValueField, DictionaryField, IdElementField, CharListField, GenericEventListField, \ InvalidField, InvalidFieldForVersion from .util import get_xml_attr, create_element, set_xml_value, value_to_xml_text, MNS, TNS from .version import Version, EXCHANGE_2013, Build log = logging.getLogger(__name__) class Fields(list): """A collection type for the FIELDS class attribute. Works like a list but supports fast lookup by name.""" def __init__(self, *fields): super().__init__(fields) self._dict = {} for f in fields: # Check for duplicate field names if f.name in self._dict: raise ValueError('Field %r is a duplicate' % f) self._dict[f.name] = f def __getitem__(self, idx_or_slice): # Support fast lookup by name. Make sure slicing returns an instance of this class if isinstance(idx_or_slice, str): return self._dict[idx_or_slice] if isinstance(idx_or_slice, int): return super().__getitem__(idx_or_slice) res = super().__getitem__(idx_or_slice) return self.__class__(*res) def __add__(self, other): # Make sure addition returns an instance of this class res = super().__add__(other) return self.__class__(*res) def __iadd__(self, other): for f in other: self.append(f) return self def __contains__(self, item): if isinstance(item, str): return item in self._dict return super().__contains__(item) def copy(self): return self.__class__(*self) def index_by_name(self, field_name): for i, f in enumerate(self): if f.name == field_name: return i raise ValueError('Unknown field name %r' % field_name) def insert(self, index, field): if field.name in self._dict: raise ValueError('Field %r is a duplicate' % field) super().insert(index, field) self._dict[field.name] = field def remove(self, field): super().remove(field) del self._dict[field.name] def append(self, field): super().append(field) self._dict[field.name] = field class Body(str): """Helper to mark the 'body' field as a complex attribute. MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/body """ body_type = 'Text' def __add__(self, other): # Make sure Body('') + 'foo' returns a Body type return self.__class__(super().__add__(other)) def __mod__(self, other): # Make sure Body('%s') % 'foo' returns a Body type return self.__class__(super().__mod__(other)) def format(self, *args, **kwargs): # Make sure Body('{}').format('foo') returns a Body type return self.__class__(super().format(*args, **kwargs)) class HTMLBody(Body): """Helper to mark the 'body' field as a complex attribute. MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/body """ body_type = 'HTML' class UID(bytes): """Helper class to encode Calendar UIDs. See issue #453. Example: class GlobalObjectId(ExtendedProperty): distinguished_property_set_id = 'Meeting' property_id = 3 property_type = 'Binary' CalendarItem.register('global_object_id', GlobalObjectId) account.calendar.filter(global_object_id=UID('261cbc18-1f65-5a0a-bd11-23b1e224cc2f')) """ _HEADER = binascii.hexlify(bytearray(( 0x04, 0x00, 0x00, 0x00, 0x82, 0x00, 0xE0, 0x00, 0x74, 0xC5, 0xB7, 0x10, 0x1A, 0x82, 0xE0, 0x08))) _EXCEPTION_REPLACEMENT_TIME = binascii.hexlify(bytearray(( 0, 0, 0, 0))) _CREATION_TIME = binascii.hexlify(bytearray(( 0, 0, 0, 0, 0, 0, 0, 0))) _RESERVED = binascii.hexlify(bytearray(( 0, 0, 0, 0, 0, 0, 0, 0))) # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxocal/1d3aac05-a7b9-45cc-a213-47f0a0a2c5c1 # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-asemail/e7424ddc-dd10-431e-a0b7-5c794863370e # https://stackoverflow.com/questions/42259122 # https://stackoverflow.com/questions/33757805 def __new__(cls, uid): payload = binascii.hexlify(bytearray('vCal-Uid\x01\x00\x00\x00{}\x00'.format(uid).encode('ascii'))) length = binascii.hexlify(bytearray(struct.pack(' for_year: break if period_type != 'Standard': continue valid_period = period if valid_period is None: raise TimezoneDefinitionInvalidForYear('Year %s not included in periods %s' % (for_year, periods)) return int(valid_period['bias'].total_seconds()) // 60 # Convert to minutes @staticmethod def _get_valid_transition_id(transitions, for_year): # Look through the transitions, and pick the relevant one according to the 'for_year' value valid_tg_id = None for tg_id, from_date in sorted(transitions.items()): if from_date and from_date.year > for_year: break valid_tg_id = tg_id if valid_tg_id is None: raise ValueError('No valid transition for year %s: %s' % (for_year, transitions)) return valid_tg_id @staticmethod def _get_std_and_dst(transitiongroup, periods, bias): # Return 'standard_time' and 'daylight_time' objects. We do unnecessary work here, but it keeps code simple. standard_time, daylight_time = None, None for transition in transitiongroup: period = periods[transition['to']] if len(transition) == 1: # This is a simple transition representing a timezone with no DST. Some servers don't accept TimeZone # elements without a STD and DST element (see issue #488). Return StandardTime and DaylightTime objects # with dummy values and 0 bias - this satisfies the broken servers and hopefully doesn't break the # well-behaving servers. standard_time = StandardTime(bias=0, time=datetime.time(0), occurrence=1, iso_month=1, weekday=1) daylight_time = DaylightTime(bias=0, time=datetime.time(0), occurrence=5, iso_month=12, weekday=7) continue # 'offset' is the time of day to transition, as timedelta since midnight. Must be a reasonable value if not datetime.timedelta(0) <= transition['offset'] < datetime.timedelta(days=1): raise ValueError("'offset' value %s must be be between 0 and 24 hours" % transition['offset']) transition_kwargs = dict( time=(datetime.datetime(2000, 1, 1) + transition['offset']).time(), occurrence=transition['occurrence'], iso_month=transition['iso_month'], weekday=transition['iso_weekday'], ) if period['name'] == 'Standard': transition_kwargs['bias'] = 0 standard_time = StandardTime(**transition_kwargs) continue if period['name'] == 'Daylight': dst_bias = int(period['bias'].total_seconds()) // 60 # Convert to minutes transition_kwargs['bias'] = dst_bias - bias daylight_time = DaylightTime(**transition_kwargs) continue raise ValueError('Unknown transition: %s' % transition) return standard_time, daylight_time class CalendarView(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendarview""" ELEMENT_NAME = 'CalendarView' NAMESPACE = MNS start = DateTimeField(field_uri='StartDate', is_required=True, is_attribute=True) end = DateTimeField(field_uri='EndDate', is_required=True, is_attribute=True) max_items = IntegerField(field_uri='MaxEntriesReturned', min=1, is_attribute=True) def clean(self, version=None): super().clean(version=version) if self.end < self.start: raise ValueError("'start' must be before 'end'") class CalendarEventDetails(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendareventdetails""" ELEMENT_NAME = 'CalendarEventDetails' id = CharField(field_uri='ID') subject = CharField(field_uri='Subject') location = CharField(field_uri='Location') is_meeting = BooleanField(field_uri='IsMeeting') is_recurring = BooleanField(field_uri='IsRecurring') is_exception = BooleanField(field_uri='IsException') is_reminder_set = BooleanField(field_uri='IsReminderSet') is_private = BooleanField(field_uri='IsPrivate') class CalendarEvent(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendarevent""" ELEMENT_NAME = 'CalendarEvent' start = DateTimeField(field_uri='StartTime') end = DateTimeField(field_uri='EndTime') busy_type = FreeBusyStatusField(field_uri='BusyType', is_required=True, default='Busy') details = EWSElementField(value_cls=CalendarEventDetails) class WorkingPeriod(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/workingperiod""" ELEMENT_NAME = 'WorkingPeriod' weekdays = EnumListField(field_uri='DayOfWeek', enum=WEEKDAY_NAMES, is_required=True) start = TimeField(field_uri='StartTimeInMinutes', is_required=True) end = TimeField(field_uri='EndTimeInMinutes', is_required=True) class FreeBusyView(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/freebusyview""" ELEMENT_NAME = 'FreeBusyView' NAMESPACE = MNS view_type = ChoiceField(field_uri='FreeBusyViewType', choices={ Choice('None'), Choice('MergedOnly'), Choice('FreeBusy'), Choice('FreeBusyMerged'), Choice('Detailed'), Choice('DetailedMerged'), }, is_required=True) # A string of digits. Each digit points to a position in .fields.FREE_BUSY_CHOICES merged = CharField(field_uri='MergedFreeBusy') calendar_events = EWSElementListField(field_uri='CalendarEventArray', value_cls=CalendarEvent) # WorkingPeriod is located inside the WorkingPeriodArray element which is inside the WorkingHours element working_hours = EWSElementListField(field_uri='WorkingPeriodArray', value_cls=WorkingPeriod) # TimeZone is also inside the WorkingHours element. It contains information about the timezone which the # account is located in. working_hours_timezone = EWSElementField(value_cls=TimeZone) @classmethod def from_xml(cls, elem, account): kwargs = {} working_hours_elem = elem.find('{%s}WorkingHours' % TNS) for f in cls.FIELDS: if f.name in ['working_hours', 'working_hours_timezone']: if working_hours_elem is None: continue kwargs[f.name] = f.from_xml(elem=working_hours_elem, account=account) continue kwargs[f.name] = f.from_xml(elem=elem, account=account) cls._clear(elem) return cls(**kwargs) class RoomList(Mailbox): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/roomlist""" ELEMENT_NAME = 'RoomList' NAMESPACE = MNS @classmethod def response_tag(cls): # In a GetRoomLists response, room lists are delivered as Address elements. See # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/address-emailaddresstype return '{%s}Address' % TNS class Room(Mailbox): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/room""" ELEMENT_NAME = 'Room' @classmethod def from_xml(cls, elem, account): id_elem = elem.find('{%s}Id' % TNS) item_id_elem = id_elem.find(ItemId.response_tag()) kwargs = dict( name=get_xml_attr(id_elem, '{%s}Name' % TNS), email_address=get_xml_attr(id_elem, '{%s}EmailAddress' % TNS), mailbox_type=get_xml_attr(id_elem, '{%s}MailboxType' % TNS), item_id=ItemId.from_xml(elem=item_id_elem, account=account) if item_id_elem else None, ) cls._clear(elem) return cls(**kwargs) class Member(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/member-ex15websvcsotherref """ ELEMENT_NAME = 'Member' mailbox = MailboxField(is_required=True) status = ChoiceField(field_uri='Status', choices={ Choice('Unrecognized'), Choice('Normal'), Choice('Demoted') }, default='Normal') def __hash__(self): return hash(self.mailbox) class UserId(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/userid""" ELEMENT_NAME = 'UserId' sid = CharField(field_uri='SID') primary_smtp_address = EmailAddressField(field_uri='PrimarySmtpAddress') display_name = CharField(field_uri='DisplayName') distinguished_user = ChoiceField(field_uri='DistinguishedUser', choices={ Choice('Default'), Choice('Anonymous') }) external_user_identity = CharField(field_uri='ExternalUserIdentity') class BasePermission(EWSElement, metaclass=EWSMeta): """Base class for the Permission and CalendarPermission classes""" PERMISSION_ENUM = {Choice('None'), Choice('Owned'), Choice('All')} can_create_items = BooleanField(field_uri='CanCreateItems', default=False) can_create_subfolders = BooleanField(field_uri='CanCreateSubfolders', default=False) is_folder_owner = BooleanField(field_uri='IsFolderOwner', default=False) is_folder_visible = BooleanField(field_uri='IsFolderVisible', default=False) is_folder_contact = BooleanField(field_uri='IsFolderContact', default=False) edit_items = ChoiceField(field_uri='EditItems', choices=PERMISSION_ENUM, default='None') delete_items = ChoiceField(field_uri='DeleteItems', choices=PERMISSION_ENUM, default='None') read_items = ChoiceField(field_uri='ReadItems', choices={Choice('None'), Choice('FullDetails')}, default='None') user_id = EWSElementField(value_cls=UserId, is_required=True) class Permission(BasePermission): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/permission""" ELEMENT_NAME = 'Permission' LEVEL_CHOICES = ( 'None', 'Owner', 'PublishingEditor', 'Editor', 'PublishingAuthor', 'Author', 'NoneditingAuthor', 'Reviewer', 'Contributor', 'Custom', ) permission_level = ChoiceField( field_uri='CalendarPermissionLevel', choices={Choice(c) for c in LEVEL_CHOICES}, default=LEVEL_CHOICES[0] ) class CalendarPermission(BasePermission): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendarpermission""" ELEMENT_NAME = 'CalendarPermission' LEVEL_CHOICES = ( 'None', 'Owner', 'PublishingEditor', 'Editor', 'PublishingAuthor', 'Author', 'NoneditingAuthor', 'Reviewer', 'Contributor', 'FreeBusyTimeOnly', 'FreeBusyTimeAndSubjectAndLocation', 'Custom', ) calendar_permission_level = ChoiceField( field_uri='CalendarPermissionLevel', choices={Choice(c) for c in LEVEL_CHOICES}, default=LEVEL_CHOICES[0] ) class PermissionSet(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/permissionset-permissionsettype and https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/permissionset-calendarpermissionsettype """ # For simplicity, we implement the two distinct but equally names elements as one class. ELEMENT_NAME = 'PermissionSet' permissions = EWSElementListField(field_uri='Permissions', value_cls=Permission) calendar_permissions = EWSElementListField(field_uri='CalendarPermissions', value_cls=CalendarPermission) unknown_entries = UnknownEntriesField(field_uri='UnknownEntries') class EffectiveRights(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/effectiverights""" ELEMENT_NAME = 'EffectiveRights' create_associated = BooleanField(field_uri='CreateAssociated', default=False) create_contents = BooleanField(field_uri='CreateContents', default=False) create_hierarchy = BooleanField(field_uri='CreateHierarchy', default=False) delete = BooleanField(field_uri='Delete', default=False) modify = BooleanField(field_uri='Modify', default=False) read = BooleanField(field_uri='Read', default=False) view_private_items = BooleanField(field_uri='ViewPrivateItems', default=False) def __contains__(self, item): return getattr(self, item, False) class DelegatePermissions(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/delegatepermissions""" ELEMENT_NAME = 'DelegatePermissions' PERMISSION_LEVEL_CHOICES = { Choice('None'), Choice('Editor'), Choice('Reviewer'), Choice('Author'), Choice('Custom'), } calendar_folder_permission_level = ChoiceField(field_uri='CalendarFolderPermissionLevel', choices=PERMISSION_LEVEL_CHOICES, default='None') tasks_folder_permission_level = ChoiceField(field_uri='TasksFolderPermissionLevel', choices=PERMISSION_LEVEL_CHOICES, default='None') inbox_folder_permission_level = ChoiceField(field_uri='InboxFolderPermissionLevel', choices=PERMISSION_LEVEL_CHOICES, default='None') contacts_folder_permission_level = ChoiceField(field_uri='ContactsFolderPermissionLevel', choices=PERMISSION_LEVEL_CHOICES, default='None') notes_folder_permission_level = ChoiceField(field_uri='NotesFolderPermissionLevel', choices=PERMISSION_LEVEL_CHOICES, default='None') journal_folder_permission_level = ChoiceField(field_uri='JournalFolderPermissionLevel', choices=PERMISSION_LEVEL_CHOICES, default='None') class DelegateUser(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/delegateuser""" ELEMENT_NAME = 'DelegateUser' NAMESPACE = MNS user_id = EWSElementField(value_cls=UserId) delegate_permissions = EWSElementField(value_cls=DelegatePermissions) receive_copies_of_meeting_messages = BooleanField(field_uri='ReceiveCopiesOfMeetingMessages', default=False) view_private_items = BooleanField(field_uri='ViewPrivateItems', default=False) class SearchableMailbox(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/searchablemailbox""" ELEMENT_NAME = 'SearchableMailbox' guid = CharField(field_uri='Guid') primary_smtp_address = EmailAddressField(field_uri='PrimarySmtpAddress') is_external = BooleanField(field_uri='IsExternalMailbox') external_email = EmailAddressField(field_uri='ExternalEmailAddress') display_name = CharField(field_uri='DisplayName') is_membership_group = BooleanField(field_uri='IsMembershipGroup') reference_id = CharField(field_uri='ReferenceId') class FailedMailbox(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/failedmailbox""" ELEMENT_NAME = 'FailedMailbox' mailbox = CharField(field_uri='Mailbox') error_code = IntegerField(field_uri='ErrorCode') error_message = CharField(field_uri='ErrorMessage') is_archive = BooleanField(field_uri='IsArchive') # MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailtipsrequested MAIL_TIPS_TYPES = ( 'All', 'OutOfOfficeMessage', 'MailboxFullStatus', 'CustomMailTip', 'ExternalMemberCount', 'TotalMemberCount', 'MaxMessageSize', 'DeliveryRestriction', 'ModerationStatus', 'InvalidRecipient', ) class OutOfOffice(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/outofoffice""" ELEMENT_NAME = 'OutOfOffice' reply_body = MessageField(field_uri='ReplyBody') start = DateTimeField(field_uri='StartTime', is_required=False) end = DateTimeField(field_uri='EndTime', is_required=False) @classmethod def duration_to_start_end(cls, elem, account): kwargs = {} duration = elem.find('{%s}Duration' % TNS) if duration is not None: for attr in ('start', 'end'): f = cls.get_field_by_fieldname(attr) kwargs[attr] = f.from_xml(elem=duration, account=account) return kwargs @classmethod def from_xml(cls, elem, account): kwargs = {} for attr in ('reply_body',): f = cls.get_field_by_fieldname(attr) kwargs[attr] = f.from_xml(elem=elem, account=account) kwargs.update(cls.duration_to_start_end(elem=elem, account=account)) cls._clear(elem) return cls(**kwargs) class MailTips(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailtips""" ELEMENT_NAME = 'MailTips' NAMESPACE = MNS recipient_address = RecipientAddressField() pending_mail_tips = ChoiceField(field_uri='PendingMailTips', choices={Choice(c) for c in MAIL_TIPS_TYPES}) out_of_office = EWSElementField(value_cls=OutOfOffice) mailbox_full = BooleanField(field_uri='MailboxFull') custom_mail_tip = TextField(field_uri='CustomMailTip') total_member_count = IntegerField(field_uri='TotalMemberCount') external_member_count = IntegerField(field_uri='ExternalMemberCount') max_message_size = IntegerField(field_uri='MaxMessageSize') delivery_restricted = BooleanField(field_uri='DeliveryRestricted') is_moderated = BooleanField(field_uri='IsModerated') invalid_recipient = BooleanField(field_uri='InvalidRecipient') ENTRY_ID = 'EntryId' # The base64-encoded PR_ENTRYID property EWS_ID = 'EwsId' # The EWS format used in Exchange 2007 SP1 and later EWS_LEGACY_ID = 'EwsLegacyId' # The EWS format used in Exchange 2007 before SP1 HEX_ENTRY_ID = 'HexEntryId' # The hexadecimal representation of the PR_ENTRYID property OWA_ID = 'OwaId' # The OWA format for Exchange 2007 and 2010 STORE_ID = 'StoreId' # The Exchange Store format # IdFormat enum ID_FORMATS = (ENTRY_ID, EWS_ID, EWS_LEGACY_ID, HEX_ENTRY_ID, OWA_ID, STORE_ID) class AlternateId(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/alternateid""" ELEMENT_NAME = 'AlternateId' id = CharField(field_uri='Id', is_required=True, is_attribute=True) format = ChoiceField(field_uri='Format', is_required=True, is_attribute=True, choices={Choice(c) for c in ID_FORMATS}) mailbox = EmailAddressField(field_uri='Mailbox', is_required=True, is_attribute=True) is_archive = BooleanField(field_uri='IsArchive', is_required=False, is_attribute=True) @classmethod def response_tag(cls): # This element is in TNS in the request and MNS in the response... return '{%s}%s' % (MNS, cls.ELEMENT_NAME) class AlternatePublicFolderId(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/alternatepublicfolderid""" ELEMENT_NAME = 'AlternatePublicFolderId' folder_id = CharField(field_uri='FolderId', is_required=True, is_attribute=True) format = ChoiceField(field_uri='Format', is_required=True, is_attribute=True, choices={Choice(c) for c in ID_FORMATS}) class AlternatePublicFolderItemId(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/alternatepublicfolderitemid """ ELEMENT_NAME = 'AlternatePublicFolderItemId' folder_id = CharField(field_uri='FolderId', is_required=True, is_attribute=True) format = ChoiceField(field_uri='Format', is_required=True, is_attribute=True, choices={Choice(c) for c in ID_FORMATS}) item_id = CharField(field_uri='ItemId', is_required=True, is_attribute=True) class FieldURI(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/fielduri""" ELEMENT_NAME = 'FieldURI' field_uri = CharField(field_uri='FieldURI', is_attribute=True, is_required=True) class IndexedFieldURI(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/indexedfielduri""" ELEMENT_NAME = 'IndexedFieldURI' field_uri = CharField(field_uri='FieldURI', is_attribute=True, is_required=True) field_index = CharField(field_uri='FieldIndex', is_attribute=True, is_required=True) class ExtendedFieldURI(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedfielduri""" ELEMENT_NAME = 'ExtendedFieldURI' distinguished_property_set_id = CharField(field_uri='DistinguishedPropertySetId', is_attribute=True) property_set_id = CharField(field_uri='PropertySetId', is_attribute=True) property_tag = CharField(field_uri='PropertyTag', is_attribute=True) property_name = CharField(field_uri='PropertyName', is_attribute=True) property_id = CharField(field_uri='PropertyId', is_attribute=True) property_type = CharField(field_uri='PropertyType', is_attribute=True) class ExceptionFieldURI(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/exceptionfielduri""" ELEMENT_NAME = 'ExceptionFieldURI' field_uri = CharField(field_uri='FieldURI', is_attribute=True, is_required=True) class CompleteName(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/completename""" ELEMENT_NAME = 'CompleteName' title = CharField(field_uri='Title') first_name = CharField(field_uri='FirstName') middle_name = CharField(field_uri='MiddleName') last_name = CharField(field_uri='LastName') suffix = CharField(field_uri='Suffix') initials = CharField(field_uri='Initials') full_name = CharField(field_uri='FullName') nickname = CharField(field_uri='Nickname') yomi_first_name = CharField(field_uri='YomiFirstName') yomi_last_name = CharField(field_uri='YomiLastName') class ReminderMessageData(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/remindermessagedata""" ELEMENT_NAME = 'ReminderMessageData' reminder_text = CharField(field_uri='ReminderText') location = CharField(field_uri='Location') start_time = TimeField(field_uri='StartTime') end_time = TimeField(field_uri='EndTime') associated_calendar_item_id = AssociatedCalendarItemIdField(field_uri='AssociatedCalendarItemId', supported_from=Build(15, 0, 913, 9)) class AcceptSharingInvitation(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/acceptsharinginvitation""" ELEMENT_NAME = 'AcceptSharingInvitation' reference_item_id = ReferenceItemIdField(field_uri='item:ReferenceItemId') class SuppressReadReceipt(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/suppressreadreceipt""" ELEMENT_NAME = 'SuppressReadReceipt' reference_item_id = ReferenceItemIdField(field_uri='item:ReferenceItemId') class RemoveItem(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/removeitem""" ELEMENT_NAME = 'RemoveItem' reference_item_id = ReferenceItemIdField(field_uri='item:ReferenceItemId') class ResponseObjects(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/responseobjects""" ELEMENT_NAME = 'ResponseObjects' NAMESPACE = EWSElement.NAMESPACE accept_item = EWSElementField(field_uri='AcceptItem', value_cls='AcceptItem', namespace=NAMESPACE) tentatively_accept_item = EWSElementField(field_uri='TentativelyAcceptItem', value_cls='TentativelyAcceptItem', namespace=NAMESPACE) decline_item = EWSElementField(field_uri='DeclineItem', value_cls='DeclineItem', namespace=NAMESPACE) reply_to_item = EWSElementField(field_uri='ReplyToItem', value_cls='ReplyToItem', namespace=NAMESPACE) forward_item = EWSElementField(field_uri='ForwardItem', value_cls='ForwardItem', namespace=NAMESPACE) reply_all_to_item = EWSElementField(field_uri='ReplyAllToItem', value_cls='ReplyAllToItem', namespace=NAMESPACE) cancel_calendar_item = EWSElementField(field_uri='CancelCalendarItem', value_cls='CancelCalendarItem', namespace=NAMESPACE) remove_item = EWSElementField(field_uri='RemoveItem', value_cls=RemoveItem) post_reply_item = EWSElementField(field_uri='PostReplyItem', value_cls='PostReplyItem', namespace=EWSElement.NAMESPACE) success_read_receipt = EWSElementField(field_uri='SuppressReadReceipt', value_cls=SuppressReadReceipt) accept_sharing_invitation = EWSElementField(field_uri='AcceptSharingInvitation', value_cls=AcceptSharingInvitation) class PhoneNumber(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/phonenumber""" ELEMENT_NAME = 'PhoneNumber' number = CharField(field_uri='Number') type = CharField(field_uri='Type') class IdChangeKeyMixIn(EWSElement, metaclass=EWSMeta): """Base class for classes that have a concept of 'id' and 'changekey' values. The values are actually stored on a separate element but we add convenience methods to hide that fact. """ ID_ELEMENT_CLS = None def __init__(self, **kwargs): _id = self.ID_ELEMENT_CLS(kwargs.pop('id', None), kwargs.pop('changekey', None)) if _id.id or _id.changekey: kwargs['_id'] = _id super().__init__(**kwargs) @classmethod def get_field_by_fieldname(cls, fieldname): if fieldname in ('id', 'changekey'): return cls.ID_ELEMENT_CLS.get_field_by_fieldname(fieldname=fieldname) return super().get_field_by_fieldname(fieldname=fieldname) @property def id(self): if self._id is None: return None return self._id.id @id.setter def id(self, value): if self._id is None: self._id = self.ID_ELEMENT_CLS() self._id.id = value @property def changekey(self): if self._id is None: return None return self._id.changekey @changekey.setter def changekey(self, value): if self._id is None: self._id = self.ID_ELEMENT_CLS() self._id.changekey = value @classmethod def id_from_xml(cls, elem): # This method must be reasonably fast id_elem = elem.find(cls.ID_ELEMENT_CLS.response_tag()) if id_elem is None: return None, None return id_elem.get(cls.ID_ELEMENT_CLS.ID_ATTR), id_elem.get(cls.ID_ELEMENT_CLS.CHANGEKEY_ATTR) def to_id_xml(self, version): return self._id.to_xml(version=version) def __eq__(self, other): if isinstance(other, tuple): return hash((self.id, self.changekey)) == hash(other) return super().__eq__(other) def __hash__(self): # If we have an ID and changekey, use that as key. Else return a hash of all attributes if self.id: return hash((self.id, self.changekey)) return super().__hash__() class DictionaryEntry(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/dictionaryentry""" ELEMENT_NAME = 'DictionaryEntry' key = TypeValueField(field_uri='DictionaryKey') value = TypeValueField(field_uri='DictionaryValue') class UserConfigurationName(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/userconfigurationname""" ELEMENT_NAME = 'UserConfigurationName' NAMESPACE = TNS name = CharField(field_uri='Name', is_attribute=True) folder = EWSElementField(value_cls=FolderId) def clean(self, version=None): from .folders import BaseFolder if isinstance(self.folder, BaseFolder): self.folder = self.folder.to_folder_id() super().clean(version=version) @classmethod def from_xml(cls, elem, account): # We also accept distinguished folders f = EWSElementField(value_cls=DistinguishedFolderId) distinguished_folder_id = f.from_xml(elem=elem, account=account) res = super().from_xml(elem=elem, account=account) if distinguished_folder_id: res.folder = distinguished_folder_id return res class UserConfigurationNameMNS(UserConfigurationName): """Like UserConfigurationName, but in the MNS namespace. MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/userconfigurationname """ NAMESPACE = MNS class UserConfiguration(IdChangeKeyMixIn): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/userconfiguration""" ELEMENT_NAME = 'UserConfiguration' NAMESPACE = MNS ID_ELEMENT_CLS = ItemId _id = IdElementField(field_uri='ItemId', value_cls=ID_ELEMENT_CLS) user_configuration_name = EWSElementField(value_cls=UserConfigurationName) dictionary = DictionaryField(field_uri='Dictionary') xml_data = Base64Field(field_uri='XmlData') binary_data = Base64Field(field_uri='BinaryData') class Attribution(IdChangeKeyMixIn): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/phonenumber""" ELEMENT_NAME = 'Attribution' ID_ELEMENT_CLS = SourceId ID = CharField(field_uri='Id') _id = IdElementField(field_uri='SourceId', value_cls=ID_ELEMENT_CLS) display_name = CharField(field_uri='DisplayName') is_writable = BooleanField(field_uri='IsWritable') is_quick_contact = BooleanField(field_uri='IsQuickContact') is_hidden = BooleanField(field_uri='IsHidden') folder_id = EWSElementField(value_cls=FolderId) class BodyContentValue(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/value-bodycontenttype """ ELEMENT_NAME = 'Value' value = CharField(field_uri='Value') body_type = CharField(field_uri='BodyType') class BodyContentAttributedValue(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/bodycontentattributedvalue """ ELEMENT_NAME = 'BodyContentAttributedValue' value = EWSElementField(value_cls=BodyContentValue) attributions = EWSElementListField(field_uri='Attributions', value_cls=Attribution) class StringAttributedValue(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/stringattributedvalue """ ELEMENT_NAME = 'StringAttributedValue' value = CharField(field_uri='Value') attributions = CharListField(field_uri='Attributions', list_elem_name='Attribution') class PersonaPhoneNumberTypeValue(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/value-personaphonenumbertype """ ELEMENT_NAME = 'Value' number = CharField(field_uri='Number') type = CharField(field_uri='Type') class PhoneNumberAttributedValue(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/phonenumberattributedvalue """ ELEMENT_NAME = 'PhoneNumberAttributedValue' value = EWSElementField(value_cls=PersonaPhoneNumberTypeValue) attributions = CharListField(field_uri='Attributions', list_elem_name='Attribution') class EmailAddressTypeValue(Mailbox): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/value-emailaddresstype """ ELEMENT_NAME = 'Value' original_display_name = TextField(field_uri='OriginalDisplayName') class EmailAddressAttributedValue(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/emailaddressattributedvalue """ ELEMENT_NAME = 'EmailAddressAttributedValue' value = EWSElementField(value_cls=EmailAddressTypeValue) attributions = EWSElementListField(field_uri='Attributions', value_cls=Attribution) class PersonaPostalAddressTypeValue(Mailbox): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/value-personapostaladdresstype """ ELEMENT_NAME = 'Value' street = TextField(field_uri='Street') city = TextField(field_uri='City') state = TextField(field_uri='State') country = TextField(field_uri='Country') postal_code = TextField(field_uri='PostalCode') post_office_box = TextField(field_uri='PostOfficeBox') type = TextField(field_uri='Type') latitude = TextField(field_uri='Latitude') longitude = TextField(field_uri='Longitude') accuracy = TextField(field_uri='Accuracy') altitude = TextField(field_uri='Altitude') altitude_accuracy = TextField(field_uri='AltitudeAccuracy') formatted_address = TextField(field_uri='FormattedAddress') location_uri = TextField(field_uri='LocationUri') location_source = TextField(field_uri='LocationSource') class PostalAddressAttributedValue(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postaladdressattributedvalue """ ELEMENT_NAME = 'PostalAddressAttributedValue' value = EWSElementField(value_cls=PersonaPostalAddressTypeValue) attributions = EWSElementListField(field_uri='Attributions', value_cls=Attribution) class Event(EWSElement, metaclass=EWSMeta): """Base class for all event types.""" watermark = CharField(field_uri='Watermark') class TimestampEvent(Event, metaclass=EWSMeta): """Base class for both item and folder events with a timestamp.""" FOLDER = 'folder' ITEM = 'item' timestamp = DateTimeField(field_uri='TimeStamp') item_id = EWSElementField(field_uri='ItemId', value_cls=ItemId) folder_id = EWSElementField(field_uri='FolderId', value_cls=FolderId) parent_folder_id = EWSElementField(field_uri='ParentFolderId', value_cls=ParentFolderId) @property def event_type(self): if self.item_id is not None: return self.ITEM if self.folder_id is not None: return self.FOLDER return None # Empty object class OldTimestampEvent(TimestampEvent, metaclass=EWSMeta): """Base class for both item and folder copy/move events.""" old_item_id = EWSElementField(field_uri='OldItemId', value_cls=ItemId) old_folder_id = EWSElementField(field_uri='OldFolderId', value_cls=FolderId) old_parent_folder_id = EWSElementField(field_uri='OldParentFolderId', value_cls=ParentFolderId) class CopiedEvent(OldTimestampEvent): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/copiedevent""" ELEMENT_NAME = 'CopiedEvent' class CreatedEvent(TimestampEvent): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createdevent""" ELEMENT_NAME = 'CreatedEvent' class DeletedEvent(TimestampEvent): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deletedevent""" ELEMENT_NAME = 'DeletedEvent' class ModifiedEvent(TimestampEvent): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/modifiedevent""" ELEMENT_NAME = 'ModifiedEvent' unread_count = IntegerField(field_uri='UnreadCount') class MovedEvent(OldTimestampEvent): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/movedevent""" ELEMENT_NAME = 'MovedEvent' class NewMailEvent(TimestampEvent): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/newmailevent""" ELEMENT_NAME = 'NewMailEvent' class StatusEvent(Event): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/statusevent""" ELEMENT_NAME = 'StatusEvent' class FreeBusyChangedEvent(TimestampEvent): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/freebusychangedevent""" ELEMENT_NAME = 'FreeBusyChangedEvent' class Notification(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/notification-ex15websvcsotherref """ ELEMENT_NAME = 'Notification' NAMESPACE = MNS subscription_id = CharField(field_uri='SubscriptionId') previous_watermark = CharField(field_uri='PreviousWatermark') more_events = BooleanField(field_uri='MoreEvents') events = GenericEventListField('') exchangelib-4.6.1/exchangelib/protocol.py000066400000000000000000001077601414601472700205000ustar00rootroot00000000000000""" A protocol is an endpoint for EWS service connections. It contains all necessary information to make HTTPS connections. Protocols should be accessed through an Account, and are either created from a default Configuration or autodiscovered when creating an Account. """ import abc import datetime import logging import os from queue import LifoQueue, Empty, Full from threading import Lock import requests.adapters import requests.sessions import requests.utils from oauthlib.oauth2 import BackendApplicationClient, WebApplicationClient from requests_oauthlib import OAuth2Session from .credentials import OAuth2AuthorizationCodeCredentials, OAuth2Credentials from .errors import TransportError, SessionPoolMinSizeReached, SessionPoolMaxSizeReached, RateLimitError, CASError, \ ErrorInvalidSchemaVersionForMailboxVersion, UnauthorizedError, MalformedResponseError from .properties import FreeBusyViewOptions, MailboxData, TimeWindow, TimeZone, RoomList, DLMailbox from .services import GetServerTimeZones, GetRoomLists, GetRooms, ResolveNames, GetUserAvailability, \ GetSearchableMailboxes, ExpandDL, ConvertId from .transport import get_auth_instance, get_service_authtype, NTLM, OAUTH2, CREDENTIALS_REQUIRED, DEFAULT_HEADERS from .version import Version, API_VERSIONS log = logging.getLogger(__name__) def close_connections(): CachingProtocol.clear_cache() class BaseProtocol: """Base class for Protocol which implements the bare essentials.""" # The maximum number of sessions (== TCP connections, see below) we will open to this service endpoint. Keep this # low unless you have an agreement with the Exchange admin on the receiving end to hammer the server and # rate-limiting policies have been disabled for the connecting user. Changing this setting only makes sense if # you are using a thread pool to run multiple concurrent workers in this process. SESSION_POOLSIZE = 1 # We want only 1 TCP connection per Session object. We may have lots of different credentials hitting the server and # each credential needs its own session (NTLM auth will only send credentials once and then secure the connection, # so a connection can only handle requests for one credential). Having multiple connections per Session could # quickly exhaust the maximum number of concurrent connections the Exchange server allows from one client. CONNECTIONS_PER_SESSION = 1 # The number of times a session may be reused before creating a new session object. 'None' means "infinite". # Discarding sessions after a certain number of usages may limit memory leaks in the Session object. MAX_SESSION_USAGE_COUNT = None # Timeout for HTTP requests TIMEOUT = 120 # The adapter class to use for HTTP requests. Override this if you need e.g. proxy support or specific TLS versions HTTP_ADAPTER_CLS = requests.adapters.HTTPAdapter # The User-Agent header to use for HTTP requests. Override this to set an app-specific one USERAGENT = None def __init__(self, config): from .configuration import Configuration if not isinstance(config, Configuration): raise ValueError("'config' %r must be a Configuration instance" % config) if not config.service_endpoint: raise AttributeError("'config.service_endpoint' must be set") self.config = config self._session_pool_size = 0 self._session_pool_maxsize = config.max_connections or self.SESSION_POOLSIZE # Try to behave nicely with the remote server. We want to keep the connection open between requests. # We also want to re-use sessions, to avoid the NTLM auth handshake on every request. We must know the # authentication method to create sessions. self._session_pool = LifoQueue() self._session_pool_lock = Lock() @property def service_endpoint(self): return self.config.service_endpoint @property def auth_type(self): return self.config.auth_type @property def credentials(self): return self.config.credentials @credentials.setter def credentials(self, value): # We are updating credentials, but that doesn't automatically propagate to the session objects. The simplest # solution is to just kill the sessions in the pool. with self._session_pool_lock: self.config._credentials = value self.close() @property def retry_policy(self): return self.config.retry_policy @property def server(self): return self.config.server def __getstate__(self): # The session pool and lock cannot be pickled state = self.__dict__.copy() del state['_session_pool'] del state['_session_pool_lock'] return state def __setstate__(self, state): # Restore the session pool and lock self.__dict__.update(state) self._session_pool = LifoQueue() self._session_pool_lock = Lock() def __del__(self): # pylint: disable=bare-except try: self.close() except Exception: # nosec # __del__ should never fail pass def close(self): log.debug('Server %s: Closing sessions', self.server) while True: try: session = self._session_pool.get(block=False) self.close_session(session) self._session_pool_size -= 1 except Empty: break @classmethod def get_adapter(cls): # We want just one connection per session. No retries, since we wrap all requests in our own retry handler return cls.HTTP_ADAPTER_CLS( pool_block=True, pool_connections=cls.CONNECTIONS_PER_SESSION, pool_maxsize=cls.CONNECTIONS_PER_SESSION, max_retries=0, ) @property def session_pool_size(self): return self._session_pool_size def increase_poolsize(self): """Increases the session pool size. We increase by one session per call.""" # Create a single session and insert it into the pool. We need to protect this with a lock while we are changing # the pool size variable, to avoid race conditions. We must not exceed the pool size limit. if self._session_pool_size == self._session_pool_maxsize: raise SessionPoolMaxSizeReached('Session pool size cannot be increased further') with self._session_pool_lock: if self._session_pool_size >= self._session_pool_maxsize: log.debug('Session pool size was increased in another thread') return log.debug('Server %s: Increasing session pool size from %s to %s', self.server, self._session_pool_size, self._session_pool_size + 1) self._session_pool.put(self.create_session(), block=False) self._session_pool_size += 1 def decrease_poolsize(self): """Decreases the session pool size in response to error messages from the server requesting to rate-limit requests. We decrease by one session per call. """ # Take a single session from the pool and discard it. We need to protect this with a lock while we are changing # the pool size variable, to avoid race conditions. We must keep at least one session in the pool. if self._session_pool_size <= 1: raise SessionPoolMinSizeReached('Session pool size cannot be decreased further') with self._session_pool_lock: if self._session_pool_size <= 1: log.debug('Session pool size was decreased in another thread') return log.warning('Server %s: Decreasing session pool size from %s to %s', self.server, self._session_pool_size, self._session_pool_size - 1) session = self.get_session() self.close_session(session) self._session_pool_size -= 1 def get_session(self): # Try to get a session from the queue. If the queue is empty, try to add one more session to the queue. If the # queue is already at its max, wait until a session becomes available. _timeout = 60 # Rate-limit messages about session starvation try: session = self._session_pool.get(block=False) log.debug('Server %s: Got session immediately', self.server) except Empty: try: self.increase_poolsize() except SessionPoolMaxSizeReached: pass while True: try: log.debug('Server %s: Waiting for session', self.server) session = self._session_pool.get(timeout=_timeout) break except Empty: # This is normal when we have many worker threads starving for available sessions log.debug('Server %s: No sessions available for %s seconds', self.server, _timeout) log.debug('Server %s: Got session %s', self.server, session.session_id) session.usage_count += 1 return session def release_session(self, session): # This should never fail, as we don't have more sessions than the queue contains log.debug('Server %s: Releasing session %s', self.server, session.session_id) if self.MAX_SESSION_USAGE_COUNT and session.usage_count > self.MAX_SESSION_USAGE_COUNT: log.debug('Server %s: session %s usage exceeded limit. Discarding', self.server, session.session_id) session = self.renew_session(session) try: self._session_pool.put(session, block=False) except Full: log.debug('Server %s: Session pool was already full %s', self.server, session.session_id) @staticmethod def close_session(session): session.close() del session def retire_session(self, session): # The session is useless. Close it completely and place a fresh session in the pool log.debug('Server %s: Retiring session %s', self.server, session.session_id) self.close_session(session) self.release_session(self.create_session()) def renew_session(self, session): # The session is useless. Close it completely and place a fresh session in the pool log.debug('Server %s: Renewing session %s', self.server, session.session_id) self.close_session(session) return self.create_session() def refresh_credentials(self, session): # Credentials need to be refreshed, probably due to an OAuth # access token expiring. If we've gotten here, it's because the # application didn't provide an OAuth client secret, so we can't # handle token refreshing for it. with self.credentials.lock: if self.credentials.sig() == session.credentials_sig: # Credentials have not been refreshed by another thread: # they're the same as the session was created with. If # this isn't the case, we can just go ahead with a new # session using the already-updated credentials. self.credentials.refresh(session=session) return self.renew_session(session) def create_session(self): if self.auth_type is None: raise ValueError('Cannot create session without knowing the auth type') if self.credentials is None: if self.auth_type in CREDENTIALS_REQUIRED: raise ValueError('Auth type %r requires credentials' % self.auth_type) session = self.raw_session(self.service_endpoint) session.auth = get_auth_instance(auth_type=self.auth_type) else: with self.credentials.lock: if isinstance(self.credentials, OAuth2Credentials): session = self.create_oauth2_session() # Keep track of the credentials used to create this session. If # and when we need to renew credentials (for example, refreshing # an OAuth access token), this lets us easily determine whether # the credentials have already been refreshed in another thread # by the time this session tries. session.credentials_sig = self.credentials.sig() else: if self.auth_type == NTLM and self.credentials.type == self.credentials.EMAIL: username = '\\' + self.credentials.username else: username = self.credentials.username session = self.raw_session(self.service_endpoint) session.auth = get_auth_instance(auth_type=self.auth_type, username=username, password=self.credentials.password) # Add some extra info session.session_id = sum(map(ord, str(os.urandom(100)))) # Used for debugging messages in services session.usage_count = 0 session.protocol = self log.debug('Server %s: Created session %s', self.server, session.session_id) return session def create_oauth2_session(self): if self.auth_type != OAUTH2: raise ValueError( 'Auth type must be %r for credentials type %s' % (OAUTH2, self.credentials.__class__.__name__) ) has_token = False scope = ['https://outlook.office365.com/.default'] session_params = {} token_params = {} if isinstance(self.credentials, OAuth2AuthorizationCodeCredentials): # Ask for a refresh token scope.append('offline_access') # We don't know (or need) the Microsoft tenant ID. Use # common/ to let Microsoft select the appropriate tenant # for the provided authorization code or refresh token. # # Suppress looks-like-password warning from Bandit. token_url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token' # nosec client_params = {} has_token = self.credentials.access_token is not None if has_token: session_params['token'] = self.credentials.access_token elif self.credentials.authorization_code is not None: token_params['code'] = self.credentials.authorization_code self.credentials.authorization_code = None if self.credentials.client_id is not None and self.credentials.client_secret is not None: # If we're given a client ID and secret, we have enough # to refresh access tokens ourselves. In other cases the # session will raise TokenExpiredError and we'll need to # ask the calling application to refresh the token (that # covers cases where the caller doesn't have access to # the client secret but is working with a service that # can provide it refreshed tokens on a limited basis). session_params.update({ 'auto_refresh_kwargs': { 'client_id': self.credentials.client_id, 'client_secret': self.credentials.client_secret, }, 'auto_refresh_url': token_url, 'token_updater': self.credentials.on_token_auto_refreshed, }) client = WebApplicationClient(self.credentials.client_id, **client_params) else: token_url = 'https://login.microsoftonline.com/%s/oauth2/v2.0/token' % self.credentials.tenant_id client = BackendApplicationClient(client_id=self.credentials.client_id) session = self.raw_session(self.service_endpoint, oauth2_client=client, oauth2_session_params=session_params) if not has_token: # Fetch the token explicitly -- it doesn't occur implicitly token = session.fetch_token(token_url=token_url, client_id=self.credentials.client_id, client_secret=self.credentials.client_secret, scope=scope, timeout=self.TIMEOUT, **token_params) # Allow the credentials object to update its copy of the new # token, and give the application an opportunity to cache it self.credentials.on_token_auto_refreshed(token) session.auth = get_auth_instance(auth_type=OAUTH2, client=client) return session @classmethod def raw_session(cls, prefix, oauth2_client=None, oauth2_session_params=None): if oauth2_client: session = OAuth2Session(client=oauth2_client, **(oauth2_session_params or {})) else: session = requests.sessions.Session() session.headers.update(DEFAULT_HEADERS) session.headers['User-Agent'] = cls.USERAGENT session.mount(prefix, adapter=cls.get_adapter()) return session def __repr__(self): return self.__class__.__name__ + repr((self.service_endpoint, self.credentials, self.auth_type)) class CachingProtocol(type): """A metaclass for Protocol that caches Protocol instances based on a server+username key.""" _protocol_cache = {} _protocol_cache_lock = Lock() def __call__(cls, *args, **kwargs): # Cache Protocol instances that point to the same endpoint and use the same credentials. This ensures that we # re-use thread and connection pools etc. instead of flooding the remote server. This is a modified Singleton # pattern. # # We ignore auth_type from kwargs in the cache key. We trust caller to supply the correct auth_type - otherwise # __init__ will guess the correct auth type. config = kwargs['config'] _protocol_cache_key = cls._cache_key(config) try: protocol, _ = cls._protocol_cache[_protocol_cache_key] except KeyError: pass else: if isinstance(protocol, Exception): # The input data leads to a TransportError. Re-throw raise protocol return protocol # Acquire lock to guard against multiple threads competing to cache information. Having a per-server lock is # probably overkill although it would reduce lock contention. log.debug('Waiting for _protocol_cache_lock') with cls._protocol_cache_lock: try: protocol, _ = cls._protocol_cache[_protocol_cache_key] except KeyError: pass else: if isinstance(protocol, Exception): # We already tried this combination, possibly in a different competing thread, but the input # data leads to a TransportError. raise protocol return protocol log.debug("Protocol __call__ cache miss. Adding key '%s'", str(_protocol_cache_key)) try: protocol = super().__call__(*args, **kwargs) except TransportError as e: # This can happen if, for example, autodiscover supplies us with a bogus EWS endpoint log.warning('Failed to create cached protocol with key %s: %s', _protocol_cache_key, e) cls._protocol_cache[_protocol_cache_key] = e, datetime.datetime.now() raise e cls._protocol_cache[_protocol_cache_key] = protocol, datetime.datetime.now() return protocol @staticmethod def _cache_key(config): # We may be using multiple different credentials for the same service endpoint. This key combination should be # safe. return config.service_endpoint, config.credentials def __getitem__(cls, config): return cls._protocol_cache[cls._cache_key(config)] def __delitem__(cls, config): del cls._protocol_cache[cls._cache_key(config)] @classmethod def clear_cache(mcs): with mcs._protocol_cache_lock: for key, (protocol, _) in mcs._protocol_cache.items(): if isinstance(protocol, Exception): continue service_endpoint = key[0] log.debug("Service endpoint '%s': Closing sessions", service_endpoint) with protocol._session_pool_lock: protocol.close() mcs._protocol_cache.clear() class Protocol(BaseProtocol, metaclass=CachingProtocol): """A class to handle all the low-level communication with an Exchange server. Contains a session pool, knows how to negotiate the authentication type of the server, refresh credentials, etc. Also contains methods for calling EWS services that are not tied to an account. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._api_version_hint = None self._version_lock = Lock() # Autodetect authentication type if necessary if self.config.auth_type is None: self.config.auth_type = self.get_auth_type() def get_auth_type(self): # Autodetect authentication type. We also set version hint here. name = str(self.credentials) if self.credentials and str(self.credentials) else 'DUMMY' auth_type, api_version_hint = get_service_authtype( service_endpoint=self.service_endpoint, retry_policy=self.retry_policy, api_versions=API_VERSIONS, name=name ) self._api_version_hint = api_version_hint return auth_type @property def version(self): # Make sure only one thread does the guessing. if not self.config.version or not self.config.version.build: with self._version_lock: if not self.config.version or not self.config.version.build: # Version.guess() needs auth objects and a working session pool self.config.version = Version.guess(self, api_version_hint=self._api_version_hint) return self.config.version def get_timezones(self, timezones=None, return_full_timezone_data=False): """Get timezone definitions from the server. :param timezones: A list of EWSDateTime instances. If None, fetches all timezones from server (Default value = None) :param return_full_timezone_data: If true, also returns periods and transitions (Default value = False) :return: A list of (tz_id, name, periods, transitions) tuples """ return GetServerTimeZones(protocol=self).call( timezones=timezones, return_full_timezone_data=return_full_timezone_data ) def get_free_busy_info(self, accounts, start, end, merged_free_busy_interval=30, requested_view='DetailedMerged'): """Return free/busy information for a list of accounts. :param accounts: A list of (account, attendee_type, exclude_conflicts) tuples, where account is either an Account object or a string, attendee_type is a MailboxData.attendee_type choice, and exclude_conflicts is a boolean. :param start: The start datetime of the request :param end: The end datetime of the request :param merged_free_busy_interval: The interval, in minutes, of merged free/busy information (Default value = 30) :param requested_view: The type of information returned. Possible values are defined in the FreeBusyViewOptions.requested_view choices. (Default value = 'DetailedMerged') :return: A generator of FreeBusyView objects """ from .account import Account for account, attendee_type, exclude_conflicts in accounts: if not isinstance(account, (Account, str)): raise ValueError("'accounts' item %r must be an 'Account' or 'str' instance" % account) if attendee_type not in MailboxData.ATTENDEE_TYPES: raise ValueError("'accounts' item %r must be one of %s" % (attendee_type, MailboxData.ATTENDEE_TYPES)) if not isinstance(exclude_conflicts, bool): raise ValueError("'accounts' item %r must be a 'bool' instance" % exclude_conflicts) if start >= end: raise ValueError("'start' must be less than 'end' (%s -> %s)" % (start, end)) if not isinstance(merged_free_busy_interval, int): raise ValueError("'merged_free_busy_interval' value %r must be an 'int'" % merged_free_busy_interval) if requested_view not in FreeBusyViewOptions.REQUESTED_VIEWS: raise ValueError( "'requested_view' value %r must be one of %s" % (requested_view, FreeBusyViewOptions.REQUESTED_VIEWS)) _, _, periods, transitions, transitions_groups = list(self.get_timezones( timezones=[start.tzinfo], return_full_timezone_data=True ))[0] return GetUserAvailability(self).call( timezone=TimeZone.from_server_timezone( periods=periods, transitions=transitions, transitionsgroups=transitions_groups, for_year=start.year ), mailbox_data=[MailboxData( email=account.primary_smtp_address if isinstance(account, Account) else account, attendee_type=attendee_type, exclude_conflicts=exclude_conflicts ) for account, attendee_type, exclude_conflicts in accounts], free_busy_view_options=FreeBusyViewOptions( time_window=TimeWindow(start=start, end=end), merged_free_busy_interval=merged_free_busy_interval, requested_view=requested_view, ), ) def get_roomlists(self): return GetRoomLists(protocol=self).call() def get_rooms(self, roomlist): return GetRooms(protocol=self).call(roomlist=RoomList(email_address=roomlist)) def resolve_names(self, names, return_full_contact_data=False, search_scope=None, shape=None): """Resolve accounts on the server using partial account data, e.g. an email address or initials. :param names: A list of identifiers to query :param return_full_contact_data: If True, returns full contact data (Default value = False) :param search_scope: The scope to perform the search. Must be one of SEARCH_SCOPE_CHOICES (Default value = None) :param shape: (Default value = None) :return: A list of Mailbox items or, if return_full_contact_data is True, tuples of (Mailbox, Contact) items """ return list(ResolveNames(protocol=self).call( unresolved_entries=names, return_full_contact_data=return_full_contact_data, search_scope=search_scope, contact_data_shape=shape, )) def expand_dl(self, distribution_list): """Expand distribution list into it's members. :param distribution_list: SMTP address of the distribution list to expand, or a DLMailbox representing the list :return: List of Mailbox items that are members of the distribution list """ if isinstance(distribution_list, str): distribution_list = DLMailbox(email_address=distribution_list, mailbox_type='PublicDL') return list(ExpandDL(protocol=self).call(distribution_list=distribution_list)) def get_searchable_mailboxes(self, search_filter=None, expand_group_membership=False): """Call the GetSearchableMailboxes service to get mailboxes that can be searched. This method is only available to users who have been assigned the Discovery Management RBAC role. See https://docs.microsoft.com/en-us/exchange/permissions-exo/permissions-exo :param search_filter: Is set, must be a single email alias (Default value = None) :param expand_group_membership: If True, returned distribution lists are expanded (Default value = False) :return: a list of SearchableMailbox, FailedMailbox or Exception instances """ return list(GetSearchableMailboxes(protocol=self).call( search_filter=search_filter, expand_group_membership=expand_group_membership, )) def convert_ids(self, ids, destination_format): """Convert item and folder IDs between multiple formats. :param ids: a list of AlternateId, AlternatePublicFolderId or AlternatePublicFolderItemId instances :param destination_format: A string :return: a generator of AlternateId, AlternatePublicFolderId or AlternatePublicFolderItemId instances """ return ConvertId(protocol=self).call(items=ids, destination_format=destination_format) def __getstate__(self): # The lock and thread pool cannot be pickled state = super().__getstate__() del state['_version_lock'] try: del state['thread_pool'] except KeyError: # thread_pool is a cached property and may not exist pass return state def __setstate__(self, state): # Restore the lock. The thread pool is a cached property and will be recreated automatically. self.__dict__.update(state) self._version_lock = Lock() def __str__(self): # Don't trigger version guessing here just for the sake of printing if self.config.version: fullname, api_version, build = self.version.fullname, self.version.api_version, self.version.build else: fullname, api_version, build = '[unknown]', '[unknown]', '[unknown]' return '''\ EWS url: %s Product name: %s EWS API version: %s Build number: %s EWS auth: %s''' % (self.service_endpoint, fullname, api_version, build, self.auth_type) class NoVerifyHTTPAdapter(requests.adapters.HTTPAdapter): """An HTTP adapter that ignores TLS validation errors. Use at own risk.""" def cert_verify(self, conn, url, verify, cert): # pylint: disable=unused-argument # We're overriding a method so we have to keep the signature super().cert_verify(conn=conn, url=url, verify=False, cert=cert) class TLSClientAuth(requests.adapters.HTTPAdapter): """An HTTP adapter that implements Certificate Based Authentication (CBA).""" cert_file = None def init_poolmanager(self, *args, **kwargs): kwargs['cert_file'] = self.cert_file return super().init_poolmanager(*args, **kwargs) class RetryPolicy(metaclass=abc.ABCMeta): """Stores retry logic used when faced with errors from the server.""" @property @abc.abstractmethod def fail_fast(self): # Used to choose the error handling policy. When True, a fault-tolerant policy is used. False, a fail-fast # policy is used. pass @property @abc.abstractmethod def back_off_until(self): pass @back_off_until.setter @abc.abstractmethod def back_off_until(self, value): pass @abc.abstractmethod def back_off(self, seconds): pass @abc.abstractmethod def may_retry_on_error(self, response, wait): pass def raise_response_errors(self, response): cas_error = response.headers.get('X-CasErrorCode') if cas_error: if cas_error.startswith('CAS error:'): # Remove unnecessary text cas_error = cas_error.split(':', 1)[1].strip() raise CASError(cas_error=cas_error, response=response) if response.status_code == 500 and (b'The specified server version is invalid' in response.content or b'ErrorInvalidSchemaVersionForMailboxVersion' in response.content): # Another way of communicating invalid schema versions raise ErrorInvalidSchemaVersionForMailboxVersion('Invalid server version') if b'The referenced account is currently locked out' in response.content: raise UnauthorizedError('The referenced account is currently locked out') if response.status_code == 401 and self.fail_fast: # This is a login failure raise UnauthorizedError('Invalid credentials for %s' % response.url) if 'TimeoutException' in response.headers: # A header set by us on CONNECTION_ERRORS raise response.headers['TimeoutException'] # This could be anything. Let higher layers handle this raise MalformedResponseError( 'Unknown failure in response. Code: %s headers: %s content: %s' % (response.status_code, response.headers, response.text) ) class FailFast(RetryPolicy): """Fail immediately on server errors.""" @property def fail_fast(self): return True @property def back_off_until(self): return None def back_off(self, seconds): raise ValueError('Cannot back off with fail-fast policy') def may_retry_on_error(self, response, wait): log.debug('No retry: no fail-fast policy') return False class FaultTolerance(RetryPolicy): """Enables fault-tolerant error handling. Tells internal methods to do an exponential back off when requests start failing, and wait up to max_wait seconds before failing. """ # Back off 60 seconds if we didn't get an explicit suggested value DEFAULT_BACKOFF = 60 def __init__(self, max_wait=3600): self.max_wait = max_wait self._back_off_until = None self._back_off_lock = Lock() def __getstate__(self): # Locks cannot be pickled state = self.__dict__.copy() del state['_back_off_lock'] return state def __setstate__(self, state): # Restore the lock self.__dict__.update(state) self._back_off_lock = Lock() @property def fail_fast(self): return False @property def back_off_until(self): """Return the back off value as a datetime. Reset the current back off value if it has expired.""" if self._back_off_until is None: return None with self._back_off_lock: if self._back_off_until is None: return None if self._back_off_until < datetime.datetime.now(): self._back_off_until = None # The back off value has expired. Reset return None return self._back_off_until @back_off_until.setter def back_off_until(self, value): with self._back_off_lock: self._back_off_until = value def back_off(self, seconds): if seconds is None: seconds = self.DEFAULT_BACKOFF value = datetime.datetime.now() + datetime.timedelta(seconds=seconds) with self._back_off_lock: self._back_off_until = value def may_retry_on_error(self, response, wait): if response.status_code not in (301, 302, 401, 500, 503): # Don't retry if we didn't get a status code that we can hope to recover from log.debug('No retry: wrong status code %s', response.status_code) return False if wait > self.max_wait: # We lost patience. Session is cleaned up in outer loop raise RateLimitError( 'Max timeout reached', url=response.url, status_code=response.status_code, total_wait=wait) if response.status_code == 401: # EWS sometimes throws 401's when it wants us to throttle connections. OK to retry. return True if response.headers.get('connection') == 'close': # Connection closed. OK to retry. return True if response.status_code == 302 and response.headers.get('location', '').lower() \ == '/ews/genericerrorpage.htm?aspxerrorpath=/ews/exchange.asmx': # The genericerrorpage.htm/internalerror.asp is ridiculous behaviour for random outages. OK to retry. # # Redirect to '/internalsite/internalerror.asp' or '/internalsite/initparams.aspx' is caused by e.g. TLS # certificate f*ckups on the Exchange server. We should not retry those. return True if response.status_code == 503: # Internal server error. OK to retry. return True if response.status_code == 500 and b"Server Error in '/EWS' Application" in response.content: # "Server Error in '/EWS' Application" has been seen in highly concurrent settings. OK to retry. log.debug('Retry allowed: conditions met') return True return False exchangelib-4.6.1/exchangelib/queryset.py000066400000000000000000000706301414601472700205130ustar00rootroot00000000000000import abc import logging import warnings from copy import deepcopy from itertools import islice from .errors import MultipleObjectsReturned, DoesNotExist from .fields import FieldPath, FieldOrder from .items import CalendarItem, ID_ONLY from .properties import InvalidField from .restriction import Q from .services import CHUNK_SIZE from .version import EXCHANGE_2010 log = logging.getLogger(__name__) class SearchableMixIn: """Implement a search API for inheritance.""" @abc.abstractmethod def get(self, *args, **kwargs): pass @abc.abstractmethod def all(self): pass @abc.abstractmethod def none(self): pass @abc.abstractmethod def filter(self, *args, **kwargs): pass @abc.abstractmethod def exclude(self, *args, **kwargs): pass @abc.abstractmethod def people(self): pass class QuerySet(SearchableMixIn): """A Django QuerySet-like class for querying items. Defers queries until the QuerySet is consumed. Supports chaining to build up complex queries. Django QuerySet documentation: https://docs.djangoproject.com/en/dev/ref/models/querysets/ """ VALUES = 'values' VALUES_LIST = 'values_list' FLAT = 'flat' NONE = 'none' RETURN_TYPES = (VALUES, VALUES_LIST, FLAT, NONE) ITEM = 'item' PERSONA = 'persona' REQUEST_TYPES = (ITEM, PERSONA) def __init__(self, folder_collection, request_type=ITEM): from .folders import FolderCollection if not isinstance(folder_collection, FolderCollection): raise ValueError("folder_collection value '%s' must be a FolderCollection instance" % folder_collection) self.folder_collection = folder_collection # A FolderCollection instance if request_type not in self.REQUEST_TYPES: raise ValueError("'request_type' %r must be one of %s" % (request_type, self.REQUEST_TYPES)) self.request_type = request_type self.q = Q() # Default to no restrictions self.only_fields = None self.order_fields = None self.return_format = self.NONE self.calendar_view = None self.page_size = None self.max_items = None self.offset = 0 self._depth = None def _copy_self(self): # When we copy a queryset where the cache has already been filled, we don't copy the cache. Thus, a copied # queryset will fetch results from the server again. # # All other behaviour would be awkward: # # qs = QuerySet(f).filter(foo='bar') # items = list(qs) # new_qs = qs.exclude(bar='baz') # This should work, and should fetch from the server # if not isinstance(self.q, Q): raise ValueError("self.q value '%s' must be None or a Q instance" % self.q) if not isinstance(self.only_fields, (type(None), tuple)): raise ValueError("self.only_fields value '%s' must be None or a tuple" % self.only_fields) if not isinstance(self.order_fields, (type(None), tuple)): raise ValueError("self.order_fields value '%s' must be None or a tuple" % self.order_fields) if self.return_format not in self.RETURN_TYPES: raise ValueError("self.return_value '%s' must be one of %s" % (self.return_format, self.RETURN_TYPES)) # Only mutable objects need to be deepcopied. Folder should be the same object new_qs = self.__class__(self.folder_collection, request_type=self.request_type) new_qs.q = deepcopy(self.q) new_qs.only_fields = self.only_fields new_qs.order_fields = None if self.order_fields is None else deepcopy(self.order_fields) new_qs.return_format = self.return_format new_qs.calendar_view = self.calendar_view new_qs.page_size = self.page_size new_qs.max_items = self.max_items new_qs.offset = self.offset new_qs._depth = self._depth return new_qs def _get_field_path(self, field_path): from .items import Persona if self.request_type == self.PERSONA: return FieldPath(field=Persona.get_field_by_fieldname(field_path)) for folder in self.folder_collection: try: return FieldPath.from_string(field_path=field_path, folder=folder) except InvalidField: pass raise InvalidField("Unknown field path %r on folders %s" % (field_path, self.folder_collection.folders)) def _get_field_order(self, field_path): from .items import Persona if self.request_type == self.PERSONA: return FieldOrder( field_path=FieldPath(field=Persona.get_field_by_fieldname(field_path.lstrip('-'))), reverse=field_path.startswith('-'), ) for folder in self.folder_collection: try: return FieldOrder.from_string(field_path=field_path, folder=folder) except InvalidField: pass raise InvalidField("Unknown field path %r on folders %s" % (field_path, self.folder_collection.folders)) @property def _id_field(self): return self._get_field_path('id') @property def _changekey_field(self): return self._get_field_path('changekey') def _additional_fields(self): if not isinstance(self.only_fields, tuple): raise ValueError("'only_fields' value %r must be a tuple" % self.only_fields) # Remove ItemId and ChangeKey. We get them unconditionally additional_fields = {f for f in self.only_fields if not f.field.is_attribute} if self.request_type != self.ITEM: return additional_fields # For CalendarItem items, we want to inject internal timezone fields into the requested fields. has_start = 'start' in {f.field.name for f in additional_fields} has_end = 'end' in {f.field.name for f in additional_fields} meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields() if self.folder_collection.account.version.build < EXCHANGE_2010: if has_start or has_end: additional_fields.add(FieldPath(field=meeting_tz_field)) else: if has_start: additional_fields.add(FieldPath(field=start_tz_field)) if has_end: additional_fields.add(FieldPath(field=end_tz_field)) return additional_fields def _format_items(self, items, return_format): return { self.VALUES: self._as_values, self.VALUES_LIST: self._as_values_list, self.FLAT: self._as_flat_values_list, self.NONE: self._as_items, }[return_format](items) def _query(self): if self.only_fields is None: # We didn't restrict list of field paths. Get all fields from the server, including extended properties. if self.request_type == self.PERSONA: additional_fields = {} # GetPersona doesn't take explicit fields. Don't bother calculating the list complex_fields_requested = True else: additional_fields = {FieldPath(field=f) for f in self.folder_collection.allowed_item_fields()} complex_fields_requested = True else: additional_fields = self._additional_fields() complex_fields_requested = any(f.field.is_complex for f in additional_fields) # EWS can do server-side sorting on multiple fields. A caveat is that server-side sorting is not supported # for calendar views. In this case, we do all the sorting client-side. if self.calendar_view: must_sort_clientside = bool(self.order_fields) order_fields = None else: must_sort_clientside = False order_fields = self.order_fields if must_sort_clientside: # Also fetch order_by fields that we only need for client-side sorting. extra_order_fields = {f.field_path for f in self.order_fields} - additional_fields if extra_order_fields: additional_fields.update(extra_order_fields) else: extra_order_fields = set() find_kwargs = dict( shape=ID_ONLY, # Always use IdOnly here, because AllProperties doesn't actually get *all* properties depth=self._depth, additional_fields=additional_fields, order_fields=order_fields, page_size=self.page_size, max_items=self.max_items, offset=self.offset, ) if self.request_type == self.PERSONA: if complex_fields_requested: find_kwargs['additional_fields'] = None items = self.folder_collection.account.fetch_personas( ids=self.folder_collection.find_people(self.q, **find_kwargs) ) else: if not additional_fields: find_kwargs['additional_fields'] = None items = self.folder_collection.find_people(self.q, **find_kwargs) else: find_kwargs['calendar_view'] = self.calendar_view if complex_fields_requested: # The FindItem service does not support complex field types. Tell find_items() to return # (id, changekey) tuples, and pass that to fetch(). find_kwargs['additional_fields'] = None items = self.folder_collection.account.fetch( ids=self.folder_collection.find_items(self.q, **find_kwargs), only_fields=additional_fields, chunk_size=self.page_size, ) else: if not additional_fields: # If additional_fields is the empty set, we only requested ID and changekey fields. We can then # take a shortcut by using (shape=ID_ONLY, additional_fields=None) to tell find_items() to return # (id, changekey) tuples. We'll post-process those later. find_kwargs['additional_fields'] = None items = self.folder_collection.find_items(self.q, **find_kwargs) if not must_sort_clientside: return items # Resort to client-side sorting of the order_by fields. This is greedy. Sorting in Python is stable, so when # sorting on multiple fields, we can just do a sort on each of the requested fields in reverse order. Reverse # each sort operation if the field was marked as such. for f in reversed(self.order_fields): try: items = sorted(items, key=lambda i: _get_sort_value_or_default(i, f), reverse=f.reverse) except TypeError as e: if 'unorderable types' not in e.args[0]: raise raise ValueError(( "Cannot sort on field '%s'. The field has no default value defined, and there are either items " "with None values for this field, or the query contains exception instances (original error: %s).") % (f.field_path, e)) if not extra_order_fields: return items # Nullify the fields we only needed for sorting before returning return (_rinse_item(i, extra_order_fields) for i in items) def __iter__(self): # Fill cache if this is the first iteration. Return an iterator over the results. Make this non-greedy by # filling the cache while we are iterating. # if self.q.is_never(): return log.debug('Initializing cache') yield from self._format_items(items=self._query(), return_format=self.return_format) """Do not implement __len__. The implementation of list() tries to preallocate memory by calling __len__ on the given sequence, before calling __iter__. If we implemented __len__, we would end up calling FindItems twice, once to get the result of self.count(), an once to return the actual result. Also, according to https://stackoverflow.com/questions/37189968/how-to-have-list-consume-iter-without-calling-len, a __len__ implementation should be cheap. That does not hold for self.count(). def __len__(self): # This queryset has no cache yet. Call the optimized counting implementation return self.count() """ def __getitem__(self, idx_or_slice): # Support indexing and slicing. This is non-greedy when possible (slicing start, stop and step are not negative, # and we're ordering on at most one field), and will only fill the cache if the entire query is iterated. if isinstance(idx_or_slice, int): return self._getitem_idx(idx_or_slice) return self._getitem_slice(idx_or_slice) def _getitem_idx(self, idx): if idx < 0: # Support negative indexes by reversing the queryset and negating the index value reverse_idx = -(idx+1) return self.reverse()[reverse_idx] # Optimize by setting an exact offset and fetching only 1 item new_qs = self._copy_self() new_qs.max_items = 1 new_qs.page_size = 1 new_qs.offset = idx # The iterator will return at most 1 item for item in new_qs.__iter__(): return item raise IndexError() def _getitem_slice(self, s): if ((s.start or 0) < 0) or ((s.stop or 0) < 0) or ((s.step or 0) < 0): # islice() does not support negative start, stop and step. Make sure cache is full by iterating the full # query result, and then slice on the cache. return list(self.__iter__())[s] # Optimize by setting an exact offset and max_items value new_qs = self._copy_self() if s.start is not None and s.stop is not None: new_qs.offset = s.start new_qs.max_items = s.stop - s.start elif s.start is not None: new_qs.offset = s.start elif s.stop is not None: new_qs.max_items = s.stop if new_qs.page_size is None and new_qs.max_items is not None and new_qs.max_items < CHUNK_SIZE: new_qs.page_size = new_qs.max_items return islice(new_qs.__iter__(), None, None, s.step) def _item_yielder(self, iterable, item_func, id_only_func, changekey_only_func, id_and_changekey_func): # Transforms results from the server according to the given transform functions. Makes sure to pass on # Exception instances unaltered. if self.only_fields: has_non_attribute_fields = bool({f for f in self.only_fields if not f.field.is_attribute}) else: has_non_attribute_fields = True if not has_non_attribute_fields: # _query() will return an iterator of (id, changekey) tuples if self._changekey_field not in self.only_fields: transform_func = id_only_func elif self._id_field not in self.only_fields: transform_func = changekey_only_func else: transform_func = id_and_changekey_func for i in iterable: if isinstance(i, Exception): yield i continue yield transform_func(*i) return for i in iterable: if isinstance(i, Exception): yield i continue yield item_func(i) def _as_items(self, iterable): from .items import Item return self._item_yielder( iterable=iterable, item_func=lambda i: i, id_only_func=lambda item_id, changekey: Item(id=item_id), changekey_only_func=lambda item_id, changekey: Item(changekey=changekey), id_and_changekey_func=lambda item_id, changekey: Item(id=item_id, changekey=changekey), ) def _as_values(self, iterable): if not self.only_fields: raise ValueError('values() requires at least one field name') return self._item_yielder( iterable=iterable, item_func=lambda i: {f.path: _get_value_or_default(f, i) for f in self.only_fields}, id_only_func=lambda item_id, changekey: {'id': item_id}, changekey_only_func=lambda item_id, changekey: {'changekey': changekey}, id_and_changekey_func=lambda item_id, changekey: {'id': item_id, 'changekey': changekey}, ) def _as_values_list(self, iterable): if not self.only_fields: raise ValueError('values_list() requires at least one field name') return self._item_yielder( iterable=iterable, item_func=lambda i: tuple(_get_value_or_default(f, i) for f in self.only_fields), id_only_func=lambda item_id, changekey: (item_id,), changekey_only_func=lambda item_id, changekey: (changekey,), id_and_changekey_func=lambda item_id, changekey: (item_id, changekey), ) def _as_flat_values_list(self, iterable): if not self.only_fields or len(self.only_fields) != 1: raise ValueError('flat=True requires exactly one field name') return self._item_yielder( iterable=iterable, item_func=lambda i: _get_value_or_default(self.only_fields[0], i), id_only_func=lambda item_id, changekey: item_id, changekey_only_func=lambda item_id, changekey: changekey, id_and_changekey_func=None, # Can never be called ) ############################### # # Methods that support chaining # ############################### # Return copies of self, so this works as expected: # # foo_qs = my_folder.filter(...) # foo_qs.filter(foo='bar') # foo_qs.filter(foo='baz') # Should not be affected by the previous statement # def all(self): """ """ new_qs = self._copy_self() return new_qs def none(self): """ """ new_qs = self._copy_self() new_qs.q = Q(conn_type=Q.NEVER) return new_qs def filter(self, *args, **kwargs): new_qs = self._copy_self() q = Q(*args, **kwargs) new_qs.q = new_qs.q & q return new_qs def exclude(self, *args, **kwargs): new_qs = self._copy_self() q = ~Q(*args, **kwargs) new_qs.q = new_qs.q & q return new_qs def people(self): """Change the queryset to search the folder for Personas instead of Items.""" new_qs = self._copy_self() new_qs.request_type = self.PERSONA return new_qs def only(self, *args): """Fetch only the specified field names. All other item fields will be 'None'.""" try: only_fields = tuple(self._get_field_path(arg) for arg in args) except ValueError as e: raise ValueError("%s in only()" % e.args[0]) new_qs = self._copy_self() new_qs.only_fields = only_fields return new_qs def order_by(self, *args): """ :return: The QuerySet in reverse order. EWS only supports server-side sorting on a single field. Sorting on multiple fields is implemented client-side and will therefore make the query greedy. """ try: order_fields = tuple(self._get_field_order(arg) for arg in args) except ValueError as e: raise ValueError("%s in order_by()" % e.args[0]) new_qs = self._copy_self() new_qs.order_fields = order_fields return new_qs def reverse(self): """Reverses the ordering of the queryset.""" if not self.order_fields: raise ValueError('Reversing only makes sense if there are order_by fields') new_qs = self._copy_self() for f in new_qs.order_fields: f.reverse = not f.reverse return new_qs def values(self, *args): try: only_fields = tuple(self._get_field_path(arg) for arg in args) except ValueError as e: raise ValueError("%s in values()" % e.args[0]) new_qs = self._copy_self() new_qs.only_fields = only_fields new_qs.return_format = self.VALUES return new_qs def values_list(self, *args, **kwargs): """Return the values of the specified field names as a list of lists. If called with flat=True and only one field name, returns a list of values. """ flat = kwargs.pop('flat', False) if kwargs: raise AttributeError('Unknown kwargs: %s' % kwargs) if flat and len(args) != 1: raise ValueError('flat=True requires exactly one field name') try: only_fields = tuple(self._get_field_path(arg) for arg in args) except ValueError as e: raise ValueError("%s in values_list()" % e.args[0]) new_qs = self._copy_self() new_qs.only_fields = only_fields new_qs.return_format = self.FLAT if flat else self.VALUES_LIST return new_qs def depth(self, depth): """Specify the search depth. Possible values are: SHALLOW, ASSOCIATED or DEEP. :param depth: """ new_qs = self._copy_self() new_qs._depth = depth return new_qs def iterator(self): # Return an iterator over the results warnings.warn('QuerySet no longer caches results. .iterator() is a no-op.', DeprecationWarning, stacklevel=2) return self.__iter__() ########################### # # Methods that end chaining # ########################### def get(self, *args, **kwargs): """Assume the query will return exactly one item. Return that item.""" if not args and set(kwargs) in ({'id'}, {'id', 'changekey'}): # We allow calling get(id=..., changekey=...) to get a single item, but only if exactly these two # kwargs are present. account = self.folder_collection.account item_id = self._id_field.field.clean(kwargs['id'], version=account.version) changekey = self._changekey_field.field.clean(kwargs.get('changekey'), version=account.version) items = list(account.fetch(ids=[(item_id, changekey)], only_fields=self.only_fields)) else: new_qs = self.filter(*args, **kwargs) items = list(new_qs.__iter__()) if not items: raise DoesNotExist() if len(items) != 1: raise MultipleObjectsReturned() return items[0] def count(self, page_size=1000): """Get the query count, with as little effort as possible 'page_size' is the number of items to fetch from the server per request. We're only fetching the IDs, so keep it high :param page_size: (Default value = 1000) """ new_qs = self._copy_self() new_qs.only_fields = () new_qs.order_fields = None new_qs.return_format = self.NONE new_qs.page_size = page_size return len(list(new_qs.__iter__())) def exists(self): """Find out if the query contains any hits, with as little effort as possible.""" new_qs = self._copy_self() new_qs.max_items = 1 return new_qs.count(page_size=1) > 0 def _id_only_copy_self(self): new_qs = self._copy_self() new_qs.only_fields = () new_qs.order_fields = None new_qs.return_format = self.NONE return new_qs def delete(self, page_size=1000, **delete_kwargs): """Delete the items matching the query, with as little effort as possible. 'page_size' is the number of items to fetch and delete per request. We're only fetching the IDs, so keep it high. :param page_size: (Default value = 1000) :param delete_kwargs: """ ids = self._id_only_copy_self() ids.page_size = page_size return self.folder_collection.account.bulk_delete( ids=ids, chunk_size=page_size, **delete_kwargs ) def send(self, page_size=1000, **send_kwargs): """Send the items matching the query, with as little effort as possible. 'page_size' is the number of items to fetch and send per request. We're only fetching the IDs, so keep it high. :param page_size: (Default value = 1000) :param send_kwargs: """ ids = self._id_only_copy_self() ids.page_size = page_size return self.folder_collection.account.bulk_send( ids=ids, chunk_size=page_size, **send_kwargs ) def copy(self, to_folder, page_size=1000, **copy_kwargs): """Copy the items matching the query, with as little effort as possible. 'page_size' is the number of items to fetch and copy per request. We're only fetching the IDs, so keep it high. :param to_folder: :param page_size: (Default value = 1000) :param copy_kwargs: """ ids = self._id_only_copy_self() ids.page_size = page_size return self.folder_collection.account.bulk_copy( ids=ids, to_folder=to_folder, chunk_size=page_size, **copy_kwargs ) def move(self, to_folder, page_size=1000): """Move the items matching the query, with as little effort as possible. 'page_size' is the number of items to fetch and move per request. We're only fetching the IDs, so keep it high. :param to_folder: :param page_size: (Default value = 1000) """ ids = self._id_only_copy_self() ids.page_size = page_size return self.folder_collection.account.bulk_move( ids=ids, to_folder=to_folder, chunk_size=page_size, ) def archive(self, to_folder, page_size=1000): """Archive the items matching the query, with as little effort as possible. 'page_size' is the number of items to fetch and move per request. We're only fetching the IDs, so keep it high. :param to_folder: :param page_size: (Default value = 1000) """ ids = self._id_only_copy_self() ids.page_size = page_size return self.folder_collection.account.bulk_archive( ids=ids, to_folder=to_folder, chunk_size=page_size, ) def mark_as_junk(self, page_size=1000, **mark_as_junk_kwargs): """Mark the items matching the query as junk, with as little effort as possible. 'page_size' is the number of items to fetch and mark per request. We're only fetching the IDs, so keep it high. :param page_size: (Default value = 1000) :param mark_as_junk_kwargs: """ ids = self._id_only_copy_self() ids.page_size = page_size return self.folder_collection.account.bulk_mark_as_junk( ids=ids, chunk_size=page_size, **mark_as_junk_kwargs ) def __str__(self): fmt_args = [('q', str(self.q)), ('folders', '[%s]' % ', '.join(str(f) for f in self.folder_collection.folders))] return self.__class__.__name__ + '(%s)' % ', '.join('%s=%s' % (k, v) for k, v in fmt_args) def _get_value_or_default(field, item): # When we request specific fields using .values() or .values_list(), the incoming item type may not have the field # we are requesting. Return None when this happens instead of raising an AttributeError. try: return field.get_value(item) except AttributeError: return None def _get_sort_value_or_default(item, field_order): # Python can only sort values when <, > and = are implemented for the two types. Try as best we can to sort # items, even when the item may have a None value for the field in question, or when the item is an # Exception. In that case, we calculate a default value and sort all None values and exceptions as the default # value. if isinstance(item, Exception): return _default_field_value(field_order.field_path.field) val = field_order.field_path.get_sort_value(item) if val is None: return _default_field_value(field_order.field_path.field) return val def _default_field_value(field): """Return the default value of a field. If the field does not have a default value, try creating an empty instance of the field value class. If that doesn't work, there's really nothing we can do about it; we'll raise an error. """ return field.default or ([field.value_cls()] if field.is_list else field.value_cls()) def _rinse_item(i, fields_to_nullify): """Set fields in fields_to_nullify to None. Make sure to accept exceptions.""" if isinstance(i, Exception): return i for f in fields_to_nullify: setattr(i, f.field.name, None) return i exchangelib-4.6.1/exchangelib/recurrence.py000066400000000000000000000320501414601472700207610ustar00rootroot00000000000000import logging from .fields import IntegerField, EnumField, EnumListField, DateOrDateTimeField, DateTimeField, EWSElementField, \ IdElementField, MONTHS, WEEK_NUMBERS, WEEKDAYS from .properties import EWSElement, IdChangeKeyMixIn, ItemId, EWSMeta log = logging.getLogger(__name__) def _month_to_str(month): return MONTHS[month-1] if isinstance(month, int) else month def _weekday_to_str(weekday): return WEEKDAYS[weekday - 1] if isinstance(weekday, int) else weekday def _week_number_to_str(week_number): return WEEK_NUMBERS[week_number - 1] if isinstance(week_number, int) else week_number class Pattern(EWSElement, metaclass=EWSMeta): """Base class for all classes implementing recurring pattern elements.""" class Regeneration(Pattern, metaclass=EWSMeta): """Base class for all classes implementing recurring regeneration elements.""" class AbsoluteYearlyPattern(Pattern): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/absoluteyearlyrecurrence """ ELEMENT_NAME = 'AbsoluteYearlyRecurrence' # The day of month of an occurrence, in range 1 -> 31. If a particular month has less days than the day_of_month # value, the last day in the month is assumed day_of_month = IntegerField(field_uri='DayOfMonth', min=1, max=31, is_required=True) # The month of the year, from 1 - 12 month = EnumField(field_uri='Month', enum=MONTHS, is_required=True) def __str__(self): return 'Occurs on day %s of %s' % (self.day_of_month, _month_to_str(self.month)) class RelativeYearlyPattern(Pattern): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/relativeyearlyrecurrence """ ELEMENT_NAME = 'RelativeYearlyRecurrence' # The weekday of the occurrence, as a valid ISO 8601 weekday number in range 1 -> 7 (1 being Monday). # Alternatively, the weekday can be one of the DAY (or 8), WEEK_DAY (or 9) or WEEKEND_DAY (or 10) consts which # is interpreted as the first day, weekday, or weekend day in the month, respectively. weekday = EnumField(field_uri='DaysOfWeek', enum=WEEKDAYS, is_required=True) # Week number of the month, in range 1 -> 5. If 5 is specified, this assumes the last week of the month for # months that have only 4 weeks week_number = EnumField(field_uri='DayOfWeekIndex', enum=WEEK_NUMBERS, is_required=True) # The month of the year, from 1 - 12 month = EnumField(field_uri='Month', enum=MONTHS, is_required=True) def __str__(self): return 'Occurs on weekday %s in the %s week of %s' % ( _weekday_to_str(self.weekday), _week_number_to_str(self.week_number), _month_to_str(self.month) ) class AbsoluteMonthlyPattern(Pattern): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/absolutemonthlyrecurrence """ ELEMENT_NAME = 'AbsoluteMonthlyRecurrence' # Interval, in months, in range 1 -> 99 interval = IntegerField(field_uri='Interval', min=1, max=99, is_required=True) # The day of month of an occurrence, in range 1 -> 31. If a particular month has less days than the day_of_month # value, the last day in the month is assumed day_of_month = IntegerField(field_uri='DayOfMonth', min=1, max=31, is_required=True) def __str__(self): return 'Occurs on day %s of every %s month(s)' % (self.day_of_month, self.interval) class RelativeMonthlyPattern(Pattern): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/relativemonthlyrecurrence """ ELEMENT_NAME = 'RelativeMonthlyRecurrence' # Interval, in months, in range 1 -> 99 interval = IntegerField(field_uri='Interval', min=1, max=99, is_required=True) # The weekday of the occurrence, as a valid ISO 8601 weekday number in range 1 -> 7 (1 being Monday). # Alternatively, the weekday can be one of the DAY (or 8), WEEK_DAY (or 9) or WEEKEND_DAY (or 10) consts which # is interpreted as the first day, weekday, or weekend day in the month, respectively. weekday = EnumField(field_uri='DaysOfWeek', enum=WEEKDAYS, is_required=True) # Week number of the month, in range 1 -> 5. If 5 is specified, this assumes the last week of the month for # months that have only 4 weeks. week_number = EnumField(field_uri='DayOfWeekIndex', enum=WEEK_NUMBERS, is_required=True) def __str__(self): return 'Occurs on weekday %s in the %s week of every %s month(s)' % ( _weekday_to_str(self.weekday), _week_number_to_str(self.week_number), self.interval ) class WeeklyPattern(Pattern): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/weeklyrecurrence""" ELEMENT_NAME = 'WeeklyRecurrence' # Interval, in weeks, in range 1 -> 99 interval = IntegerField(field_uri='Interval', min=1, max=99, is_required=True) # List of valid ISO 8601 weekdays, as list of numbers in range 1 -> 7 (1 being Monday) weekdays = EnumListField(field_uri='DaysOfWeek', enum=WEEKDAYS, is_required=True) # The first day of the week. Defaults to Monday first_day_of_week = EnumField(field_uri='FirstDayOfWeek', enum=WEEKDAYS, default=1, is_required=True) def __str__(self): if isinstance(self.weekdays, str): weekdays = [self.weekdays] elif isinstance(self.weekdays, int): weekdays = [_weekday_to_str(self.weekdays)] else: weekdays = [_weekday_to_str(i) for i in self.weekdays] return 'Occurs on weekdays %s of every %s week(s) where the first day of the week is %s' % ( ', '.join(weekdays), self.interval, _weekday_to_str(self.first_day_of_week) ) class DailyPattern(Pattern): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/dailyrecurrence""" ELEMENT_NAME = 'DailyRecurrence' # Interval, in days, in range 1 -> 999 interval = IntegerField(field_uri='Interval', min=1, max=999, is_required=True) def __str__(self): return 'Occurs every %s day(s)' % self.interval class YearlyRegeneration(Regeneration): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/yearlyregeneration""" ELEMENT_NAME = 'YearlyRegeneration' # Interval, in years interval = IntegerField(field_uri='Interval', min=1, is_required=True) def __str__(self): return 'Regenerates every %s year(s)' % self.interval class MonthlyRegeneration(Regeneration): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/monthlyregeneration""" ELEMENT_NAME = 'MonthlyRegeneration' # Interval, in months interval = IntegerField(field_uri='Interval', min=1, is_required=True) def __str__(self): return 'Regenerates every %s month(s)' % self.interval class WeeklyRegeneration(Regeneration): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/weeklyregeneration""" ELEMENT_NAME = 'WeeklyRegeneration' # Interval, in weeks interval = IntegerField(field_uri='Interval', min=1, is_required=True) def __str__(self): return 'Regenerates every %s week(s)' % self.interval class DailyRegeneration(Regeneration): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/dailyregeneration""" ELEMENT_NAME = 'DailyRegeneration' # Interval, in days interval = IntegerField(field_uri='Interval', min=1, is_required=True) def __str__(self): return 'Regenerates every %s day(s)' % self.interval class Boundary(EWSElement, metaclass=EWSMeta): """Base class for all classes implementing recurring boundary elements.""" class NoEndPattern(Boundary): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/noendrecurrence""" ELEMENT_NAME = 'NoEndRecurrence' # Start date, as EWSDate or EWSDateTime start = DateOrDateTimeField(field_uri='StartDate', is_required=True) def __str__(self): return 'Starts on %s' % self.start class EndDatePattern(Boundary): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/enddaterecurrence""" ELEMENT_NAME = 'EndDateRecurrence' # Start date, as EWSDate or EWSDateTime start = DateOrDateTimeField(field_uri='StartDate', is_required=True) # End date, as EWSDate end = DateOrDateTimeField(field_uri='EndDate', is_required=True) def __str__(self): return 'Starts on %s, ends on %s' % (self.start, self.end) class NumberedPattern(Boundary): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/numberedrecurrence""" ELEMENT_NAME = 'NumberedRecurrence' # Start date, as EWSDate or EWSDateTime start = DateOrDateTimeField(field_uri='StartDate', is_required=True) # The number of occurrences in this pattern, in range 1 -> 999 number = IntegerField(field_uri='NumberOfOccurrences', min=1, max=999, is_required=True) def __str__(self): return 'Starts on %s and occurs %s times' % (self.start, self.number) class Occurrence(IdChangeKeyMixIn): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/occurrence""" ELEMENT_NAME = 'Occurrence' ID_ELEMENT_CLS = ItemId _id = IdElementField(field_uri='ItemId', value_cls=ID_ELEMENT_CLS) # The modified start time of the item, as EWSDateTime start = DateTimeField(field_uri='Start') # The modified end time of the item, as EWSDateTime end = DateTimeField(field_uri='End') # The original start time of the item, as EWSDateTime original_start = DateTimeField(field_uri='OriginalStart') # Container elements: # 'ModifiedOccurrences' # 'DeletedOccurrences' class FirstOccurrence(Occurrence): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/firstoccurrence""" ELEMENT_NAME = 'FirstOccurrence' class LastOccurrence(Occurrence): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/lastoccurrence""" ELEMENT_NAME = 'LastOccurrence' class DeletedOccurrence(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deletedoccurrence""" ELEMENT_NAME = 'DeletedOccurrence' # The modified start time of the item, as EWSDateTime start = DateTimeField(field_uri='Start') PATTERN_CLASSES = AbsoluteYearlyPattern, RelativeYearlyPattern, AbsoluteMonthlyPattern, RelativeMonthlyPattern, \ WeeklyPattern, DailyPattern REGENERATION_CLASSES = YearlyRegeneration, MonthlyRegeneration, WeeklyRegeneration, DailyRegeneration BOUNDARY_CLASSES = NoEndPattern, EndDatePattern, NumberedPattern class Recurrence(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/recurrence-recurrencetype """ ELEMENT_NAME = 'Recurrence' PATTERN_CLASSES = PATTERN_CLASSES pattern = EWSElementField(value_cls=Pattern) boundary = EWSElementField(value_cls=Boundary) def __init__(self, **kwargs): # Allow specifying a start, end and/or number as a shortcut to creating a boundary start = kwargs.pop('start', None) end = kwargs.pop('end', None) number = kwargs.pop('number', None) if any([start, end, number]): if 'boundary' in kwargs: raise ValueError("'boundary' is not allowed in combination with 'start', 'end' or 'number'") if start and not end and not number: kwargs['boundary'] = NoEndPattern(start=start) elif start and end and not number: kwargs['boundary'] = EndDatePattern(start=start, end=end) elif start and number and not end: kwargs['boundary'] = NumberedPattern(start=start, number=number) else: raise ValueError("Unsupported 'start', 'end', 'number' combination") super().__init__(**kwargs) @classmethod def from_xml(cls, elem, account): for pattern_cls in cls.PATTERN_CLASSES: pattern_elem = elem.find(pattern_cls.response_tag()) if pattern_elem is None: continue pattern = pattern_cls.from_xml(elem=pattern_elem, account=account) break else: pattern = None for boundary_cls in BOUNDARY_CLASSES: boundary_elem = elem.find(boundary_cls.response_tag()) if boundary_elem is None: continue boundary = boundary_cls.from_xml(elem=boundary_elem, account=account) break else: boundary = None return cls(pattern=pattern, boundary=boundary) def __str__(self): return 'Pattern: %s, Boundary: %s' % (self.pattern, self.boundary) class TaskRecurrence(Recurrence): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/recurrence-taskrecurrencetype """ PATTERN_CLASSES = PATTERN_CLASSES + REGENERATION_CLASSES exchangelib-4.6.1/exchangelib/restriction.py000066400000000000000000000627241414601472700212040ustar00rootroot00000000000000import logging from collections import OrderedDict from copy import copy from .fields import InvalidField, FieldPath, DateTimeBackedDateField from .util import create_element, xml_to_str, value_to_xml_text, is_iterable from .version import EXCHANGE_2010 log = logging.getLogger(__name__) class Q: """A class with an API similar to Django Q objects. Used to implemnt advanced filtering logic.""" # Connection types AND = 'AND' OR = 'OR' NOT = 'NOT' NEVER = 'NEVER' # This is not specified by EWS. We use it for queries that will never match, e.g. 'foo__in=()' CONN_TYPES = {AND, OR, NOT, NEVER} # EWS Operators EQ = '==' NE = '!=' GT = '>' GTE = '>=' LT = '<' LTE = '<=' EXACT = 'exact' IEXACT = 'iexact' CONTAINS = 'contains' ICONTAINS = 'icontains' STARTSWITH = 'startswith' ISTARTSWITH = 'istartswith' EXISTS = 'exists' OP_TYPES = {EQ, NE, GT, GTE, LT, LTE, EXACT, IEXACT, CONTAINS, ICONTAINS, STARTSWITH, ISTARTSWITH, EXISTS} CONTAINS_OPS = {EXACT, IEXACT, CONTAINS, ICONTAINS, STARTSWITH, ISTARTSWITH} # Valid lookups LOOKUP_RANGE = 'range' LOOKUP_IN = 'in' LOOKUP_NOT = 'not' LOOKUP_GT = 'gt' LOOKUP_GTE = 'gte' LOOKUP_LT = 'lt' LOOKUP_LTE = 'lte' LOOKUP_EXACT = 'exact' LOOKUP_IEXACT = 'iexact' LOOKUP_CONTAINS = 'contains' LOOKUP_ICONTAINS = 'icontains' LOOKUP_STARTSWITH = 'startswith' LOOKUP_ISTARTSWITH = 'istartswith' LOOKUP_EXISTS = 'exists' LOOKUP_TYPES = {LOOKUP_RANGE, LOOKUP_IN, LOOKUP_NOT, LOOKUP_GT, LOOKUP_GTE, LOOKUP_LT, LOOKUP_LTE, LOOKUP_EXACT, LOOKUP_IEXACT, LOOKUP_CONTAINS, LOOKUP_ICONTAINS, LOOKUP_STARTSWITH, LOOKUP_ISTARTSWITH, LOOKUP_EXISTS} __slots__ = 'conn_type', 'field_path', 'op', 'value', 'children', 'query_string' def __init__(self, *args, **kwargs): self.conn_type = kwargs.pop('conn_type', self.AND) self.field_path = None # Name of the field we want to filter on self.op = None self.value = None self.query_string = None # Parsing of args and kwargs may require child elements self.children = [] # Check for query string as the only argument if not kwargs and len(args) == 1 and isinstance(args[0], str): self.query_string = args[0] args = () # Parse args which must now be Q objects for q in args: if not isinstance(q, self.__class__): raise ValueError("Non-keyword arg %r must be a Q instance" % q) self.children.extend(args) # Parse keyword args and extract the filter is_single_kwarg = len(args) == 0 and len(kwargs) == 1 for key, value in kwargs.items(): self.children.extend( self._get_children_from_kwarg(key=key, value=value, is_single_kwarg=is_single_kwarg) ) # Simplify this object self.reduce() # Final sanity check self._check_integrity() def _get_children_from_kwarg(self, key, value, is_single_kwarg=False): """Generate Q objects corresponding to a single keyword argument. Make this a leaf if there are no children to generate. """ key_parts = key.rsplit('__', 1) if len(key_parts) == 2 and key_parts[1] in self.LOOKUP_TYPES: # This is a kwarg with a lookup at the end field_path, lookup = key_parts if lookup == self.LOOKUP_EXISTS: # value=True will fall through to further processing if not value: return [~self.__class__(**{key: True})] if lookup == self.LOOKUP_RANGE: # EWS doesn't have a 'range' operator. Emulate 'foo__range=(1, 2)' as 'foo__gte=1 and foo__lte=2' # (both values inclusive). if len(value) != 2: raise ValueError("Value of lookup '%s' must have exactly 2 elements" % key) return [ self.__class__(**{'%s__gte' % field_path: value[0]}), self.__class__(**{'%s__lte' % field_path: value[1]}), ] # Filtering on list types is a bit quirky. The only lookup type I have found to work is: # # item:Categories == 'foo' AND item:Categories == 'bar' AND ... # # item:Categories == 'foo' OR item:Categories == 'bar' OR ... # # The former returns items that have all these categories, but maybe also others. The latter returns # items that have at least one of these categories. This translates to the 'contains' and 'in' lookups, # respectively. Both versions are case-insensitive. # # Exact matching and case-sensitive or partial-string matching is not possible since that requires the # 'Contains' element which only supports matching on string elements, not arrays. # # Exact matching of categories (i.e. match ['a', 'b'] but not ['a', 'b', 'c']) could be implemented by # post-processing items by fetch the categories field unconditionally and removing the items that don't # have an exact match. if lookup == self.LOOKUP_IN: # EWS doesn't have an '__in' operator. Allow '__in' lookups on list and non-list field types, # specifying a list value. We'll emulate it as a set of OR'ed exact matches. if not is_iterable(value, generators_allowed=True): raise ValueError("Value for lookup %r must be a list" % key) children = [self.__class__(**{field_path: v}) for v in value] if not children: # This is an '__in' operator with an empty list as the value. We interpret it to mean "is foo # contained in the empty set?" which is always false. Mark this Q object as such. return [self.__class__(conn_type=self.NEVER)] return [self.__class__(*children, conn_type=self.OR)] if lookup == self.LOOKUP_CONTAINS and is_iterable(value, generators_allowed=True): # A '__contains' lookup with an list as the value ony makes sense for list fields, since exact match # on multiple distinct values will always fail for single-value fields. # # An empty list as value is allowed. We interpret it to mean "are all values in the empty set contained # in foo?" which is always true. children = [self.__class__(**{field_path: v}) for v in value] return [self.__class__(*children, conn_type=self.AND)] try: op = self._lookup_to_op(lookup) except KeyError: raise ValueError("Lookup '%s' is not supported (called as '%s=%r')" % (lookup, key, value)) else: field_path, op = key, self.EQ if not is_single_kwarg: return [self.__class__(**{key: value})] # This is a single-kwarg Q object with a lookup that requires a single value. Make this a leaf self.field_path = field_path self.op = op self.value = value return [] def reduce(self): """Simplify this object, if possible.""" self._reduce_children() self._promote() def _reduce_children(self): """Look at the children of this object and remove unnecessary items.""" children = self.children if any((isinstance(a, self.__class__) and a.is_never()) for a in children): # We have at least one 'never' arg if self.conn_type == self.AND: # Remove all other args since nothing we AND together with a 'never' arg can change the result children = [self.__class__(conn_type=self.NEVER)] elif self.conn_type == self.OR: # Remove all 'never' args because all other args will decide the result. Keep one 'never' arg in case # all args are 'never' args. children = [a for a in children if not (isinstance(a, self.__class__) and a.is_never())] if not children: children = [self.__class__(conn_type=self.NEVER)] elif self.conn_type == self.NOT: # Let's interpret 'not never' to mean 'always'. Remove all 'never' args children = [a for a in children if not (isinstance(a, self.__class__) and a.is_never())] # Remove any empty Q elements in args before proceeding children = [a for a in children if not (isinstance(a, self.__class__) and a.is_empty())] self.children = children def _promote(self): """When we only have one child and no expression on ourselves, we are a no-op. Flatten by taking over the only child. """ if len(self.children) != 1 or self.field_path is not None or self.conn_type == self.NOT: return q = self.children[0] self.conn_type = q.conn_type self.field_path = q.field_path self.op = q.op self.value = q.value self.query_string = q.query_string self.children = q.children def clean(self, version): """Do some basic checks on the attributes, using a generic folder. to_xml() does a really good job of validating. There's no reason to replicate much of that here. """ from .folders import Folder self.to_xml(folders=[Folder()], version=version, applies_to=Restriction.ITEMS) @classmethod def _lookup_to_op(cls, lookup): return { cls.LOOKUP_NOT: cls.NE, cls.LOOKUP_GT: cls.GT, cls.LOOKUP_GTE: cls.GTE, cls.LOOKUP_LT: cls.LT, cls.LOOKUP_LTE: cls.LTE, cls.LOOKUP_EXACT: cls.EXACT, cls.LOOKUP_IEXACT: cls.IEXACT, cls.LOOKUP_CONTAINS: cls.CONTAINS, cls.LOOKUP_ICONTAINS: cls.ICONTAINS, cls.LOOKUP_STARTSWITH: cls.STARTSWITH, cls.LOOKUP_ISTARTSWITH: cls.ISTARTSWITH, cls.LOOKUP_EXISTS: cls.EXISTS, }[lookup] @classmethod def _conn_to_xml(cls, conn_type): xml_tag_map = { cls.AND: 't:And', cls.OR: 't:Or', cls.NOT: 't:Not', } return create_element(xml_tag_map[conn_type]) @classmethod def _op_to_xml(cls, op): xml_tag_map = { cls.EQ: 't:IsEqualTo', cls.NE: 't:IsNotEqualTo', cls.GTE: 't:IsGreaterThanOrEqualTo', cls.LTE: 't:IsLessThanOrEqualTo', cls.LT: 't:IsLessThan', cls.GT: 't:IsGreaterThan', cls.EXISTS: 't:Exists', } if op in xml_tag_map: return create_element(xml_tag_map[op]) valid_ops = cls.EXACT, cls.IEXACT, cls.CONTAINS, cls.ICONTAINS, cls.STARTSWITH, cls.ISTARTSWITH if op not in valid_ops: raise ValueError("'op' %s must be one of %s" % (op, valid_ops)) # For description of Contains attribute values, see # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/contains # # Possible ContainmentMode values: # FullString, Prefixed, Substring, PrefixOnWords, ExactPhrase # Django lookups have no equivalent of PrefixOnWords and ExactPhrase (and I'm unsure how they actually # work). # # EWS has no equivalent of '__endswith' or '__iendswith'. That could be emulated using '__contains' and # '__icontains' and filtering results afterwards in Python. But it could be inefficient because we might be # fetching and discarding a lot of non-matching items, plus we would need to always fetch the field we're # matching on, to be able to do the filtering. I think it's better to leave this to the consumer, i.e.: # # items = [i for i in fld.filter(subject__contains=suffix) if i.subject.endswith(suffix)] # items = [i for i in fld.filter(subject__icontains=suffix) if i.subject.lower().endswith(suffix.lower())] # # Possible ContainmentComparison values (there are more, but the rest are "To be removed"): # Exact, IgnoreCase, IgnoreNonSpacingCharacters, IgnoreCaseAndNonSpacingCharacters # I'm unsure about non-spacing characters, but as I read # https://en.wikipedia.org/wiki/Graphic_character#Spacing_and_non-spacing_characters # we shouldn't ignore them ('a' would match both 'a' and 'å', the latter having a non-spacing character). if op in {cls.EXACT, cls.IEXACT}: match_mode = 'FullString' elif op in (cls.CONTAINS, cls.ICONTAINS): match_mode = 'Substring' elif op in (cls.STARTSWITH, cls.ISTARTSWITH): match_mode = 'Prefixed' else: raise ValueError('Unsupported op: %s' % op) if op in (cls.IEXACT, cls.ICONTAINS, cls.ISTARTSWITH): compare_mode = 'IgnoreCase' else: compare_mode = 'Exact' return create_element( 't:Contains', attrs=OrderedDict([ ('ContainmentMode', match_mode), ('ContainmentComparison', compare_mode), ]) ) def is_leaf(self): return not self.children def is_empty(self): """Return True if this object is without any restrictions at all.""" return self.is_leaf() and self.field_path is None and self.query_string is None and self.conn_type != self.NEVER def is_never(self): """Return True if this object has a restriction that will never match anything.""" return self.conn_type == self.NEVER def expr(self): if self.is_empty(): return None if self.is_never(): return self.NEVER if self.query_string: return self.query_string if self.is_leaf(): expr = '%s %s %r' % (self.field_path, self.op, self.value) else: # Sort children by field name so we get stable output (for easier testing). Children should never be empty. expr = (' %s ' % (self.AND if self.conn_type == self.NOT else self.conn_type)).join( (c.expr() if c.is_leaf() or c.conn_type == self.NOT else '(%s)' % c.expr()) for c in sorted(self.children, key=lambda i: i.field_path or '') ) if self.conn_type == self.NOT: # Add the NOT operator. Put children in parens if there is more than one child. if self.is_leaf() or len(self.children) == 1: return self.conn_type + ' %s' % expr return self.conn_type + ' (%s)' % expr return expr def to_xml(self, folders, version, applies_to): if self.query_string: self._check_integrity() if version.build < EXCHANGE_2010: raise NotImplementedError('QueryString filtering is only supported for Exchange 2010 servers and later') elem = create_element('m:QueryString') elem.text = self.query_string return elem # Translate this Q object to a valid Restriction XML tree elem = self.xml_elem(folders=folders, version=version, applies_to=applies_to) if elem is None: return None restriction = create_element('m:Restriction') restriction.append(elem) return restriction def _check_integrity(self): if self.is_empty(): return if self.conn_type == self.NEVER: if any([self.field_path, self.op, self.value, self.children]): raise ValueError("'never' queries cannot be combined with other settings") return if self.query_string: if any([self.field_path, self.op, self.value, self.children]): raise ValueError('Query strings cannot be combined with other settings') return if self.conn_type not in self.CONN_TYPES: raise ValueError("'conn_type' %s must be one of %s" % (self.conn_type, self.CONN_TYPES)) if not self.is_leaf(): for q in self.children: if q.query_string and len(self.children) > 1: raise ValueError( 'A query string cannot be combined with other restrictions' ) return if not self.field_path: raise ValueError("'field_path' must be set") if self.op not in self.OP_TYPES: raise ValueError("'op' %s must be one of %s" % (self.op, self.OP_TYPES)) if self.op == self.EXISTS and self.value is not True: raise ValueError("'value' must be True when operator is EXISTS") if self.value is None: raise ValueError('Value for filter on field path "%s" cannot be None' % self.field_path) if is_iterable(self.value, generators_allowed=True): raise ValueError( 'Value %r for filter on field path "%s" must be a single value' % (self.value, self.field_path) ) def _validate_field_path(self, field_path, folder, applies_to, version): from .indexed_properties import MultiFieldIndexedElement if applies_to == Restriction.FOLDERS: # This is a restriction on Folder fields folder.validate_field(field=field_path.field, version=version) else: folder.validate_item_field(field=field_path.field, version=version) if not field_path.field.is_searchable: raise ValueError("EWS does not support filtering on field '%s'" % field_path.field.name) if field_path.subfield and not field_path.subfield.is_searchable: raise ValueError("EWS does not support filtering on subfield '%s'" % field_path.subfield.name) if issubclass(field_path.field.value_cls, MultiFieldIndexedElement) and not field_path.subfield: raise ValueError("Field path '%s' must contain a subfield" % self.field_path) def _get_field_path(self, folders, applies_to, version): # Convert the string field path to a real FieldPath object. The path is validated using the given folders. for folder in folders: try: if applies_to == Restriction.FOLDERS: # This is a restriction on Folder fields field = folder.get_field_by_fieldname(fieldname=self.field_path) field_path = FieldPath(field=field) else: field_path = FieldPath.from_string(field_path=self.field_path, folder=folder) except ValueError: continue self._validate_field_path(field_path=field_path, folder=folder, applies_to=applies_to, version=version) break else: raise InvalidField("Unknown field path %r on folders %s" % (self.field_path, folders)) return field_path def _get_clean_value(self, field_path, version): if self.op == self.EXISTS: return None clean_field = field_path.subfield if (field_path.subfield and field_path.label) else field_path.field if clean_field.is_list: # __contains and __in are implemented as multiple leaves, with one value per leaf. clean() on list fields # only works on lists, so clean a one-element list. return clean_field.clean(value=[self.value], version=version)[0] return clean_field.clean(value=self.value, version=version) def xml_elem(self, folders, version, applies_to): # Recursively build an XML tree structure of this Q object. If this is an empty leaf (the equivalent of Q()), # return None. from .indexed_properties import SingleFieldIndexedElement # Don't check self.value just yet. We want to return error messages on the field path first, and then the value. # This is done in _get_field_path() and _get_clean_value(), respectively. self._check_integrity() if self.is_empty(): return None if self.is_never(): raise ValueError("EWS does not support 'never' queries") if self.is_leaf(): elem = self._op_to_xml(self.op) field_path = self._get_field_path(folders, applies_to=applies_to, version=version) clean_value = self._get_clean_value(field_path=field_path, version=version) if issubclass(field_path.field.value_cls, SingleFieldIndexedElement) and not field_path.label: # We allow a filter shortcut of e.g. email_addresses__contains=EmailAddress(label='Foo', ...) instead of # email_addresses__Foo_email_address=.... Set FieldPath label now so we can generate the field_uri. field_path.label = clean_value.label elif isinstance(field_path.field, DateTimeBackedDateField): # We need to convert to datetime clean_value = field_path.field.date_to_datetime(clean_value) elem.append(field_path.to_xml()) constant = create_element('t:Constant') if self.op != self.EXISTS: # Use .set() to not fill up the create_element() cache with unique values constant.set('Value', value_to_xml_text(clean_value)) if self.op in self.CONTAINS_OPS: elem.append(constant) else: uriorconst = create_element('t:FieldURIOrConstant') uriorconst.append(constant) elem.append(uriorconst) elif len(self.children) == 1: # We have only one child elem = self.children[0].xml_elem(folders=folders, version=version, applies_to=applies_to) else: # We have multiple children. If conn_type is NOT, then group children with AND. We'll add the NOT later elem = self._conn_to_xml(self.AND if self.conn_type == self.NOT else self.conn_type) # Sort children by field name so we get stable output (for easier testing). Children should never be empty for c in sorted(self.children, key=lambda i: i.field_path or ''): elem.append(c.xml_elem(folders=folders, version=version, applies_to=applies_to)) if elem is None: return None # Should not be necessary, but play safe if self.conn_type == self.NOT: # Encapsulate everything in the NOT element not_elem = self._conn_to_xml(self.conn_type) not_elem.append(elem) return not_elem return elem def __and__(self, other): # & operator. Return a new Q with two children and conn_type AND return self.__class__(self, other, conn_type=self.AND) def __or__(self, other): # | operator. Return a new Q with two children and conn_type OR return self.__class__(self, other, conn_type=self.OR) def __invert__(self): # ~ operator. If op has an inverse, change op. Else return a new Q with conn_type NOT if self.conn_type == self.NOT: # This is NOT NOT. Change to AND new = copy(self) new.conn_type = self.AND new.reduce() return new if self.is_leaf(): inverse_ops = { self.EQ: self.NE, self.NE: self.EQ, self.GT: self.LTE, self.GTE: self.LT, self.LT: self.GTE, self.LTE: self.GT, } try: new = copy(self) new.op = inverse_ops[self.op] new.reduce() return new except KeyError: pass return self.__class__(self, conn_type=self.NOT) def __eq__(self, other): return repr(self) == repr(other) def __hash__(self): return hash(repr(self)) def __str__(self): return self.expr() or 'Q()' def __repr__(self): if self.is_leaf(): if self.query_string: return self.__class__.__name__ + '(%r)' % self.query_string if self.is_never(): return self.__class__.__name__ + '(conn_type=%r)' % (self.conn_type) return self.__class__.__name__ + '(%s %s %r)' % (self.field_path, self.op, self.value) sorted_children = tuple(sorted(self.children, key=lambda i: i.field_path or '')) if self.conn_type == self.NOT or len(self.children) > 1: return self.__class__.__name__ + repr((self.conn_type,) + sorted_children) return self.__class__.__name__ + repr(sorted_children) class Restriction: """Implement an EWS Restriction type.""" # The type of item the restriction applies to FOLDERS = 'folders' ITEMS = 'items' RESTRICTION_TYPES = (FOLDERS, ITEMS) def __init__(self, q, folders, applies_to): if not isinstance(q, Q): raise ValueError("'q' value %r must be a Q instance" % q) if q.is_empty(): raise ValueError("Q object must not be empty") from .folders import BaseFolder for folder in folders: if not isinstance(folder, BaseFolder): raise ValueError("'folder' value %r must be a Folder instance" % folder) if applies_to not in self.RESTRICTION_TYPES: raise ValueError("'applies_to' must be one of %s" % (self.RESTRICTION_TYPES,)) self.q = q self.folders = folders self.applies_to = applies_to def to_xml(self, version): return self.q.to_xml(folders=self.folders, version=version, applies_to=self.applies_to) def __str__(self): """Print the XML syntax tree.""" return xml_to_str(self.to_xml(version=self.folders[0].account.version)) exchangelib-4.6.1/exchangelib/services/000077500000000000000000000000001414601472700200755ustar00rootroot00000000000000exchangelib-4.6.1/exchangelib/services/__init__.py000066400000000000000000000067131414601472700222150ustar00rootroot00000000000000"""Implement a selection of EWS services (operations). Exchange is very picky about things like the order of XML elements in SOAP requests, so we need to generate XML automatically instead of taking advantage of Python SOAP libraries and the WSDL file. Exchange EWS operations overview: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ews-operations-in-exchange """ from .archive_item import ArchiveItem from .common import CHUNK_SIZE from .convert_id import ConvertId from .copy_item import CopyItem from .create_attachment import CreateAttachment from .create_folder import CreateFolder from .create_item import CreateItem from .create_user_configuration import CreateUserConfiguration from .delete_attachment import DeleteAttachment from .delete_folder import DeleteFolder from .delete_item import DeleteItem from .delete_user_configuration import DeleteUserConfiguration from .empty_folder import EmptyFolder from .expand_dl import ExpandDL from .export_items import ExportItems from .find_folder import FindFolder from .find_item import FindItem from .find_people import FindPeople from .get_attachment import GetAttachment from .get_delegate import GetDelegate from .get_events import GetEvents from .get_folder import GetFolder from .get_item import GetItem from .get_mail_tips import GetMailTips from .get_persona import GetPersona from .get_room_lists import GetRoomLists from .get_rooms import GetRooms from .get_searchable_mailboxes import GetSearchableMailboxes from .get_server_time_zones import GetServerTimeZones from .get_streaming_events import GetStreamingEvents from .get_user_availability import GetUserAvailability from .get_user_configuration import GetUserConfiguration from .get_user_oof_settings import GetUserOofSettings from .mark_as_junk import MarkAsJunk from .move_folder import MoveFolder from .move_item import MoveItem from .resolve_names import ResolveNames from .send_item import SendItem from .send_notification import SendNotification from .set_user_oof_settings import SetUserOofSettings from .subscribe import SubscribeToStreaming, SubscribeToPull, SubscribeToPush from .sync_folder_hierarchy import SyncFolderHierarchy from .sync_folder_items import SyncFolderItems from .unsubscribe import Unsubscribe from .update_folder import UpdateFolder from .update_item import UpdateItem from .update_user_configuration import UpdateUserConfiguration from .upload_items import UploadItems __all__ = [ 'CHUNK_SIZE', 'ArchiveItem', 'ConvertId', 'CopyItem', 'CreateAttachment', 'CreateFolder', 'CreateItem', 'CreateUserConfiguration', 'DeleteAttachment', 'DeleteFolder', 'DeleteUserConfiguration', 'DeleteItem', 'EmptyFolder', 'ExpandDL', 'ExportItems', 'FindFolder', 'FindItem', 'FindPeople', 'GetAttachment', 'GetDelegate', 'GetEvents', 'GetFolder', 'GetItem', 'GetMailTips', 'GetPersona', 'GetRoomLists', 'GetRooms', 'GetSearchableMailboxes', 'GetServerTimeZones', 'GetStreamingEvents', 'GetUserAvailability', 'GetUserConfiguration', 'GetUserOofSettings', 'MarkAsJunk', 'MoveFolder', 'MoveItem', 'ResolveNames', 'SendItem', 'SendNotification', 'SetUserOofSettings', 'SubscribeToPull', 'SubscribeToPush', 'SubscribeToStreaming', 'SyncFolderHierarchy', 'SyncFolderItems', 'Unsubscribe', 'UpdateFolder', 'UpdateItem', 'UpdateUserConfiguration', 'UploadItems', ] exchangelib-4.6.1/exchangelib/services/archive_item.py000066400000000000000000000027501414601472700231120ustar00rootroot00000000000000from .common import EWSAccountService, create_folder_ids_element, create_item_ids_element from ..util import create_element, MNS from ..version import EXCHANGE_2013 class ArchiveItem(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/archiveitem-operation""" SERVICE_NAME = 'ArchiveItem' element_container_name = '{%s}Items' % MNS supported_from = EXCHANGE_2013 def call(self, items, to_folder): """Move a list of items to a specific folder in the archive mailbox. :param items: a list of (id, changekey) tuples or Item objects :param to_folder: :return: None """ return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, to_folder=to_folder)) def _elems_to_objs(self, elems): from ..items import Item for elem in elems: if isinstance(elem, Exception): yield elem continue yield Item.id_from_xml(elem) def get_payload(self, items, to_folder): archiveitem = create_element('m:%s' % self.SERVICE_NAME) folder_id = create_folder_ids_element(tag='m:ArchiveSourceFolderId', folders=[to_folder], version=self.account.version) item_ids = create_item_ids_element(items=items, version=self.account.version) archiveitem.append(folder_id) archiveitem.append(item_ids) return archiveitem exchangelib-4.6.1/exchangelib/services/common.py000066400000000000000000001244151414601472700217460ustar00rootroot00000000000000import abc import logging import traceback from itertools import chain from .. import errors from ..credentials import IMPERSONATION, OAuth2Credentials from ..errors import EWSWarning, TransportError, SOAPError, ErrorTimeoutExpired, ErrorBatchProcessingStopped, \ ErrorQuotaExceeded, ErrorCannotDeleteObject, ErrorCreateItemAccessDenied, ErrorFolderNotFound, \ ErrorNonExistentMailbox, ErrorMailboxStoreUnavailable, ErrorImpersonateUserDenied, ErrorInternalServerError, \ ErrorInternalServerTransientError, ErrorNoRespondingCASInDestinationSite, ErrorImpersonationFailed, \ ErrorMailboxMoveInProgress, ErrorAccessDenied, ErrorConnectionFailed, RateLimitError, ErrorServerBusy, \ ErrorTooManyObjectsOpened, ErrorInvalidLicense, ErrorInvalidSchemaVersionForMailboxVersion, \ ErrorInvalidServerVersion, ErrorItemNotFound, ErrorADUnavailable, ErrorInvalidChangeKey, \ ErrorItemSave, ErrorInvalidIdMalformed, ErrorMessageSizeExceeded, UnauthorizedError, \ ErrorCannotDeleteTaskOccurrence, ErrorMimeContentConversionFailed, ErrorRecurrenceHasNoOccurrence, \ ErrorNoPublicFolderReplicaAvailable, MalformedResponseError, ErrorExceededConnectionCount, \ SessionPoolMinSizeReached, ErrorIncorrectSchemaVersion, ErrorInvalidRequest, ErrorCorruptData, \ ErrorCannotEmptyFolder, ErrorDeleteDistinguishedFolder, ErrorInvalidSubscription, ErrorInvalidWatermark, \ ErrorInvalidSyncStateData, ErrorNameResolutionNoResults, ErrorNameResolutionMultipleResults, \ ErrorConnectionFailedTransientError from ..properties import FieldURI, IndexedFieldURI, ExtendedFieldURI, ExceptionFieldURI, ItemId from ..transport import wrap from ..util import chunkify, create_element, add_xml_child, get_xml_attr, to_xml, post_ratelimited, \ xml_to_str, set_xml_value, SOAPNS, TNS, MNS, ENS, ParseError, DummyResponse from ..version import API_VERSIONS, Version log = logging.getLogger(__name__) CHUNK_SIZE = 100 # A default chunk size for all services KNOWN_EXCEPTIONS = ( ErrorAccessDenied, ErrorADUnavailable, ErrorBatchProcessingStopped, ErrorCannotDeleteObject, ErrorCannotEmptyFolder, ErrorConnectionFailed, ErrorConnectionFailedTransientError, ErrorCreateItemAccessDenied, ErrorDeleteDistinguishedFolder, ErrorExceededConnectionCount, ErrorFolderNotFound, ErrorImpersonateUserDenied, ErrorImpersonationFailed, ErrorInternalServerError, ErrorInternalServerTransientError, ErrorInvalidChangeKey, ErrorInvalidLicense, ErrorInvalidSubscription, ErrorInvalidSyncStateData, ErrorInvalidWatermark, ErrorItemNotFound, ErrorMailboxMoveInProgress, ErrorMailboxStoreUnavailable, ErrorNameResolutionMultipleResults, ErrorNameResolutionNoResults, ErrorNonExistentMailbox, ErrorNoPublicFolderReplicaAvailable, ErrorNoRespondingCASInDestinationSite, ErrorQuotaExceeded, ErrorTimeoutExpired, RateLimitError, UnauthorizedError, ) class EWSService(metaclass=abc.ABCMeta): """Base class for all EWS services.""" SERVICE_NAME = None # The name of the SOAP service element_container_name = None # The name of the XML element wrapping the collection of returned items paging_container_name = None # The name of the element that contains paging information and the paged results returns_elements = True # If False, the service does not return response elements, just the RsponseCode status # Return exception instance instead of raising exceptions for the following errors when contained in an element ERRORS_TO_CATCH_IN_RESPONSE = ( EWSWarning, ErrorCannotDeleteObject, ErrorInvalidChangeKey, ErrorItemNotFound, ErrorItemSave, ErrorInvalidIdMalformed, ErrorMessageSizeExceeded, ErrorCannotDeleteTaskOccurrence, ErrorMimeContentConversionFailed, ErrorRecurrenceHasNoOccurrence, ErrorCorruptData ) # Similarly, define the warnings we want to return unraised WARNINGS_TO_CATCH_IN_RESPONSE = ErrorBatchProcessingStopped # Define the warnings we want to ignore, to let response processing proceed WARNINGS_TO_IGNORE_IN_RESPONSE = () # The exception type to raise when all attempted API versions failed NO_VALID_SERVER_VERSIONS = ErrorInvalidServerVersion # Marks the version from which the service was introduced supported_from = None # Marks services that support paging of requested items supports_paging = False # Marks services that need affinity to the backend server prefer_affinity = False def __init__(self, protocol, chunk_size=None, timeout=None): self.chunk_size = chunk_size or CHUNK_SIZE # The number of items to send in a single request if not isinstance(self.chunk_size, int): raise ValueError("'chunk_size' %r must be an integer" % chunk_size) if self.chunk_size < 1: raise ValueError("'chunk_size' must be a positive number") if self.supported_from and protocol.version.build < self.supported_from: raise NotImplementedError( '%r is only supported on %r and later' % (self.SERVICE_NAME, self.supported_from.fullname()) ) self.protocol = protocol # Allow a service to override the default protocol timeout. Useful for streaming services self.timeout = timeout # Controls whether the HTTP request should be streaming or fetch everything at once self.streaming = False # Streaming connection variables self._streaming_session = None self._streaming_response = None # The following two methods are the minimum required to be implemented by subclasses, but the name and number of # kwargs differs between services. Therefore, we cannot make these methods abstract. # @abc.abstractmethod # def call(self, **kwargs): # """Defines the arguments required by the service. Arguments are basic Python types or EWSElement objects. # Returns either XML objects or EWSElement objects. # """" # pass # @abc.abstractmethod # def get_payload(self, **kwargs): # """Using the arguments from .call(), return the payload expected by the service, as an XML object. The XML # object should consist of a SERVICE_NAME element and everything within that. # """ # pass def get(self, expect_result=True, **kwargs): """Like .call(), but expects exactly one result from the server, or zero when expect_result=False, or either zero or one when expect_result=None. Returns either one object or None. :param expect_result: None, True, or False :param kwargs: Same as arguments for .call() :return: Same as .call(), but returns either None or exactly one item """ res = list(self.call(**kwargs)) # Raise any errors for r in res: if isinstance(r, Exception): raise r if expect_result is None and not res: # Allow empty result return None if expect_result is False: if res: raise ValueError('Expected result length 0, but got %r' % res) return None if len(res) != 1: raise ValueError('Expected result length 1, but got %r' % res) return res[0] def parse(self, xml): """Used mostly for testing, when we want to parse static XML data.""" resp = DummyResponse(url=None, headers=None, request_headers=None, content=xml) _, body = self._get_soap_parts(response=resp) return self._elems_to_objs(self._get_elements_in_response(response=self._get_soap_messages(body=body))) def _elems_to_objs(self, elems): """Takes a generator of XML elements and exceptions. Returns the equivalent Python objects (or exceptions).""" raise NotImplementedError() @property def _version_hint(self): # We may be here due to version guessing in Protocol.version, so we can't use the self.protocol.version property return self.protocol.config.version @_version_hint.setter def _version_hint(self, value): self.protocol.config.version = value def _extra_headers(self, session): headers = {} if self.prefer_affinity: headers['X-PreferServerAffinity'] = 'True' for cookie in session.cookies: if cookie.name == 'X-BackEndCookie': headers['X-BackEndOverrideCookie'] = cookie.value return headers @property def _account_to_impersonate(self): if isinstance(self.protocol.credentials, OAuth2Credentials): return self.protocol.credentials.identity return None @property def _timezone(self): return None def _response_generator(self, payload): """Send the payload to the server, and return the response. :param payload: payload as an XML object :return: the response, as XML objects """ response = self._get_response_xml(payload=payload) if self.supports_paging: return (self._get_page(message) for message in response) return self._get_elements_in_response(response=response) def _chunked_get_elements(self, payload_func, items, **kwargs): """Yield elements in a response. Like ._get_elements(), but chop items into suitable chunks and send multiple requests. :param payload_func: A reference to .payload() :param items: An iterable of items (messages, folders, etc.) to process :param kwargs: Same as arguments for .call(), except for the 'items' argument :return: Same as ._get_elements() """ for i, chunk in enumerate(chunkify(items, self.chunk_size), start=1): log.debug('Processing chunk %s containing %s items', i, len(chunk)) yield from self._get_elements(payload=payload_func(chunk, **kwargs)) def stop_streaming(self): if self._streaming_response: self._streaming_response.close() # Release memory self._streaming_response = None if self._streaming_session: self.protocol.release_session(self._streaming_session) self._streaming_session = None def _get_elements(self, payload): """Send the payload to be sent and parsed. Handles and re-raise exceptions that are not meant to be returned to the caller as exception objects. Retry the request according to the retry policy. """ while True: try: # Create a generator over the response elements so exceptions in response elements are also raised # here and can be handled. yield from self._response_generator(payload=payload) return except ErrorServerBusy as e: self._handle_backoff(e) continue except KNOWN_EXCEPTIONS: # These are known and understood, and don't require a backtrace. raise except (ErrorTooManyObjectsOpened, ErrorTimeoutExpired) as e: # ErrorTooManyObjectsOpened means there are too many connections to the Exchange database. This is very # often a symptom of sending too many requests. # # ErrorTimeoutExpired can be caused by a busy server, or by overly large requests. Start by lowering the # session count. This is done by downstream code. if isinstance(e, ErrorTimeoutExpired) and self.protocol.session_pool_size <= 1: # We're already as low as we can go, so downstream cannot limit the session count to put less load # on the server. We don't have a way of lowering the page size of requests from # this part of the code yet. Let the user handle this. raise e # Re-raise as an ErrorServerBusy with a default delay of 5 minutes raise ErrorServerBusy('Reraised from %s(%s)' % (e.__class__.__name__, e)) except Exception: # This may run from a thread pool, which obfuscates the stack trace. Print trace immediately. account = self.account if isinstance(self, EWSAccountService) else None log.warning('Account %s: Exception in _get_elements: %s', account, traceback.format_exc(20)) raise finally: if self.streaming: self.stop_streaming() def _get_response(self, payload, api_version): """Send the actual HTTP request and get the response.""" session = self.protocol.get_session() self._streaming_session, self._streaming_response = None, None r, session = post_ratelimited( protocol=self.protocol, session=session, url=self.protocol.service_endpoint, headers=self._extra_headers(session), data=wrap( content=payload, api_version=api_version, account_to_impersonate=self._account_to_impersonate, timezone=self._timezone, ), allow_redirects=False, stream=self.streaming, timeout=self.timeout or self.protocol.TIMEOUT, ) if self.streaming: # We con only release the session when we have fully consumed the response. Save session and response # objects for later. self._streaming_session, self._streaming_response = session, r else: self.protocol.release_session(session) return r @property def _api_versions_to_try(self): # Put the hint first in the list, and then all other versions except the hint, from newest to oldest return [self._version_hint.api_version] + [v for v in API_VERSIONS if v != self._version_hint.api_version] def _get_response_xml(self, payload, **parse_opts): """Send the payload to the server and return relevant elements from the result. Several things happen here: * The payload is wrapped in SOAP headers and sent to the server * The Exchange API version is negotiated and stored in the protocol object * Connection errors are handled and possibly reraised as ErrorServerBusy * SOAP errors are raised * EWS errors are raised, or passed on to the caller :param payload: The request payload, as an XML object :return: A generator of XML objects or None if the service does not return a result """ # Microsoft really doesn't want to make our lives easy. The server may report one version in our initial version # guessing tango, but then the server may decide that any arbitrary legacy backend server may actually process # the request for an account. Prepare to handle version-related errors and set the server version per-account. log.debug('Calling service %s', self.SERVICE_NAME) for api_version in self._api_versions_to_try: log.debug('Trying API version %s', api_version) r = self._get_response(payload=payload, api_version=api_version) if self.streaming: # Let 'requests' decode raw data automatically r.raw.decode_content = True try: header, body = self._get_soap_parts(response=r, **parse_opts) except Exception: r.close() # Release memory raise # The body may contain error messages from Exchange, but we still want to collect version info if header is not None: self._update_api_version(api_version=api_version, header=header, **parse_opts) try: return self._get_soap_messages(body=body, **parse_opts) except (ErrorInvalidServerVersion, ErrorIncorrectSchemaVersion, ErrorInvalidRequest, ErrorInvalidSchemaVersionForMailboxVersion): # The guessed server version is wrong. Try the next version log.debug('API version %s was invalid', api_version) continue except ErrorExceededConnectionCount as e: # This indicates that the connecting user has too many open TCP connections to the server. Decrease # our session pool size. try: self.protocol.decrease_poolsize() continue except SessionPoolMinSizeReached: # We're already as low as we can go. Let the user handle this. raise e finally: if not self.streaming: # In streaming mode, we may not have accessed the raw stream yet. Caller must handle this. r.close() # Release memory raise self.NO_VALID_SERVER_VERSIONS('Tried versions %s but all were invalid' % self._api_versions_to_try) def _handle_backoff(self, e): """Take a request from the server to back off and checks the retry policy for what to do. Re-raise the exception if conditions are not met. :param e: An ErrorServerBusy instance :return: """ log.debug('Got ErrorServerBusy (back off %s seconds)', e.back_off) # ErrorServerBusy is very often a symptom of sending too many requests. Scale back connections if possible. try: self.protocol.decrease_poolsize() except SessionPoolMinSizeReached: pass if self.protocol.retry_policy.fail_fast: raise e self.protocol.retry_policy.back_off(e.back_off) # We'll warn about this later if we actually need to sleep def _update_api_version(self, api_version, header, **parse_opts): """Parse the server version contained in SOAP headers and update the version hint stored by the caller, if necessary. """ try: head_version = Version.from_soap_header(requested_api_version=api_version, header=header) except TransportError as te: log.debug('Failed to update version info (%s)', te) return if self._version_hint == head_version: # Nothing to do return log.debug('Found new version (%s -> %s)', self._version_hint, head_version) # The api_version that worked was different than our hint, or we never got a build version. Store the working # version. self._version_hint = head_version @classmethod def _response_tag(cls): """Return the name of the element containing the service response.""" return '{%s}%sResponse' % (MNS, cls.SERVICE_NAME) @staticmethod def _response_messages_tag(): """Return the name of the element containing service response messages.""" return '{%s}ResponseMessages' % MNS @classmethod def _response_message_tag(cls): """Return the name of the element of a single response message.""" return '{%s}%sResponseMessage' % (MNS, cls.SERVICE_NAME) @classmethod def _get_soap_parts(cls, response, **parse_opts): """Split the SOAP response into its headers an body elements.""" try: root = to_xml(response.iter_content()) except ParseError as e: raise SOAPError('Bad SOAP response: %s' % e) header = root.find('{%s}Header' % SOAPNS) if header is None: # This is normal when the response contains SOAP-level errors log.debug('No header in XML response') body = root.find('{%s}Body' % SOAPNS) if body is None: raise MalformedResponseError('No Body element in SOAP response') return header, body def _get_soap_messages(self, body, **parse_opts): """Return the elements in the response containing the response messages. Raises any SOAP exceptions.""" response = body.find(self._response_tag()) if response is None: fault = body.find('{%s}Fault' % SOAPNS) if fault is None: raise SOAPError( 'Unknown SOAP response (expected %s or Fault): %s' % (self._response_tag(), xml_to_str(body)) ) self._raise_soap_errors(fault=fault) # Will throw SOAPError or custom EWS error response_messages = response.find(self._response_messages_tag()) if response_messages is None: # Result isn't delivered in a list of FooResponseMessages, but directly in the FooResponse. Consumers expect # a list, so return a list return [response] return response_messages.findall(self._response_message_tag()) @classmethod def _raise_soap_errors(cls, fault): """Parse error messages contained in SOAP headers and raise as exceptions defined in this package.""" # Fault: See http://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383507 faultcode = get_xml_attr(fault, 'faultcode') faultstring = get_xml_attr(fault, 'faultstring') faultactor = get_xml_attr(fault, 'faultactor') detail = fault.find('detail') if detail is not None: code, msg = None, '' if detail.find('{%s}ResponseCode' % ENS) is not None: code = get_xml_attr(detail, '{%s}ResponseCode' % ENS).strip() if detail.find('{%s}Message' % ENS) is not None: msg = get_xml_attr(detail, '{%s}Message' % ENS).strip() msg_xml = detail.find('{%s}MessageXml' % TNS) # Crazy. Here, it's in the TNS namespace if code == 'ErrorServerBusy': back_off = None try: value = msg_xml.find('{%s}Value' % TNS) if value.get('Name') == 'BackOffMilliseconds': back_off = int(value.text) / 1000.0 # Convert to seconds except (TypeError, AttributeError): pass raise ErrorServerBusy(msg, back_off=back_off) if code == 'ErrorSchemaValidation' and msg_xml is not None: violation = get_xml_attr(msg_xml, '{%s}Violation' % TNS) if violation is not None: msg = '%s %s' % (msg, violation) try: raise vars(errors)[code](msg) except KeyError: detail = '%s: code: %s msg: %s (%s)' % (cls.SERVICE_NAME, code, msg, xml_to_str(detail)) try: raise vars(errors)[faultcode](faultstring) except KeyError: pass raise SOAPError('SOAP error code: %s string: %s actor: %s detail: %s' % ( faultcode, faultstring, faultactor, detail)) def _get_element_container(self, message, name=None): """Return the XML element in a response element that contains the elements we want the service to return. For example, in a GetFolder response, 'message' is the GetFolderResponseMessage element, and we return the 'Folders' element: NoError [...] Some service responses don't have a containing element for the returned elements ('name' is None). In that case, we return the 'SomeServiceResponseMessage' element. If the response contains a warning or an error message, we raise the relevant exception, unless the error class is contained in WARNINGS_TO_CATCH_IN_RESPONSE or ERRORS_TO_CATCH_IN_RESPONSE, in which case we return the exception instance. """ # ResponseClass is an XML attribute of various SomeServiceResponseMessage elements: Possible values are: # Success, Warning, Error. See e.g. # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/finditemresponsemessage response_class = message.get('ResponseClass') # ResponseCode, MessageText: See # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/responsecode response_code = get_xml_attr(message, '{%s}ResponseCode' % MNS) if response_class == 'Success' and response_code == 'NoError': if not name: return message container = message.find(name) if container is None: raise MalformedResponseError('No %s elements in ResponseMessage (%s)' % (name, xml_to_str(message))) return container if response_code == 'NoError': return True # Raise any non-acceptable errors in the container, or return the container or the acceptable exception instance msg_text = get_xml_attr(message, '{%s}MessageText' % MNS) msg_xml = message.find('{%s}MessageXml' % MNS) if response_class == 'Warning': try: raise self._get_exception(code=response_code, text=msg_text, msg_xml=msg_xml) except self.WARNINGS_TO_CATCH_IN_RESPONSE as e: return e except self.WARNINGS_TO_IGNORE_IN_RESPONSE as e: log.warning(str(e)) container = message.find(name) if container is None: raise MalformedResponseError('No %s elements in ResponseMessage (%s)' % (name, xml_to_str(message))) return container # rspclass == 'Error', or 'Success' and not 'NoError' try: raise self._get_exception(code=response_code, text=msg_text, msg_xml=msg_xml) except self.ERRORS_TO_CATCH_IN_RESPONSE as e: return e @staticmethod def _get_exception(code, text, msg_xml): """Parse error messages contained in EWS responses and raise as exceptions defined in this package.""" if not code: return TransportError('Empty ResponseCode in ResponseMessage (MessageText: %s, MessageXml: %s)' % ( text, msg_xml)) if msg_xml is not None: # If this is an ErrorInvalidPropertyRequest error, the xml may contain a specific FieldURI for elem_cls in (FieldURI, IndexedFieldURI, ExtendedFieldURI, ExceptionFieldURI): elem = msg_xml.find(elem_cls.response_tag()) if elem is not None: field_uri = elem_cls.from_xml(elem, account=None) text += ' (field: %s)' % field_uri break # If this is an ErrorInvalidValueForProperty error, the xml may contain the name and value of the property if code == 'ErrorInvalidValueForProperty': msg_parts = {} for elem in msg_xml.findall('{%s}Value' % TNS): key, val = elem.get('Name'), elem.text if key: msg_parts[key] = val if msg_parts: text += ' (%s)' % ', '.join('%s: %s' % (k, v) for k, v in msg_parts.items()) # If this is an ErrorInternalServerError error, the xml may contain a more specific error code inner_code, inner_text = None, None for value_elem in msg_xml.findall('{%s}Value' % TNS): name = value_elem.get('Name') if name == 'InnerErrorResponseCode': inner_code = value_elem.text elif name == 'InnerErrorMessageText': inner_text = value_elem.text if inner_code: try: # Raise the error as the inner error code return vars(errors)[inner_code]('%s (raised from: %s(%r))' % (inner_text, code, text)) except KeyError: # Inner code is unknown to us. Just append to the original text text += ' (inner error: %s(%r))' % (inner_code, inner_text) try: # Raise the error corresponding to the ResponseCode return vars(errors)[code](text) except KeyError: # Should not happen return TransportError('Unknown ResponseCode in ResponseMessage: %s (MessageText: %s, MessageXml: %s)' % ( code, text, msg_xml)) def _get_elements_in_response(self, response): """Take a list of 'SomeServiceResponseMessage' elements and return the elements in each response message that we want the service to return. With e.g. 'CreateItem', we get a list of 'CreateItemResponseMessage' elements and return the 'Message' elements. NoError NoError :param response: a list of 'SomeServiceResponseMessage' XML objects :return: a generator of items as returned by '_get_elements_in_container() """ for msg in response: container_or_exc = self._get_element_container(message=msg, name=self.element_container_name) if isinstance(container_or_exc, (bool, Exception)): yield container_or_exc else: for c in self._get_elements_in_container(container=container_or_exc): yield c @classmethod def _get_elements_in_container(cls, container): """Return a list of response elements from an XML response element container. With e.g. 'CreateItem', 'Items' is the container element and we return the 'Message' child elements: If the service does not return response elements, return True to indicate the status. Errors have already been raised. """ if cls.returns_elements: return list(container) return [True] def _get_elems_from_page(self, elem, max_items, total_item_count): container = elem.find(self.element_container_name) if container is None: raise MalformedResponseError('No %s elements in ResponseMessage (%s)' % ( self.element_container_name, xml_to_str(elem))) for e in self._get_elements_in_container(container=container): if max_items and total_item_count >= max_items: # No need to continue. Break out of elements loop log.debug("'max_items' count reached (elements)") break yield e def _get_pages(self, payload_func, kwargs, expected_message_count): """Request a page, or a list of pages if multiple collections are pages in a single request. Return each page. """ payload = payload_func(**kwargs) page_elems = list(self._get_elements(payload=payload)) if len(page_elems) != expected_message_count: raise MalformedResponseError( "Expected %s items in 'response', got %s" % (expected_message_count, len(page_elems)) ) return page_elems @staticmethod def _get_next_offset(paging_infos): next_offsets = {p['next_offset'] for p in paging_infos if p['next_offset'] is not None} if not next_offsets: # Paging is done for all messages return None # We cannot guarantee that all messages that have a next_offset also have the *same* next_offset. This is # because the collections that we are iterating may change while iterating. We'll do our best but we cannot # guarantee 100% consistency when large collections are simultaneously being changed on the server. # # It's not possible to supply a per-folder offset when iterating multiple folders, so we'll just have to # choose something that is most likely to work. Select the lowest of all the values to at least make sure # we don't miss any items, although we may then get duplicates ¯\_(ツ)_/¯ if len(next_offsets) > 1: log.warning('Inconsistent next_offset values: %r. Using lowest value', next_offsets) return min(next_offsets) def _paged_call(self, payload_func, max_items, folders, **kwargs): """Call a service that supports paging requests. Return a generator over all response items. Keeps track of all paging-related counters. """ paging_infos = {f: dict(item_count=0, next_offset=None) for f in folders} common_next_offset = kwargs['offset'] total_item_count = 0 while True: if not paging_infos: # Paging is done for all folders break log.debug('Getting page at offset %s (max_items %s)', common_next_offset, max_items) kwargs['offset'] = common_next_offset kwargs['folders'] = paging_infos.keys() # Only request the paging of the remaining folders. pages = self._get_pages(payload_func, kwargs, len(paging_infos)) for (page, next_offset), (f, paging_info) in zip(pages, list(paging_infos.items())): paging_info['next_offset'] = next_offset if isinstance(page, Exception): # Assume this folder no longer works. Don't attempt to page it again. log.debug('Exception occurred for folder %s. Removing.', f) del paging_infos[f] yield page continue if page is not None: for elem in self._get_elems_from_page(page, max_items, total_item_count): paging_info['item_count'] += 1 total_item_count += 1 yield elem if max_items and total_item_count >= max_items: # No need to continue. Break out of inner loop log.debug("'max_items' count reached (inner)") break if not paging_info['next_offset']: # Paging is done for this folder. Don't attempt to page it again. log.debug('Paging has completed for folder %s. Removing.', f) del paging_infos[f] continue log.debug('Folder %s still has items', f) # Check sanity of paging offsets, but don't fail. When we are iterating huge collections that take a # long time to complete, the collection may change while we are iterating. This can affect the # 'next_offset' value and make it inconsistent with the number of already collected items. # We may have a mismatch if we stopped early due to reaching 'max_items'. if paging_info['next_offset'] != paging_info['item_count'] and ( not max_items or total_item_count < max_items ): log.warning('Unexpected next offset: %s -> %s. Maybe the server-side collection has changed?', paging_info['item_count'], paging_info['next_offset']) # Also break out of outer loop if max_items and total_item_count >= max_items: log.debug("'max_items' count reached (outer)") break common_next_offset = self._get_next_offset(paging_infos.values()) if common_next_offset is None: # Paging is done for all folders break @staticmethod def _get_paging_values(elem): """Read paging information from the paging container element.""" offset_attr = elem.get('IndexedPagingOffset') next_offset = None if offset_attr is None else int(offset_attr) item_count = int(elem.get('TotalItemsInView')) is_last_page = elem.get('IncludesLastItemInRange').lower() in ('true', '0') log.debug('Got page with offset %s, item_count %s, last_page %s', next_offset, item_count, is_last_page) # Clean up contradictory paging values if next_offset is None and not is_last_page: log.debug("Not last page in range, but server didn't send a page offset. Assuming first page") next_offset = 1 if next_offset is not None and is_last_page: if next_offset != item_count: log.debug("Last page in range, but we still got an offset. Assuming paging has completed") next_offset = None if not item_count and not is_last_page: log.debug("Not last page in range, but also no items left. Assuming paging has completed") next_offset = None if item_count and next_offset == 0: log.debug("Non-zero offset, but also no items left. Assuming paging has completed") next_offset = None return item_count, next_offset def _get_page(self, message): """Get a single page from a request message, and return the container and next offset.""" paging_elem = self._get_element_container(message=message, name=self.paging_container_name) if isinstance(paging_elem, Exception): return paging_elem, None item_count, next_offset = self._get_paging_values(paging_elem) if not item_count: paging_elem = None return paging_elem, next_offset class EWSAccountService(EWSService, metaclass=abc.ABCMeta): """Base class for services that act on items concerning a single Mailbox on the server.""" NO_VALID_SERVER_VERSIONS = ErrorInvalidSchemaVersionForMailboxVersion def __init__(self, *args, **kwargs): self.account = kwargs.pop('account') kwargs['protocol'] = self.account.protocol super().__init__(*args, **kwargs) @property def _version_hint(self): return self.account.version @_version_hint.setter def _version_hint(self, value): self.account.version = value def _extra_headers(self, *args, **kwargs): headers = super()._extra_headers(*args, **kwargs) # See # https://blogs.msdn.microsoft.com/webdav_101/2015/05/11/best-practices-ews-authentication-and-access-issues/ headers['X-AnchorMailbox'] = self.account.primary_smtp_address return headers @property def _account_to_impersonate(self): if self.account.access_type == IMPERSONATION: return self.account.identity return None @property def _timezone(self): return self.account.default_timezone def to_item_id(item, item_cls, version): # Coerce a tuple, dict or object to an 'item_cls' instance. Used to create [Parent][Item|Folder]Id instances from a # variety of input. if isinstance(item, item_cls): # Allow any subclass of item_cls, e.g. OccurrenceItemId when ItemId is passed return item from ..folders import BaseFolder from ..items import BaseItem if isinstance(item, (BaseFolder, BaseItem)): return item.to_id_xml(version=version) if isinstance(item, (tuple, list)): return item_cls(*item) if isinstance(item, dict): return item_cls(**item) return item_cls(item.id, item.changekey) def create_shape_element(tag, shape, additional_fields, version): shape_elem = create_element(tag) add_xml_child(shape_elem, 't:BaseShape', shape) if additional_fields: additional_properties = create_element('t:AdditionalProperties') expanded_fields = chain(*(f.expand(version=version) for f in additional_fields)) # 'path' is insufficient to consistently sort additional properties. For example, we have both # 'contacts:Companies' and 'task:Companies' with path 'companies'. Sort by both 'field_uri' and 'path'. # Extended properties do not have a 'field_uri' value. set_xml_value(additional_properties, sorted( expanded_fields, key=lambda f: (getattr(f.field, 'field_uri', ''), f.path) ), version=version) shape_elem.append(additional_properties) return shape_elem def create_folder_ids_element(tag, folders, version): from ..folders import FolderId folder_ids = create_element(tag) for folder in folders: if not isinstance(folder, FolderId): folder = to_item_id(folder, FolderId, version=version) set_xml_value(folder_ids, folder, version=version) if not len(folder_ids): raise ValueError('"folders" must not be empty') return folder_ids def create_item_ids_element(items, version, tag='m:ItemIds'): item_ids = create_element(tag) for item in items: set_xml_value(item_ids, to_item_id(item, ItemId, version=version), version=version) if not len(item_ids): raise ValueError('"items" must not be empty') return item_ids def create_attachment_ids_element(items, version): from ..attachments import AttachmentId attachment_ids = create_element('m:AttachmentIds') for item in items: attachment_id = item if isinstance(item, AttachmentId) else AttachmentId(id=item) set_xml_value(attachment_ids, attachment_id, version=version) if not len(attachment_ids): raise ValueError('"items" must not be empty') return attachment_ids def parse_folder_elem(elem, folder, account): from ..folders import BaseFolder, Folder, DistinguishedFolderId, RootOfHierarchy if isinstance(elem, Exception): return elem if isinstance(folder, RootOfHierarchy): f = folder.from_xml(elem=elem, account=folder.account) elif isinstance(folder, Folder): f = folder.from_xml_with_root(elem=elem, root=folder.root) elif isinstance(folder, DistinguishedFolderId): # We don't know the root, so assume account.root. folder_cls = None for cls in account.root.WELLKNOWN_FOLDERS: if cls.DISTINGUISHED_FOLDER_ID == folder.id: folder_cls = cls break if not folder_cls: raise ValueError('Unknown distinguished folder ID: %s' % folder.id) f = folder_cls.from_xml_with_root(elem=elem, root=account.root) else: # 'folder' is a generic FolderId instance. We don't know the root so assume account.root. f = Folder.from_xml_with_root(elem=elem, root=account.root) if isinstance(folder, DistinguishedFolderId): f.is_distinguished = True elif isinstance(folder, BaseFolder) and folder.is_distinguished: f.is_distinguished = True return f exchangelib-4.6.1/exchangelib/services/convert_id.py000066400000000000000000000045611414601472700226110ustar00rootroot00000000000000from .common import EWSService from ..properties import AlternateId, AlternatePublicFolderId, AlternatePublicFolderItemId, ID_FORMATS from ..util import create_element, set_xml_value from ..version import EXCHANGE_2007_SP1 class ConvertId(EWSService): """Take a list of IDs to convert. Returns a list of converted IDs or exception instances, in the same order as the input list. MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/convertid-operation """ SERVICE_NAME = 'ConvertId' supported_from = EXCHANGE_2007_SP1 def call(self, items, destination_format): if destination_format not in ID_FORMATS: raise ValueError("'destination_format' %r must be one of %s" % (destination_format, ID_FORMATS)) return self._elems_to_objs( self._chunked_get_elements(self.get_payload, items=items, destination_format=destination_format) ) def _elems_to_objs(self, elems): cls_map = {cls.response_tag(): cls for cls in ( AlternateId, AlternatePublicFolderId, AlternatePublicFolderItemId )} for elem in elems: if isinstance(elem, Exception): yield elem continue yield cls_map[elem.tag].from_xml(elem, account=None) def get_payload(self, items, destination_format): supported_item_classes = AlternateId, AlternatePublicFolderId, AlternatePublicFolderItemId convertid = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(DestinationFormat=destination_format)) item_ids = create_element('m:SourceIds') for item in items: if not isinstance(item, supported_item_classes): raise ValueError("'item' value %r must be an instance of %r" % (item, supported_item_classes)) set_xml_value(item_ids, item, version=self.protocol.version) if not len(item_ids): raise ValueError('"items" must not be empty') convertid.append(item_ids) return convertid @classmethod def _get_elements_in_container(cls, container): # We may have other elements in here, e.g. 'ResponseCode'. Filter away those. return container.findall(AlternateId.response_tag()) \ + container.findall(AlternatePublicFolderId.response_tag()) \ + container.findall(AlternatePublicFolderItemId.response_tag()) exchangelib-4.6.1/exchangelib/services/copy_item.py000066400000000000000000000003211414601472700224330ustar00rootroot00000000000000from . import move_item class CopyItem(move_item.MoveItem): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/copyitem-operation""" SERVICE_NAME = 'CopyItem' exchangelib-4.6.1/exchangelib/services/create_attachment.py000066400000000000000000000033721414601472700241270ustar00rootroot00000000000000from .common import EWSAccountService, to_item_id from ..properties import ParentItemId from ..util import create_element, set_xml_value, MNS class CreateAttachment(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createattachment-operation """ SERVICE_NAME = 'CreateAttachment' element_container_name = '{%s}Attachments' % MNS def call(self, parent_item, items): return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, parent_item=parent_item)) def _elems_to_objs(self, elems): from ..attachments import FileAttachment, ItemAttachment cls_map = {cls.response_tag(): cls for cls in (FileAttachment, ItemAttachment)} for elem in elems: if isinstance(elem, Exception): yield elem continue yield cls_map[elem.tag].from_xml(elem=elem, account=self.account) def get_payload(self, items, parent_item): from ..items import BaseItem payload = create_element('m:%s' % self.SERVICE_NAME) version = self.account.version if isinstance(parent_item, BaseItem): # to_item_id() would convert this to a normal ItemId, but the service wants a ParentItemId parent_item = ParentItemId(parent_item.id, parent_item.changekey) set_xml_value(payload, to_item_id(parent_item, ParentItemId, version=version), version=version) attachments = create_element('m:Attachments') for item in items: set_xml_value(attachments, item, version=self.account.version) if not len(attachments): raise ValueError('"items" must not be empty') payload.append(attachments) return payload exchangelib-4.6.1/exchangelib/services/create_folder.py000066400000000000000000000034661414601472700232560ustar00rootroot00000000000000from .common import EWSAccountService, parse_folder_elem, create_folder_ids_element from ..util import create_element, set_xml_value, MNS class CreateFolder(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createfolder-operation""" SERVICE_NAME = 'CreateFolder' element_container_name = '{%s}Folders' % MNS def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.folders = [] # A hack to communicate parsing args to _elems_to_objs() def call(self, parent_folder, folders): # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same # class as the folder instance it was requested with. self.folders = list(folders) # Convert to a list, in case 'folders' is a generator. We're iterating twice. return self._elems_to_objs(self._chunked_get_elements( self.get_payload, items=self.folders, parent_folder=parent_folder, )) def _elems_to_objs(self, elems): for folder, elem in zip(self.folders, elems): if isinstance(elem, Exception): yield elem continue yield parse_folder_elem(elem=elem, folder=folder, account=self.account) def get_payload(self, folders, parent_folder): create_folder = create_element('m:%s' % self.SERVICE_NAME) parentfolderid = create_element('m:ParentFolderId') set_xml_value(parentfolderid, parent_folder, version=self.account.version) set_xml_value(create_folder, parentfolderid, version=self.account.version) folder_ids = create_folder_ids_element(tag='m:Folders', folders=folders, version=self.account.version) create_folder.append(folder_ids) return create_folder exchangelib-4.6.1/exchangelib/services/create_item.py000066400000000000000000000110771414601472700227360ustar00rootroot00000000000000from collections import OrderedDict from .common import EWSAccountService from ..util import create_element, set_xml_value, MNS class CreateItem(EWSAccountService): """Take a folder and a list of items. Return the result of creation as a list of tuples (success[True|False], errormessage), in the same order as the input list. MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation """ SERVICE_NAME = 'CreateItem' element_container_name = '{%s}Items' % MNS def call(self, items, folder, message_disposition, send_meeting_invitations): from ..folders import BaseFolder, FolderId from ..items import SAVE_ONLY, SEND_AND_SAVE_COPY, SEND_ONLY, \ SEND_MEETING_INVITATIONS_CHOICES, MESSAGE_DISPOSITION_CHOICES if message_disposition not in MESSAGE_DISPOSITION_CHOICES: raise ValueError("'message_disposition' %s must be one of %s" % ( message_disposition, MESSAGE_DISPOSITION_CHOICES )) if send_meeting_invitations not in SEND_MEETING_INVITATIONS_CHOICES: raise ValueError("'send_meeting_invitations' %s must be one of %s" % ( send_meeting_invitations, SEND_MEETING_INVITATIONS_CHOICES )) if folder is not None: if not isinstance(folder, (BaseFolder, FolderId)): raise ValueError("'folder' %r must be a Folder or FolderId instance" % folder) if folder.account != self.account: raise ValueError('"Folder must belong to this account') if message_disposition == SAVE_ONLY and folder is None: raise AttributeError("Folder must be supplied when in save-only mode") if message_disposition == SEND_AND_SAVE_COPY and folder is None: folder = self.account.sent # 'Sent' is default EWS behaviour if message_disposition == SEND_ONLY and folder is not None: raise AttributeError("Folder must be None in send-ony mode") return self._elems_to_objs(self._chunked_get_elements( self.get_payload, items=items, folder=folder, message_disposition=message_disposition, send_meeting_invitations=send_meeting_invitations, )) def _elems_to_objs(self, elems): from ..items import BulkCreateResult for elem in elems: if isinstance(elem, (Exception, type(None))): yield elem continue if isinstance(elem, bool): yield elem continue yield BulkCreateResult.from_xml(elem=elem, account=self.account) @classmethod def _get_elements_in_container(cls, container): res = super()._get_elements_in_container(container) return res or [True] def get_payload(self, items, folder, message_disposition, send_meeting_invitations): """Take a list of Item objects (CalendarItem, Message etc) and return the XML for a CreateItem request. convert items to XML Elements. MessageDisposition is only applicable to email messages, where it is required. SendMeetingInvitations is required for calendar items. It is also applicable to tasks, meeting request responses (see https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation-meeting-request ) and sharing invitation accepts (see https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-acceptsharinginvitation ). The last two are not supported yet. :param items: :param folder: :param message_disposition: :param send_meeting_invitations: """ createitem = create_element( 'm:%s' % self.SERVICE_NAME, attrs=OrderedDict([ ('MessageDisposition', message_disposition), ('SendMeetingInvitations', send_meeting_invitations), ]) ) if folder: saveditemfolderid = create_element('m:SavedItemFolderId') set_xml_value(saveditemfolderid, folder, version=self.account.version) createitem.append(saveditemfolderid) item_elems = create_element('m:Items') for item in items: if not item.account: item.account = self.account set_xml_value(item_elems, item, version=self.account.version) if not len(item_elems): raise ValueError('"items" must not be empty') createitem.append(item_elems) return createitem exchangelib-4.6.1/exchangelib/services/create_user_configuration.py000066400000000000000000000013641414601472700257030ustar00rootroot00000000000000from .common import EWSAccountService from ..util import create_element, set_xml_value class CreateUserConfiguration(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createuserconfiguration-operation """ SERVICE_NAME = 'CreateUserConfiguration' returns_elements = False def call(self, user_configuration): return self._get_elements(payload=self.get_payload(user_configuration=user_configuration)) def get_payload(self, user_configuration): createuserconfiguration = create_element('m:%s' % self.SERVICE_NAME) set_xml_value(createuserconfiguration, user_configuration, version=self.protocol.version) return createuserconfiguration exchangelib-4.6.1/exchangelib/services/delete_attachment.py000066400000000000000000000021101414601472700241130ustar00rootroot00000000000000from .common import EWSAccountService, create_attachment_ids_element from ..properties import RootItemId from ..util import create_element class DeleteAttachment(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteattachment-operation """ SERVICE_NAME = 'DeleteAttachment' def call(self, items): return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items)) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield RootItemId.from_xml(elem=elem, account=self.account) @classmethod def _get_elements_in_container(cls, container): return container.findall(RootItemId.response_tag()) def get_payload(self, items): payload = create_element('m:%s' % self.SERVICE_NAME) attachment_ids = create_attachment_ids_element(items=items, version=self.account.version) payload.append(attachment_ids) return payload exchangelib-4.6.1/exchangelib/services/delete_folder.py000066400000000000000000000014401414601472700232430ustar00rootroot00000000000000from .common import EWSAccountService, create_folder_ids_element from ..util import create_element class DeleteFolder(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deletefolder-operation""" SERVICE_NAME = 'DeleteFolder' returns_elements = False def call(self, folders, delete_type): return self._chunked_get_elements(self.get_payload, items=folders, delete_type=delete_type) def get_payload(self, folders, delete_type): deletefolder = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(DeleteType=delete_type)) folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version) deletefolder.append(folder_ids) return deletefolder exchangelib-4.6.1/exchangelib/services/delete_item.py000066400000000000000000000063521414601472700227350ustar00rootroot00000000000000from collections import OrderedDict from .common import EWSAccountService, create_item_ids_element from ..util import create_element from ..version import EXCHANGE_2013_SP1 class DeleteItem(EWSAccountService): """Take a folder and a list of (id, changekey) tuples. Return result of deletion as a list of tuples (success[True|False], errormessage), in the same order as the input list. MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteitem-operation """ SERVICE_NAME = 'DeleteItem' returns_elements = False def call(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts): from ..items import DELETE_TYPE_CHOICES, SEND_MEETING_CANCELLATIONS_CHOICES, AFFECTED_TASK_OCCURRENCES_CHOICES if delete_type not in DELETE_TYPE_CHOICES: raise ValueError("'delete_type' %s must be one of %s" % ( delete_type, DELETE_TYPE_CHOICES )) if send_meeting_cancellations not in SEND_MEETING_CANCELLATIONS_CHOICES: raise ValueError("'send_meeting_cancellations' %s must be one of %s" % ( send_meeting_cancellations, SEND_MEETING_CANCELLATIONS_CHOICES )) if affected_task_occurrences not in AFFECTED_TASK_OCCURRENCES_CHOICES: raise ValueError("'affected_task_occurrences' %s must be one of %s" % ( affected_task_occurrences, AFFECTED_TASK_OCCURRENCES_CHOICES )) if suppress_read_receipts not in (True, False): raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts) return self._chunked_get_elements( self.get_payload, items=items, delete_type=delete_type, send_meeting_cancellations=send_meeting_cancellations, affected_task_occurrences=affected_task_occurrences, suppress_read_receipts=suppress_read_receipts, ) def get_payload(self, items, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts): # Takes a list of (id, changekey) tuples or Item objects and returns the XML for a DeleteItem request. if self.account.version.build >= EXCHANGE_2013_SP1: deleteitem = create_element( 'm:%s' % self.SERVICE_NAME, attrs=OrderedDict([ ('DeleteType', delete_type), ('SendMeetingCancellations', send_meeting_cancellations), ('AffectedTaskOccurrences', affected_task_occurrences), ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'), ]) ) else: deleteitem = create_element( 'm:%s' % self.SERVICE_NAME, attrs=OrderedDict([ ('DeleteType', delete_type), ('SendMeetingCancellations', send_meeting_cancellations), ('AffectedTaskOccurrences', affected_task_occurrences), ]) ) item_ids = create_item_ids_element(items=items, version=self.account.version) deleteitem.append(item_ids) return deleteitem exchangelib-4.6.1/exchangelib/services/delete_user_configuration.py000066400000000000000000000014141414601472700256760ustar00rootroot00000000000000from .common import EWSAccountService from ..util import create_element, set_xml_value class DeleteUserConfiguration(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteuserconfiguration-operation """ SERVICE_NAME = 'DeleteUserConfiguration' returns_elements = False def call(self, user_configuration_name): return self._get_elements(payload=self.get_payload(user_configuration_name=user_configuration_name)) def get_payload(self, user_configuration_name): deleteuserconfiguration = create_element('m:%s' % self.SERVICE_NAME) set_xml_value(deleteuserconfiguration, user_configuration_name, version=self.account.version) return deleteuserconfiguration exchangelib-4.6.1/exchangelib/services/empty_folder.py000066400000000000000000000021041414601472700231350ustar00rootroot00000000000000from collections import OrderedDict from .common import EWSAccountService, create_folder_ids_element from ..util import create_element class EmptyFolder(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/emptyfolder-operation""" SERVICE_NAME = 'EmptyFolder' returns_elements = False def call(self, folders, delete_type, delete_sub_folders): return self._chunked_get_elements( self.get_payload, items=folders, delete_type=delete_type, delete_sub_folders=delete_sub_folders ) def get_payload(self, folders, delete_type, delete_sub_folders): emptyfolder = create_element( 'm:%s' % self.SERVICE_NAME, attrs=OrderedDict([ ('DeleteType', delete_type), ('DeleteSubFolders', 'true' if delete_sub_folders else 'false'), ]) ) folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version) emptyfolder.append(folder_ids) return emptyfolder exchangelib-4.6.1/exchangelib/services/expand_dl.py000066400000000000000000000020461414601472700224070ustar00rootroot00000000000000from .common import EWSService from ..errors import ErrorNameResolutionMultipleResults from ..properties import Mailbox from ..util import create_element, set_xml_value, MNS class ExpandDL(EWSService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/expanddl-operation""" SERVICE_NAME = 'ExpandDL' element_container_name = '{%s}DLExpansion' % MNS WARNINGS_TO_IGNORE_IN_RESPONSE = ErrorNameResolutionMultipleResults def call(self, distribution_list): return self._elems_to_objs(self._get_elements(payload=self.get_payload(distribution_list=distribution_list))) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield Mailbox.from_xml(elem, account=None) def get_payload(self, distribution_list): payload = create_element('m:%s' % self.SERVICE_NAME) set_xml_value(payload, distribution_list, version=self.protocol.version) return payload exchangelib-4.6.1/exchangelib/services/export_items.py000066400000000000000000000023541414601472700231750ustar00rootroot00000000000000from .common import EWSAccountService, create_item_ids_element from ..errors import ResponseMessageError from ..util import create_element, MNS class ExportItems(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/exportitems-operation""" ERRORS_TO_CATCH_IN_RESPONSE = ResponseMessageError SERVICE_NAME = 'ExportItems' element_container_name = '{%s}Data' % MNS def call(self, items): return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items)) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield elem.text # All we want is the 64bit string in the 'Data' tag def get_payload(self, items): exportitems = create_element('m:%s' % self.SERVICE_NAME) item_ids = create_item_ids_element(items=items, version=self.account.version) exportitems.append(item_ids) return exportitems # We need to override this since ExportItemsResponseMessage is formatted a # little bit differently. . @classmethod def _get_elements_in_container(cls, container): return [container] exchangelib-4.6.1/exchangelib/services/find_folder.py000066400000000000000000000070601414601472700227250ustar00rootroot00000000000000from collections import OrderedDict from .common import EWSAccountService, create_shape_element from ..util import create_element, set_xml_value, TNS, MNS from ..version import EXCHANGE_2010 class FindFolder(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/findfolder-operation""" SERVICE_NAME = 'FindFolder' element_container_name = '{%s}Folders' % TNS paging_container_name = '{%s}RootFolder' % MNS supports_paging = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.root = None # A hack to communicate parsing args to _elems_to_objs() def call(self, folders, additional_fields, restriction, shape, depth, max_items, offset): """Find subfolders of a folder. :param folders: the folders to act on :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects :param restriction: Restriction object that defines the filters for the query :param shape: The set of attributes to return :param depth: How deep in the folder structure to search for folders :param max_items: The maximum number of items to return :param offset: the offset relative to the first item in the item collection. Usually 0. :return: XML elements for the matching folders """ roots = {f.root for f in folders} if len(roots) != 1: raise ValueError('FindFolder must be called with folders in the same root hierarchy (%r)' % roots) self.root = roots.pop() return self._elems_to_objs(self._paged_call( payload_func=self.get_payload, max_items=max_items, folders=folders, **dict( additional_fields=additional_fields, restriction=restriction, shape=shape, depth=depth, page_size=self.chunk_size, offset=offset, ) )) def _elems_to_objs(self, elems): from ..folders import Folder for elem in elems: if isinstance(elem, Exception): yield elem continue yield Folder.from_xml_with_root(elem=elem, root=self.root) def get_payload(self, folders, additional_fields, restriction, shape, depth, page_size, offset=0): findfolder = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth)) foldershape = create_shape_element( tag='m:FolderShape', shape=shape, additional_fields=additional_fields, version=self.account.version ) findfolder.append(foldershape) if self.account.version.build >= EXCHANGE_2010: indexedpageviewitem = create_element( 'm:IndexedPageFolderView', attrs=OrderedDict([ ('MaxEntriesReturned', str(page_size)), ('Offset', str(offset)), ('BasePoint', 'Beginning'), ]) ) findfolder.append(indexedpageviewitem) else: if offset != 0: raise ValueError('Offsets are only supported from Exchange 2010') if restriction: findfolder.append(restriction.to_xml(version=self.account.version)) parentfolderids = create_element('m:ParentFolderIds') set_xml_value(parentfolderids, folders, version=self.account.version) findfolder.append(parentfolderids) return findfolder exchangelib-4.6.1/exchangelib/services/find_item.py000066400000000000000000000102501414601472700224030ustar00rootroot00000000000000from collections import OrderedDict from .common import EWSAccountService, create_shape_element from ..util import create_element, set_xml_value, TNS, MNS class FindItem(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/finditem-operation""" SERVICE_NAME = 'FindItem' element_container_name = '{%s}Items' % TNS paging_container_name = '{%s}RootFolder' % MNS supports_paging = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # A hack to communicate parsing args to _elems_to_objs() self.additional_fields = None self.shape = None def call(self, folders, additional_fields, restriction, order_fields, shape, query_string, depth, calendar_view, max_items, offset): """Find items in an account. :param folders: the folders to act on :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects :param restriction: a Restriction object for :param order_fields: the fields to sort the results by :param shape: The set of attributes to return :param query_string: a QueryString object :param depth: How deep in the folder structure to search for items :param calendar_view: If set, returns recurring calendar items unfolded :param max_items: the max number of items to return :param offset: the offset relative to the first item in the item collection. Usually 0. :return: XML elements for the matching items """ self.additional_fields = additional_fields self.shape = shape return self._elems_to_objs(self._paged_call( payload_func=self.get_payload, max_items=max_items, folders=folders, **dict( additional_fields=additional_fields, restriction=restriction, order_fields=order_fields, query_string=query_string, shape=shape, depth=depth, calendar_view=calendar_view, page_size=self.chunk_size, offset=offset, ) )) def _elems_to_objs(self, elems): from ..folders.base import BaseFolder from ..items import Item, ID_ONLY for elem in elems: if isinstance(elem, Exception): yield elem continue if self.shape == ID_ONLY and self.additional_fields is None: yield Item.id_from_xml(elem) continue yield BaseFolder.item_model_from_tag(elem.tag).from_xml(elem=elem, account=self.account) def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, calendar_view, page_size, offset=0): finditem = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth)) itemshape = create_shape_element( tag='m:ItemShape', shape=shape, additional_fields=additional_fields, version=self.account.version ) finditem.append(itemshape) if calendar_view is None: view_type = create_element( 'm:IndexedPageItemView', attrs=OrderedDict([ ('MaxEntriesReturned', str(page_size)), ('Offset', str(offset)), ('BasePoint', 'Beginning'), ]) ) else: view_type = calendar_view.to_xml(version=self.account.version) finditem.append(view_type) if restriction: finditem.append(restriction.to_xml(version=self.account.version)) if order_fields: finditem.append(set_xml_value( create_element('m:SortOrder'), order_fields, version=self.account.version )) finditem.append(set_xml_value( create_element('m:ParentFolderIds'), folders, version=self.account.version )) if query_string: finditem.append(query_string.to_xml(version=self.account.version)) return finditem exchangelib-4.6.1/exchangelib/services/find_people.py000066400000000000000000000114151414601472700227350ustar00rootroot00000000000000import logging from collections import OrderedDict from .common import EWSAccountService, create_shape_element from ..util import create_element, set_xml_value, MNS from ..version import EXCHANGE_2013 log = logging.getLogger(__name__) class FindPeople(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/findpeople-operation""" SERVICE_NAME = 'FindPeople' element_container_name = '{%s}People' % MNS supported_from = EXCHANGE_2013 supports_paging = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # A hack to communicate parsing args to _elems_to_objs() self.additional_fields = None self.shape = None def call(self, folder, additional_fields, restriction, order_fields, shape, query_string, depth, max_items, offset): """Find items in an account. :param folder: the Folder object to query :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects :param restriction: a Restriction object for :param order_fields: the fields to sort the results by :param shape: The set of attributes to return :param query_string: a QueryString object :param depth: How deep in the folder structure to search for items :param max_items: the max number of items to return :param offset: the offset relative to the first item in the item collection. Usually 0. :return: XML elements for the matching items """ self.additional_fields = additional_fields self.shape = shape return self._elems_to_objs(self._paged_call( payload_func=self.get_payload, max_items=max_items, folders=[folder], # We can only query one folder, so there will only be one element in response **dict( additional_fields=additional_fields, restriction=restriction, order_fields=order_fields, query_string=query_string, shape=shape, depth=depth, page_size=self.chunk_size, offset=offset, ) )) def _elems_to_objs(self, elems): from ..items import Persona, ID_ONLY for elem in elems: if isinstance(elem, Exception): yield elem continue if self.shape == ID_ONLY and self.additional_fields is None: yield Persona.id_from_xml(elem) continue yield Persona.from_xml(elem, account=self.account) def get_payload(self, folders, additional_fields, restriction, order_fields, query_string, shape, depth, page_size, offset=0): folders = list(folders) if len(folders) != 1: raise ValueError('%r can only query one folder' % self.SERVICE_NAME) folder = folders[0] findpeople = create_element('m:%s' % self.SERVICE_NAME, attrs=dict(Traversal=depth)) personashape = create_shape_element( tag='m:PersonaShape', shape=shape, additional_fields=additional_fields, version=self.account.version ) findpeople.append(personashape) view_type = create_element( 'm:IndexedPageItemView', attrs=OrderedDict([ ('MaxEntriesReturned', str(page_size)), ('Offset', str(offset)), ('BasePoint', 'Beginning'), ]) ) findpeople.append(view_type) if restriction: findpeople.append(restriction.to_xml(version=self.account.version)) if order_fields: findpeople.append(set_xml_value( create_element('m:SortOrder'), order_fields, version=self.account.version )) findpeople.append(set_xml_value( create_element('m:ParentFolderId'), folder, version=self.account.version )) if query_string: findpeople.append(query_string.to_xml(version=self.account.version)) return findpeople @staticmethod def _get_paging_values(elem): """Find paging values. The paging element from FindPeople is different from other paging containers.""" item_count = int(elem.find('{%s}TotalNumberOfPeopleInView' % MNS).text) first_matching = int(elem.find('{%s}FirstMatchingRowIndex' % MNS).text) first_loaded = int(elem.find('{%s}FirstLoadedRowIndex' % MNS).text) log.debug('Got page with total items %s, first matching %s, first loaded %s ', item_count, first_matching, first_loaded) next_offset = None # GetPersona does not support fetching more pages return item_count, next_offset exchangelib-4.6.1/exchangelib/services/get_attachment.py000066400000000000000000000125461414601472700234460ustar00rootroot00000000000000from itertools import chain from .common import EWSAccountService, create_attachment_ids_element from ..util import create_element, add_xml_child, set_xml_value, DummyResponse, StreamingBase64Parser,\ StreamingContentHandler, ElementNotFound, MNS # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/bodytype BODY_TYPE_CHOICES = ('Best', 'HTML', 'Text') class GetAttachment(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getattachment-operation""" SERVICE_NAME = 'GetAttachment' element_container_name = '{%s}Attachments' % MNS def call(self, items, include_mime_content, body_type, filter_html_content, additional_fields): if body_type and body_type not in BODY_TYPE_CHOICES: raise ValueError("'body_type' %s must be one of %s" % (body_type, BODY_TYPE_CHOICES)) return self._elems_to_objs(self._chunked_get_elements( self.get_payload, items=items, include_mime_content=include_mime_content, body_type=body_type, filter_html_content=filter_html_content, additional_fields=additional_fields, )) def _elems_to_objs(self, elems): from ..attachments import FileAttachment, ItemAttachment cls_map = {cls.response_tag(): cls for cls in (FileAttachment, ItemAttachment)} for elem in elems: if isinstance(elem, Exception): yield elem continue yield cls_map[elem.tag].from_xml(elem=elem, account=self.account) def get_payload(self, items, include_mime_content, body_type, filter_html_content, additional_fields): payload = create_element('m:%s' % self.SERVICE_NAME) shape_elem = create_element('m:AttachmentShape') if include_mime_content: add_xml_child(shape_elem, 't:IncludeMimeContent', 'true') if body_type: add_xml_child(shape_elem, 't:BodyType', body_type) if filter_html_content is not None: add_xml_child(shape_elem, 't:FilterHtmlContent', 'true' if filter_html_content else 'false') if additional_fields: additional_properties = create_element('t:AdditionalProperties') expanded_fields = chain(*(f.expand(version=self.account.version) for f in additional_fields)) set_xml_value(additional_properties, sorted( expanded_fields, key=lambda f: (getattr(f.field, 'field_uri', ''), f.path) ), version=self.account.version) shape_elem.append(additional_properties) if len(shape_elem): payload.append(shape_elem) attachment_ids = create_attachment_ids_element(items=items, version=self.account.version) payload.append(attachment_ids) return payload def _update_api_version(self, api_version, header, **parse_opts): if not parse_opts.get('stream_file_content', False): super()._update_api_version(api_version, header, **parse_opts) # TODO: We're skipping this part in streaming mode because StreamingBase64Parser cannot parse the SOAP header @classmethod def _get_soap_parts(cls, response, **parse_opts): if not parse_opts.get('stream_file_content', False): return super()._get_soap_parts(response, **parse_opts) # Pass the response unaltered. We want to use our custom streaming parser return None, response def _get_soap_messages(self, body, **parse_opts): if not parse_opts.get('stream_file_content', False): return super()._get_soap_messages(body, **parse_opts) from ..attachments import FileAttachment # 'body' is actually the raw response passed on by '_get_soap_parts' r = body parser = StreamingBase64Parser() field = FileAttachment.get_field_by_fieldname('_content') handler = StreamingContentHandler(parser=parser, ns=field.namespace, element_name=field.field_uri) parser.setContentHandler(handler) return parser.parse(r) def stream_file_content(self, attachment_id): # The streaming XML parser can only stream content of one attachment payload = self.get_payload( items=[attachment_id], include_mime_content=False, body_type=None, filter_html_content=None, additional_fields=None, ) self.streaming = True try: yield from self._get_response_xml(payload=payload, stream_file_content=True) except ElementNotFound as enf: # When the returned XML does not contain a Content element, ElementNotFound is thrown by parser.parse(). # Let the non-streaming SOAP parser parse the response and hook into the normal exception handling. # Wrap in DummyResponse because _get_soap_parts() expects an iter_content() method. response = DummyResponse(url=None, headers=None, request_headers=None, content=enf.data) _, body = super()._get_soap_parts(response=response) res = super()._get_soap_messages(body=body) for e in self._get_elements_in_response(response=res): if isinstance(e, Exception): raise e # The returned content did not contain any EWS exceptions. Give up and re-raise the original exception. raise enf finally: self.streaming = False self.stop_streaming() exchangelib-4.6.1/exchangelib/services/get_delegate.py000066400000000000000000000032711414601472700230630ustar00rootroot00000000000000from .common import EWSAccountService from ..properties import DLMailbox, DelegateUser # The service expects a Mailbox element in the MNS namespace from ..util import create_element, set_xml_value, MNS from ..version import EXCHANGE_2007_SP1 class GetDelegate(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getdelegate-operation""" SERVICE_NAME = 'GetDelegate' supported_from = EXCHANGE_2007_SP1 def call(self, user_ids, include_permissions): return self._elems_to_objs(self._chunked_get_elements( self.get_payload, items=user_ids or [None], mailbox=DLMailbox(email_address=self.account.primary_smtp_address), include_permissions=include_permissions, )) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield DelegateUser.from_xml(elem=elem, account=self.account) def get_payload(self, user_ids, mailbox, include_permissions): payload = create_element( 'm:%s' % self.SERVICE_NAME, attrs=dict(IncludePermissions='true' if include_permissions else 'false'), ) set_xml_value(payload, mailbox, version=self.protocol.version) if user_ids != [None]: set_xml_value(payload, user_ids, version=self.protocol.version) return payload @classmethod def _get_elements_in_container(cls, container): return container.findall(DelegateUser.response_tag()) @classmethod def _response_message_tag(cls): return '{%s}DelegateUserResponseMessageType' % MNS exchangelib-4.6.1/exchangelib/services/get_events.py000066400000000000000000000023751414601472700226210ustar00rootroot00000000000000import logging from .common import EWSAccountService, add_xml_child from ..properties import Notification from ..util import create_element log = logging.getLogger(__name__) class GetEvents(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getevents-operation """ SERVICE_NAME = 'GetEvents' prefer_affinity = True def call(self, subscription_id, watermark): return self._elems_to_objs(self._get_elements(payload=self.get_payload( subscription_id=subscription_id, watermark=watermark, ))) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield Notification.from_xml(elem=elem, account=None) @classmethod def _get_elements_in_container(cls, container): return container.findall(Notification.response_tag()) def get_payload(self, subscription_id, watermark): getstreamingevents = create_element('m:%s' % self.SERVICE_NAME) add_xml_child(getstreamingevents, 'm:SubscriptionId', subscription_id) add_xml_child(getstreamingevents, 'm:Watermark', watermark) return getstreamingevents exchangelib-4.6.1/exchangelib/services/get_folder.py000066400000000000000000000047601414601472700225700ustar00rootroot00000000000000from .common import EWSAccountService, parse_folder_elem, create_folder_ids_element, \ create_shape_element from ..errors import ErrorFolderNotFound, ErrorNoPublicFolderReplicaAvailable, ErrorInvalidOperation from ..util import create_element, MNS class GetFolder(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getfolder-operation""" SERVICE_NAME = 'GetFolder' element_container_name = '{%s}Folders' % MNS ERRORS_TO_CATCH_IN_RESPONSE = EWSAccountService.ERRORS_TO_CATCH_IN_RESPONSE + ( ErrorFolderNotFound, ErrorNoPublicFolderReplicaAvailable, ErrorInvalidOperation, ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.folders = [] # A hack to communicate parsing args to _elems_to_objs() def call(self, folders, additional_fields, shape): """Take a folder ID and returns the full information for that folder. :param folders: a list of Folder objects :param additional_fields: the extra fields that should be returned with the folder, as FieldPath objects :param shape: The set of attributes to return :return: XML elements for the folders, in stable order """ # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same # class as the folder instance it was requested with. self.folders = list(folders) # Convert to a list, in case 'folders' is a generator. We're iterating twice. return self._elems_to_objs(self._chunked_get_elements( self.get_payload, items=self.folders, additional_fields=additional_fields, shape=shape, )) def _elems_to_objs(self, elems): for folder, elem in zip(self.folders, elems): if isinstance(elem, Exception): yield elem continue yield parse_folder_elem(elem=elem, folder=folder, account=self.account) def get_payload(self, folders, additional_fields, shape): getfolder = create_element('m:%s' % self.SERVICE_NAME) foldershape = create_shape_element( tag='m:FolderShape', shape=shape, additional_fields=additional_fields, version=self.account.version ) getfolder.append(foldershape) folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version) getfolder.append(folder_ids) return getfolder exchangelib-4.6.1/exchangelib/services/get_item.py000066400000000000000000000033201414601472700222420ustar00rootroot00000000000000from .common import EWSAccountService, create_item_ids_element, create_shape_element from ..util import create_element, MNS class GetItem(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getitem-operation""" SERVICE_NAME = 'GetItem' element_container_name = '{%s}Items' % MNS def call(self, items, additional_fields, shape): """Return all items in an account that correspond to a list of ID's, in stable order. :param items: a list of (id, changekey) tuples or Item objects :param additional_fields: the extra fields that should be returned with the item, as FieldPath objects :param shape: The shape of returned objects :return: XML elements for the items, in stable order """ return self._elems_to_objs(self._chunked_get_elements( self.get_payload, items=items, additional_fields=additional_fields, shape=shape, )) def _elems_to_objs(self, elems): from ..folders.base import BaseFolder for elem in elems: if isinstance(elem, Exception): yield elem continue yield BaseFolder.item_model_from_tag(elem.tag).from_xml(elem=elem, account=self.account) def get_payload(self, items, additional_fields, shape): getitem = create_element('m:%s' % self.SERVICE_NAME) itemshape = create_shape_element( tag='m:ItemShape', shape=shape, additional_fields=additional_fields, version=self.account.version ) getitem.append(itemshape) item_ids = create_item_ids_element(items=items, version=self.account.version) getitem.append(item_ids) return getitem exchangelib-4.6.1/exchangelib/services/get_mail_tips.py000066400000000000000000000033461414601472700232750ustar00rootroot00000000000000from .common import EWSService from ..properties import MailTips from ..util import create_element, set_xml_value, MNS class GetMailTips(EWSService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getmailtips-operation""" SERVICE_NAME = 'GetMailTips' def call(self, sending_as, recipients, mail_tips_requested): return self._elems_to_objs(self._chunked_get_elements( self.get_payload, items=recipients, sending_as=sending_as, mail_tips_requested=mail_tips_requested, )) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield MailTips.from_xml(elem=elem, account=None) def get_payload(self, recipients, sending_as, mail_tips_requested): payload = create_element('m:%s' % self.SERVICE_NAME) set_xml_value(payload, sending_as, version=self.protocol.version) recipients_elem = create_element('m:Recipients') for recipient in recipients: set_xml_value(recipients_elem, recipient, version=self.protocol.version) if not len(recipients_elem): raise ValueError('"recipients" must not be empty') payload.append(recipients_elem) if mail_tips_requested: set_xml_value(payload, mail_tips_requested, version=self.protocol.version) return payload def _get_elements_in_response(self, response): for msg in response: yield self._get_element_container(message=msg, name=MailTips.response_tag()) @classmethod def _response_message_tag(cls): return '{%s}MailTipsResponseMessageType' % MNS exchangelib-4.6.1/exchangelib/services/get_persona.py000066400000000000000000000025211414601472700227550ustar00rootroot00000000000000from .common import EWSAccountService, to_item_id from ..properties import PersonaId from ..util import create_element, set_xml_value, MNS class GetPersona(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getpersona-operation""" SERVICE_NAME = 'GetPersona' def call(self, persona): return self._elems_to_objs(self._get_elements(payload=self.get_payload(persona=persona))) def _elems_to_objs(self, elems): from ..items import Persona elements = list(elems) if len(elements) != 1: raise ValueError('Expected exactly one element in response') elem = elements[0] if isinstance(elem, Exception): raise elem return Persona.from_xml(elem=elem, account=None) def get_payload(self, persona): version = self.protocol.version payload = create_element('m:%s' % self.SERVICE_NAME) set_xml_value(payload, to_item_id(persona, PersonaId, version=version), version=version) return payload @classmethod def _get_elements_in_container(cls, container): from ..items import Persona return container.findall('{%s}%s' % (MNS, Persona.ELEMENT_NAME)) @classmethod def _response_tag(cls): return '{%s}%sResponseMessage' % (MNS, cls.SERVICE_NAME) exchangelib-4.6.1/exchangelib/services/get_room_lists.py000066400000000000000000000014731414601472700235050ustar00rootroot00000000000000from .common import EWSService from ..properties import RoomList from ..util import create_element, MNS from ..version import EXCHANGE_2010 class GetRoomLists(EWSService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getroomlists-operation""" SERVICE_NAME = 'GetRoomLists' element_container_name = '{%s}RoomLists' % MNS supported_from = EXCHANGE_2010 def call(self): return self._elems_to_objs(self._get_elements(payload=self.get_payload())) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield RoomList.from_xml(elem=elem, account=None) def get_payload(self): return create_element('m:%s' % self.SERVICE_NAME) exchangelib-4.6.1/exchangelib/services/get_rooms.py000066400000000000000000000016741414601472700224550ustar00rootroot00000000000000from .common import EWSService from ..properties import Room from ..util import create_element, set_xml_value, MNS from ..version import EXCHANGE_2010 class GetRooms(EWSService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getrooms-operation""" SERVICE_NAME = 'GetRooms' element_container_name = '{%s}Rooms' % MNS supported_from = EXCHANGE_2010 def call(self, roomlist): return self._elems_to_objs(self._get_elements(payload=self.get_payload(roomlist=roomlist))) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield Room.from_xml(elem=elem, account=None) def get_payload(self, roomlist): getrooms = create_element('m:%s' % self.SERVICE_NAME) set_xml_value(getrooms, roomlist, version=self.protocol.version) return getrooms exchangelib-4.6.1/exchangelib/services/get_searchable_mailboxes.py000066400000000000000000000045771414601472700254570ustar00rootroot00000000000000from .common import EWSService from ..errors import MalformedResponseError from ..properties import SearchableMailbox, FailedMailbox from ..util import create_element, add_xml_child, MNS from ..version import EXCHANGE_2013 class GetSearchableMailboxes(EWSService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getsearchablemailboxes-operation """ SERVICE_NAME = 'GetSearchableMailboxes' element_container_name = '{%s}SearchableMailboxes' % MNS failed_mailboxes_container_name = '{%s}FailedMailboxes' % MNS supported_from = EXCHANGE_2013 def call(self, search_filter, expand_group_membership): return self._elems_to_objs(self._get_elements(payload=self.get_payload( search_filter=search_filter, expand_group_membership=expand_group_membership, ))) def _elems_to_objs(self, elems): cls_map = {cls.response_tag(): cls for cls in (SearchableMailbox, FailedMailbox)} for elem in elems: if isinstance(elem, Exception): yield elem continue yield cls_map[elem.tag].from_xml(elem=elem, account=None) def get_payload(self, search_filter, expand_group_membership): payload = create_element('m:%s' % self.SERVICE_NAME) if search_filter: add_xml_child(payload, 'm:SearchFilter', search_filter) if expand_group_membership is not None: add_xml_child(payload, 'm:ExpandGroupMembership', 'true' if expand_group_membership else 'false') return payload def _get_elements_in_response(self, response): for msg in response: for container_name in (self.element_container_name, self.failed_mailboxes_container_name): try: container_or_exc = self._get_element_container(message=msg, name=container_name) except MalformedResponseError: # Responses may contain no failed mailboxes. _get_element_container() does not accept this. if container_name == self.failed_mailboxes_container_name: continue raise if isinstance(container_or_exc, (bool, Exception)): yield container_or_exc continue yield from self._get_elements_in_container(container=container_or_exc) exchangelib-4.6.1/exchangelib/services/get_server_time_zones.py000066400000000000000000000131701414601472700250520ustar00rootroot00000000000000import datetime from .common import EWSService from ..errors import NaiveDateTimeNotAllowed from ..ewsdatetime import EWSDateTime from ..fields import WEEKDAY_NAMES from ..util import create_element, set_xml_value, xml_text_to_value, peek, TNS, MNS from ..version import EXCHANGE_2010 class GetServerTimeZones(EWSService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getservertimezones-operation """ SERVICE_NAME = 'GetServerTimeZones' element_container_name = '{%s}TimeZoneDefinitions' % MNS supported_from = EXCHANGE_2010 def call(self, timezones=None, return_full_timezone_data=False): return self._elems_to_objs(self._get_elements(payload=self.get_payload( timezones=timezones, return_full_timezone_data=return_full_timezone_data ))) def get_payload(self, timezones, return_full_timezone_data): payload = create_element( 'm:%s' % self.SERVICE_NAME, attrs=dict(ReturnFullTimeZoneData='true' if return_full_timezone_data else 'false'), ) if timezones is not None: is_empty, timezones = peek(timezones) if not is_empty: tz_ids = create_element('m:Ids') for timezone in timezones: tz_id = set_xml_value(create_element('t:Id'), timezone.ms_id, version=self.protocol.version) tz_ids.append(tz_id) payload.append(tz_ids) return payload def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue tz_id = elem.get('Id') tz_name = elem.get('Name') tz_periods = self._get_periods(elem) tz_transitions_groups = self._get_transitions_groups(elem) tz_transitions = self._get_transitions(elem) yield tz_id, tz_name, tz_periods, tz_transitions, tz_transitions_groups @staticmethod def _get_periods(timezonedef): tz_periods = {} periods = timezonedef.find('{%s}Periods' % TNS) for period in periods.findall('{%s}Period' % TNS): # Convert e.g. "trule:Microsoft/Registry/W. Europe Standard Time/2006-Daylight" to (2006, 'Daylight') p_year, p_type = period.get('Id').rsplit('/', 1)[1].split('-') tz_periods[(int(p_year), p_type)] = dict( name=period.get('Name'), bias=xml_text_to_value(period.get('Bias'), datetime.timedelta) ) return tz_periods @staticmethod def _get_transitions_groups(timezonedef): tz_transitions_groups = {} transitiongroups = timezonedef.find('{%s}TransitionsGroups' % TNS) if transitiongroups is not None: for transitiongroup in transitiongroups.findall('{%s}TransitionsGroup' % TNS): tg_id = int(transitiongroup.get('Id')) tz_transitions_groups[tg_id] = [] for transition in transitiongroup.findall('{%s}Transition' % TNS): # Apply same conversion to To as for period IDs to_year, to_type = transition.find('{%s}To' % TNS).text.rsplit('/', 1)[1].split('-') tz_transitions_groups[tg_id].append(dict( to=(int(to_year), to_type), )) for transition in transitiongroup.findall('{%s}RecurringDayTransition' % TNS): # Apply same conversion to To as for period IDs to_year, to_type = transition.find('{%s}To' % TNS).text.rsplit('/', 1)[1].split('-') occurrence = xml_text_to_value(transition.find('{%s}Occurrence' % TNS).text, int) if occurrence == -1: # See TimeZoneTransition.from_xml() occurrence = 5 tz_transitions_groups[tg_id].append(dict( to=(int(to_year), to_type), offset=xml_text_to_value(transition.find('{%s}TimeOffset' % TNS).text, datetime.timedelta), iso_month=xml_text_to_value(transition.find('{%s}Month' % TNS).text, int), iso_weekday=WEEKDAY_NAMES.index(transition.find('{%s}DayOfWeek' % TNS).text) + 1, occurrence=occurrence, )) return tz_transitions_groups @staticmethod def _get_transitions(timezonedef): tz_transitions = {} transitions = timezonedef.find('{%s}Transitions' % TNS) if transitions is not None: for transition in transitions.findall('{%s}Transition' % TNS): to = transition.find('{%s}To' % TNS) if to.get('Kind') != 'Group': raise ValueError('Unexpected "Kind" XML attr: %s' % to.get('Kind')) tg_id = xml_text_to_value(to.text, int) tz_transitions[tg_id] = None for transition in transitions.findall('{%s}AbsoluteDateTransition' % TNS): to = transition.find('{%s}To' % TNS) if to.get('Kind') != 'Group': raise ValueError('Unexpected "Kind" XML attr: %s' % to.get('Kind')) tg_id = xml_text_to_value(to.text, int) try: t_date = xml_text_to_value(transition.find('{%s}DateTime' % TNS).text, EWSDateTime).date() except NaiveDateTimeNotAllowed as e: # We encountered a naive datetime. Don't worry. we just need the date t_date = e.local_dt.date() tz_transitions[tg_id] = t_date return tz_transitions exchangelib-4.6.1/exchangelib/services/get_streaming_events.py000066400000000000000000000103561414601472700246700ustar00rootroot00000000000000import logging from .common import EWSAccountService, add_xml_child from ..properties import Notification from ..util import create_element, get_xml_attr, get_xml_attrs, MNS, DocumentYielder, DummyResponse log = logging.getLogger(__name__) xml_log = logging.getLogger('%s.xml' % __name__) class GetStreamingEvents(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getstreamingevents-operation """ SERVICE_NAME = 'GetStreamingEvents' element_container_name = '{%s}Notifications' % MNS streaming = True prefer_affinity = True # Connection status values OK = 'OK' CLOSED = 'Closed' def __init__(self, *args, **kwargs): # These values are set each time call() is consumed self.connection_status = None self.error_subscription_ids = [] super().__init__(*args, **kwargs) def call(self, subscription_ids, connection_timeout): if connection_timeout < 1: raise ValueError("'connection_timeout' must be a positive integer") return self._elems_to_objs(self._get_elements(payload=self.get_payload( subscription_ids=subscription_ids, connection_timeout=connection_timeout, ))) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield Notification.from_xml(elem=elem, account=None) @classmethod def _get_soap_parts(cls, response, **parse_opts): # Pass the response unaltered. We want to use our custom document yielder return None, response def _get_soap_messages(self, body, **parse_opts): # 'body' is actually the raw response passed on by '_get_soap_parts'. We want to continuously read the content, # looking for complete XML documents. When we have a full document, we want to parse it as if it was a normal # XML response. r = body for i, doc in enumerate(DocumentYielder(r.iter_content()), start=1): xml_log.debug('''Response XML (docs received: %(i)s): %(xml_response)s''', dict(i=i, xml_response=doc)) response = DummyResponse(url=None, headers=None, request_headers=None, content=doc) try: _, body = super()._get_soap_parts(response=response, **parse_opts) except Exception: r.close() # Release memory raise # TODO: We're skipping ._update_api_version() here because we don't have access to the 'api_version' used. # TODO: We should be doing a lot of error handling for ._get_soap_messages(). yield from super()._get_soap_messages(body=body, **parse_opts) if self.connection_status == self.CLOSED: # Don't wait for the TCP connection to timeout break def _get_element_container(self, message, name=None): error_ids_elem = message.find('{%s}ErrorSubscriptionIds' % MNS) if error_ids_elem is not None: self.error_subscription_ids = get_xml_attrs(error_ids_elem, '{%s}ErrorSubscriptionId' % MNS) log.debug('These subscription IDs are invalid: %s', self.error_subscription_ids) self.connection_status = get_xml_attr(message, '{%s}ConnectionStatus' % MNS) # Either 'OK' or 'Closed' log.debug('Connection status is: %s', self.connection_status) # Upstream expects to find a 'name' tag but our response does not always have it. Return an empty element. if message.find(name) is None: return [] return super()._get_element_container(message=message, name=name) def get_payload(self, subscription_ids, connection_timeout): getstreamingevents = create_element('m:%s' % self.SERVICE_NAME) subscriptions_elem = create_element('m:SubscriptionIds') for subscription_id in subscription_ids: add_xml_child(subscriptions_elem, 't:SubscriptionId', subscription_id) if not len(subscriptions_elem): raise ValueError('"subscription_ids" must not be empty') getstreamingevents.append(subscriptions_elem) add_xml_child(getstreamingevents, 'm:ConnectionTimeout', connection_timeout) return getstreamingevents exchangelib-4.6.1/exchangelib/services/get_user_availability.py000066400000000000000000000043001414601472700250130ustar00rootroot00000000000000from .common import EWSService from ..properties import FreeBusyView from ..util import create_element, set_xml_value, MNS class GetUserAvailability(EWSService): """Get detailed availability information for a list of users. MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getuseravailability-operation """ SERVICE_NAME = 'GetUserAvailability' def call(self, timezone, mailbox_data, free_busy_view_options): # TODO: Also supports SuggestionsViewOptions, see # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/suggestionsviewoptions return self._elems_to_objs(self._get_elements(payload=self.get_payload( timezone=timezone, mailbox_data=mailbox_data, free_busy_view_options=free_busy_view_options ))) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield FreeBusyView.from_xml(elem=elem, account=None) def get_payload(self, timezone, mailbox_data, free_busy_view_options): payload = create_element('m:%sRequest' % self.SERVICE_NAME) set_xml_value(payload, timezone, version=self.protocol.version) mailbox_data_array = create_element('m:MailboxDataArray') set_xml_value(mailbox_data_array, mailbox_data, version=self.protocol.version) payload.append(mailbox_data_array) set_xml_value(payload, free_busy_view_options, version=self.protocol.version) return payload @staticmethod def _response_messages_tag(): return '{%s}FreeBusyResponseArray' % MNS @classmethod def _response_message_tag(cls): return '{%s}FreeBusyResponse' % MNS def _get_elements_in_response(self, response): for msg in response: # Just check the response code and raise errors self._get_element_container(message=msg.find('{%s}ResponseMessage' % MNS)) yield from self._get_elements_in_container(container=msg) @classmethod def _get_elements_in_container(cls, container): return [container.find('{%s}FreeBusyView' % MNS)] exchangelib-4.6.1/exchangelib/services/get_user_configuration.py000066400000000000000000000034211414601472700252130ustar00rootroot00000000000000from .common import EWSAccountService from ..properties import UserConfiguration from ..util import create_element, set_xml_value ID = 'Id' DICTIONARY = 'Dictionary' XML_DATA = 'XmlData' BINARY_DATA = 'BinaryData' ALL = 'All' PROPERTIES_CHOICES = {ID, DICTIONARY, XML_DATA, BINARY_DATA, ALL} class GetUserConfiguration(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getuserconfiguration-operation """ SERVICE_NAME = 'GetUserConfiguration' def call(self, user_configuration_name, properties): if properties not in PROPERTIES_CHOICES: raise ValueError("'properties' %r must be one of %s" % (properties, PROPERTIES_CHOICES)) return self._elems_to_objs(self._get_elements(payload=self.get_payload( user_configuration_name=user_configuration_name, properties=properties ))) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield UserConfiguration.from_xml(elem=elem, account=self.account) @classmethod def _get_elements_in_container(cls, container): return container.findall(UserConfiguration.response_tag()) def get_payload(self, user_configuration_name, properties): getuserconfiguration = create_element('m:%s' % self.SERVICE_NAME) set_xml_value(getuserconfiguration, user_configuration_name, version=self.account.version) user_configuration_properties = create_element('m:UserConfigurationProperties') set_xml_value(user_configuration_properties, properties, version=self.account.version) getuserconfiguration.append(user_configuration_properties) return getuserconfiguration exchangelib-4.6.1/exchangelib/services/get_user_oof_settings.py000066400000000000000000000031341414601472700250500ustar00rootroot00000000000000from .common import EWSAccountService from ..properties import AvailabilityMailbox from ..settings import OofSettings from ..util import create_element, set_xml_value, MNS, TNS class GetUserOofSettings(EWSAccountService): """Get automatic reply settings for the specified mailbox. MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/getuseroofsettings-operation """ SERVICE_NAME = 'GetUserOofSettings' element_container_name = '{%s}OofSettings' % TNS def call(self, mailbox): return self._elems_to_objs(self._get_elements(payload=self.get_payload(mailbox=mailbox))) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield OofSettings.from_xml(elem=elem, account=self.account) def get_payload(self, mailbox): payload = create_element('m:%sRequest' % self.SERVICE_NAME) return set_xml_value(payload, AvailabilityMailbox.from_mailbox(mailbox), version=self.account.version) @classmethod def _get_elements_in_container(cls, container): # This service only returns one result, directly in 'container' return [container] def _get_element_container(self, message, name=None): # This service returns the result container outside the response message super()._get_element_container(message=message.find(self._response_message_tag()), name=None) return message.find(name) @classmethod def _response_message_tag(cls): return '{%s}ResponseMessage' % MNS exchangelib-4.6.1/exchangelib/services/mark_as_junk.py000066400000000000000000000025271414601472700231210ustar00rootroot00000000000000from .common import EWSAccountService, create_item_ids_element from ..properties import MovedItemId from ..util import create_element class MarkAsJunk(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/markasjunk-operation""" SERVICE_NAME = 'MarkAsJunk' def call(self, items, is_junk, move_item): return self._elems_to_objs( self._chunked_get_elements(self.get_payload, items=items, is_junk=is_junk, move_item=move_item) ) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, (Exception, type(None))): yield elem continue yield MovedItemId.id_from_xml(elem) @classmethod def _get_elements_in_container(cls, container): return container.findall(MovedItemId.response_tag()) def get_payload(self, items, is_junk, move_item): # Takes a list of items and returns either success or raises an error message mark_as_junk = create_element( 'm:%s' % self.SERVICE_NAME, attrs=dict(IsJunk='true' if is_junk else 'false', MoveItem='true' if move_item else 'false') ) item_ids = create_item_ids_element(items=items, version=self.account.version) mark_as_junk.append(item_ids) return mark_as_junk exchangelib-4.6.1/exchangelib/services/move_folder.py000066400000000000000000000030301414601472700227440ustar00rootroot00000000000000from .common import EWSAccountService, create_folder_ids_element from ..util import create_element, set_xml_value, MNS class MoveFolder(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/movefolder-operation""" SERVICE_NAME = "MoveFolder" element_container_name = '{%s}Folders' % MNS def call(self, folders, to_folder): from ..folders import BaseFolder, FolderId if not isinstance(to_folder, (BaseFolder, FolderId)): raise ValueError("'to_folder' %r must be a Folder or FolderId instance" % to_folder) return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=folders, to_folder=to_folder)) def _elems_to_objs(self, elems): from ..folders import FolderId for elem in elems: if isinstance(elem, (Exception, type(None))): yield elem continue yield FolderId.from_xml(elem=elem.find(FolderId.response_tag()), account=self.account) def get_payload(self, folders, to_folder): # Takes a list of folders and returns their new folder IDs movefolder = create_element('m:%s' % self.SERVICE_NAME) tofolderid = create_element('m:ToFolderId') set_xml_value(tofolderid, to_folder, version=self.account.version) movefolder.append(tofolderid) folder_ids = create_folder_ids_element(tag='m:FolderIds', folders=folders, version=self.account.version) movefolder.append(folder_ids) return movefolder exchangelib-4.6.1/exchangelib/services/move_item.py000066400000000000000000000026371414601472700224430ustar00rootroot00000000000000from .common import EWSAccountService, create_item_ids_element from ..util import create_element, set_xml_value, MNS class MoveItem(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/moveitem-operation""" SERVICE_NAME = 'MoveItem' element_container_name = '{%s}Items' % MNS def call(self, items, to_folder): from ..folders import BaseFolder, FolderId if not isinstance(to_folder, (BaseFolder, FolderId)): raise ValueError("'to_folder' %r must be a Folder or FolderId instance" % to_folder) return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items, to_folder=to_folder)) def _elems_to_objs(self, elems): from ..items import Item for elem in elems: if isinstance(elem, (Exception, type(None))): yield elem continue yield Item.id_from_xml(elem) def get_payload(self, items, to_folder): # Takes a list of items and returns their new item IDs moveitem = create_element('m:%s' % self.SERVICE_NAME) tofolderid = create_element('m:ToFolderId') set_xml_value(tofolderid, to_folder, version=self.account.version) moveitem.append(tofolderid) item_ids = create_item_ids_element(items=items, version=self.account.version) moveitem.append(item_ids) return moveitem exchangelib-4.6.1/exchangelib/services/resolve_names.py000066400000000000000000000102101414601472700233030ustar00rootroot00000000000000import logging from .common import EWSService from ..errors import ErrorNameResolutionNoResults, ErrorNameResolutionMultipleResults from ..properties import Mailbox from ..util import create_element, set_xml_value, add_xml_child, MNS from ..version import EXCHANGE_2010_SP2 log = logging.getLogger(__name__) class ResolveNames(EWSService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/resolvenames-operation""" SERVICE_NAME = 'ResolveNames' element_container_name = '{%s}ResolutionSet' % MNS ERRORS_TO_CATCH_IN_RESPONSE = ErrorNameResolutionNoResults WARNINGS_TO_IGNORE_IN_RESPONSE = ErrorNameResolutionMultipleResults # Note: paging information is returned as attrs on the 'ResolutionSet' element, but this service does not # support the 'IndexedPageItemView' element, so it's not really a paging service. According to docs, at most # 100 candidates are returned for a lookup. supports_paging = False def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.return_full_contact_data = False # A hack to communicate parsing args to _elems_to_objs() def call(self, unresolved_entries, parent_folders=None, return_full_contact_data=False, search_scope=None, contact_data_shape=None): from ..items import SHAPE_CHOICES, SEARCH_SCOPE_CHOICES if self.chunk_size > 100: log.warning( 'Chunk size %s is dangerously high. %s supports returning at most 100 candidates for a lookup', self.chunk_size, self.SERVICE_NAME ) if search_scope and search_scope not in SEARCH_SCOPE_CHOICES: raise ValueError("'search_scope' %s must be one if %s" % (search_scope, SEARCH_SCOPE_CHOICES)) if contact_data_shape and contact_data_shape not in SHAPE_CHOICES: raise ValueError("'shape' %s must be one if %s" % (contact_data_shape, SHAPE_CHOICES)) self.return_full_contact_data = return_full_contact_data return self._elems_to_objs(self._chunked_get_elements( self.get_payload, items=unresolved_entries, parent_folders=parent_folders, return_full_contact_data=return_full_contact_data, search_scope=search_scope, contact_data_shape=contact_data_shape, )) def _elems_to_objs(self, elems): from ..items import Contact for elem in elems: if isinstance(elem, Exception): yield elem continue if self.return_full_contact_data: mailbox_elem = elem.find(Mailbox.response_tag()) contact_elem = elem.find(Contact.response_tag()) yield ( None if mailbox_elem is None else Mailbox.from_xml(elem=mailbox_elem, account=None), None if contact_elem is None else Contact.from_xml(elem=contact_elem, account=None), ) else: yield Mailbox.from_xml(elem=elem.find(Mailbox.response_tag()), account=None) def get_payload(self, unresolved_entries, parent_folders, return_full_contact_data, search_scope, contact_data_shape): payload = create_element( 'm:%s' % self.SERVICE_NAME, attrs=dict(ReturnFullContactData='true' if return_full_contact_data else 'false'), ) if search_scope: payload.set('SearchScope', search_scope) if contact_data_shape: if self.protocol.version.build < EXCHANGE_2010_SP2: raise NotImplementedError( "'contact_data_shape' is only supported for Exchange 2010 SP2 servers and later") payload.set('ContactDataShape', contact_data_shape) if parent_folders: parentfolderids = create_element('m:ParentFolderIds') set_xml_value(parentfolderids, parent_folders, version=self.protocol.version) for entry in unresolved_entries: add_xml_child(payload, 'm:UnresolvedEntry', entry) if not len(payload): raise ValueError('"unresolved_entries" must not be empty') return payload exchangelib-4.6.1/exchangelib/services/send_item.py000066400000000000000000000025011414601472700224140ustar00rootroot00000000000000from .common import EWSAccountService, create_item_ids_element from ..util import create_element, set_xml_value class SendItem(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/senditem-operation""" SERVICE_NAME = 'SendItem' returns_elements = False def call(self, items, saved_item_folder): from ..folders import BaseFolder, FolderId if saved_item_folder and not isinstance(saved_item_folder, (BaseFolder, FolderId)): raise ValueError("'saved_item_folder' %r must be a Folder or FolderId instance" % saved_item_folder) return self._chunked_get_elements(self.get_payload, items=items, saved_item_folder=saved_item_folder) def get_payload(self, items, saved_item_folder): senditem = create_element( 'm:%s' % self.SERVICE_NAME, attrs=dict(SaveItemToFolder='true' if saved_item_folder else 'false'), ) item_ids = create_item_ids_element(items=items, version=self.account.version) senditem.append(item_ids) if saved_item_folder: saveditemfolderid = create_element('m:SavedItemFolderId') set_xml_value(saveditemfolderid, saved_item_folder, version=self.account.version) senditem.append(saveditemfolderid) return senditem exchangelib-4.6.1/exchangelib/services/send_notification.py000066400000000000000000000017361414601472700241550ustar00rootroot00000000000000from .common import EWSService from ..properties import Notification from ..util import MNS class SendNotification(EWSService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/sendnotification This is not an actual EWS service you can call. We only use it to parse the XML body of push notifications. """ SERVICE_NAME = 'SendNotification' def call(self): raise NotImplementedError() def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield Notification.from_xml(elem=elem, account=None) @classmethod def _response_tag(cls): """Return the name of the element containing the service response.""" return '{%s}%s' % (MNS, cls.SERVICE_NAME) @classmethod def _get_elements_in_container(cls, container): return container.findall(Notification.response_tag()) exchangelib-4.6.1/exchangelib/services/set_user_oof_settings.py000066400000000000000000000027611414601472700250710ustar00rootroot00000000000000from .common import EWSAccountService from ..properties import AvailabilityMailbox, Mailbox from ..settings import OofSettings from ..util import create_element, set_xml_value, MNS class SetUserOofSettings(EWSAccountService): """Set automatic replies for the specified mailbox. MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/setuseroofsettings-operation """ SERVICE_NAME = 'SetUserOofSettings' returns_elements = False def call(self, oof_settings, mailbox): if not isinstance(oof_settings, OofSettings): raise ValueError("'oof_settings' %r must be an OofSettings instance" % oof_settings) if not isinstance(mailbox, Mailbox): raise ValueError("'mailbox' %r must be an Mailbox instance" % mailbox) return self._get_elements(payload=self.get_payload(oof_settings=oof_settings, mailbox=mailbox)) def get_payload(self, oof_settings, mailbox): payload = create_element('m:%sRequest' % self.SERVICE_NAME) set_xml_value(payload, AvailabilityMailbox.from_mailbox(mailbox), version=self.account.version) set_xml_value(payload, oof_settings, version=self.account.version) return payload def _get_element_container(self, message, name=None): message = message.find(self._response_message_tag()) return super()._get_element_container(message=message, name=name) @classmethod def _response_message_tag(cls): return '{%s}ResponseMessage' % MNS exchangelib-4.6.1/exchangelib/services/subscribe.py000066400000000000000000000111651414601472700224340ustar00rootroot00000000000000"""The 'Subscribe' service has two different modes, pull and push, with different signatures. Implement as two distinct classes. """ import abc from .common import EWSAccountService, create_folder_ids_element, add_xml_child from ..util import create_element, MNS class Subscribe(EWSAccountService, metaclass=abc.ABCMeta): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/subscribe-operation""" SERVICE_NAME = 'Subscribe' EVENT_TYPES = ( 'CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent', ) subscription_request_elem_tag = None def _partial_call(self, payload_func, folders, event_types, **kwargs): if set(event_types) - set(self.EVENT_TYPES): raise ValueError("'event_types' values must consist of values in %s" % str(self.EVENT_TYPES)) return self._elems_to_objs(self._get_elements( payload=payload_func(folders=folders, event_types=event_types, **kwargs) )) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue subscription_elem, watermark_elem = elem yield subscription_elem.text, watermark_elem.text @classmethod def _get_elements_in_container(cls, container): return [(container.find('{%s}SubscriptionId' % MNS), container.find('{%s}Watermark' % MNS))] def _partial_payload(self, folders, event_types): request_elem = create_element(self.subscription_request_elem_tag) folder_ids = create_folder_ids_element(tag='t:FolderIds', folders=folders, version=self.account.version) request_elem.append(folder_ids) event_types_elem = create_element('t:EventTypes') for event_type in event_types: add_xml_child(event_types_elem, 't:EventType', event_type) if not len(event_types_elem): raise ValueError('"event_types" must not be empty') request_elem.append(event_types_elem) return request_elem class SubscribeToPull(Subscribe): subscription_request_elem_tag = 'm:PullSubscriptionRequest' def call(self, folders, event_types, watermark, timeout): yield from self._partial_call( payload_func=self.get_payload, folders=folders, event_types=event_types, timeout=timeout, watermark=watermark, ) def get_payload(self, folders, event_types, watermark, timeout): subscribe = create_element('m:%s' % self.SERVICE_NAME) request_elem = self._partial_payload(folders=folders, event_types=event_types) if watermark: add_xml_child(request_elem, 'm:Watermark', watermark) add_xml_child(request_elem, 't:Timeout', timeout) # In minutes subscribe.append(request_elem) return subscribe class SubscribeToPush(Subscribe): subscription_request_elem_tag = 'm:PushSubscriptionRequest' def call(self, folders, event_types, watermark, status_frequency, url): yield from self._partial_call( payload_func=self.get_payload, folders=folders, event_types=event_types, status_frequency=status_frequency, url=url, watermark=watermark, ) def get_payload(self, folders, event_types, watermark, status_frequency, url): subscribe = create_element('m:%s' % self.SERVICE_NAME) request_elem = self._partial_payload(folders=folders, event_types=event_types) if watermark: add_xml_child(request_elem, 'm:Watermark', watermark) add_xml_child(request_elem, 't:StatusFrequency', status_frequency) # In minutes add_xml_child(request_elem, 't:URL', url) subscribe.append(request_elem) return subscribe class SubscribeToStreaming(Subscribe): subscription_request_elem_tag = 'm:StreamingSubscriptionRequest' def call(self, folders, event_types): yield from self._partial_call(payload_func=self.get_payload, folders=folders, event_types=event_types) def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield elem.text @classmethod def _get_elements_in_container(cls, container): return [container.find('{%s}SubscriptionId' % MNS)] def get_payload(self, folders, event_types): subscribe = create_element('m:%s' % self.SERVICE_NAME) request_elem = self._partial_payload(folders=folders, event_types=event_types) subscribe.append(request_elem) return subscribe exchangelib-4.6.1/exchangelib/services/sync_folder_hierarchy.py000066400000000000000000000072411414601472700250200ustar00rootroot00000000000000import abc import logging from .common import EWSAccountService, add_xml_child, create_folder_ids_element, create_shape_element, parse_folder_elem from ..properties import FolderId from ..util import create_element, xml_text_to_value, MNS, TNS log = logging.getLogger(__name__) class SyncFolder(EWSAccountService, metaclass=abc.ABCMeta): """Base class for SyncFolderHierarchy and SyncFolderItems.""" element_container_name = '{%s}Changes' % MNS # Change types CREATE = 'create' UPDATE = 'update' DELETE = 'delete' CHANGE_TYPES = (CREATE, UPDATE, DELETE) shape_tag = None last_in_range_name = None def __init__(self, *args, **kwargs): # These values are reset and set each time call() is consumed self.sync_state = None self.includes_last_item_in_range = None super().__init__(*args, **kwargs) def _change_types_map(self): return { '{%s}Create' % TNS: self.CREATE, '{%s}Update' % TNS: self.UPDATE, '{%s}Delete' % TNS: self.DELETE, } def _get_element_container(self, message, name=None): self.sync_state = message.find('{%s}SyncState' % MNS).text log.debug('Sync state is: %s', self.sync_state) self.includes_last_item_in_range = xml_text_to_value( message.find(self.last_in_range_name).text, bool ) log.debug('Includes last item in range: %s', self.includes_last_item_in_range) return super()._get_element_container(message=message, name=name) def _partial_get_payload(self, folder, shape, additional_fields, sync_state): svc_elem = create_element('m:%s' % self.SERVICE_NAME) foldershape = create_shape_element( tag=self.shape_tag, shape=shape, additional_fields=additional_fields, version=self.account.version ) svc_elem.append(foldershape) folder_id = create_folder_ids_element(tag='m:SyncFolderId', folders=[folder], version=self.account.version) svc_elem.append(folder_id) if sync_state: add_xml_child(svc_elem, 'm:SyncState', sync_state) return svc_elem class SyncFolderHierarchy(SyncFolder): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderhierarchy-operation """ SERVICE_NAME = 'SyncFolderHierarchy' shape_tag = 'm:FolderShape' last_in_range_name = '{%s}IncludesLastFolderInRange' % MNS def call(self, folder, shape, additional_fields, sync_state): self.sync_state = sync_state change_types = self._change_types_map() for elem in self._get_elements(payload=self.get_payload( folder=folder, shape=shape, additional_fields=additional_fields, sync_state=sync_state, )): if isinstance(elem, Exception): yield elem continue change_type = change_types[elem.tag] if change_type == self.DELETE: folder = FolderId.from_xml(elem=elem.find(FolderId.response_tag()), account=self.account) else: # We can't find() the element because we don't know which tag to look for. The change element can # contain multiple folder types, each with their own tag. folder_elem = elem[0] folder = parse_folder_elem(elem=folder_elem, folder=folder, account=self.account) yield change_type, folder def get_payload(self, folder, shape, additional_fields, sync_state): return self._partial_get_payload( folder=folder, shape=shape, additional_fields=additional_fields, sync_state=sync_state ) exchangelib-4.6.1/exchangelib/services/sync_folder_items.py000066400000000000000000000071001414601472700241550ustar00rootroot00000000000000from .common import add_xml_child, create_item_ids_element from .sync_folder_hierarchy import SyncFolder from ..properties import ItemId from ..util import xml_text_to_value, peek, TNS, MNS class SyncFolderItems(SyncFolder): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderitems-operation """ SERVICE_NAME = 'SyncFolderItems' SYNC_SCOPES = { 'NormalItems', 'NormalAndAssociatedItems', } # Extra change type READ_FLAG_CHANGE = 'read_flag_change' CHANGE_TYPES = SyncFolder.CHANGE_TYPES + (READ_FLAG_CHANGE,) shape_tag = 'm:ItemShape' last_in_range_name = '{%s}IncludesLastItemInRange' % MNS def _change_types_map(self): res = super()._change_types_map() res['{%s}ReadFlagChange' % TNS] = self.READ_FLAG_CHANGE return res def call(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope): self.sync_state = sync_state if max_changes_returned is None: max_changes_returned = self.chunk_size if max_changes_returned <= 0: raise ValueError("'max_changes_returned' %s must be a positive integer" % max_changes_returned) if sync_scope is not None and sync_scope not in self.SYNC_SCOPES: raise ValueError("'sync_scope' %s must be one of %r" % (sync_scope, self.SYNC_SCOPES)) return self._elems_to_objs(self._get_elements(payload=self.get_payload( folder=folder, shape=shape, additional_fields=additional_fields, sync_state=sync_state, ignore=ignore, max_changes_returned=max_changes_returned, sync_scope=sync_scope, ))) def _elems_to_objs(self, elems): from ..folders.base import BaseFolder change_types = self._change_types_map() for elem in elems: if isinstance(elem, Exception): yield elem continue change_type = change_types[elem.tag] if change_type == self.READ_FLAG_CHANGE: item = ( ItemId.from_xml(elem=elem.find(ItemId.response_tag()), account=self.account), xml_text_to_value(elem.find('{%s}IsRead' % TNS).text, bool) ) elif change_type == self.DELETE: item = ItemId.from_xml(elem=elem.find(ItemId.response_tag()), account=self.account) else: # We can't find() the element because we don't know which tag to look for. The change element can # contain multiple item types, each with their own tag. item_elem = elem[0] item = BaseFolder.item_model_from_tag(item_elem.tag).from_xml(elem=item_elem, account=self.account) yield change_type, item def get_payload(self, folder, shape, additional_fields, sync_state, ignore, max_changes_returned, sync_scope): syncfolderitems = self._partial_get_payload( folder=folder, shape=shape, additional_fields=additional_fields, sync_state=sync_state ) is_empty, ignore = (True, None) if ignore is None else peek(ignore) if not is_empty: item_ids = create_item_ids_element(items=ignore, version=self.account.version, tag='m:Ignore') syncfolderitems.append(item_ids) add_xml_child(syncfolderitems, 'm:MaxChangesReturned', max_changes_returned) if sync_scope: add_xml_child(syncfolderitems, 'm:SyncScope', sync_scope) return syncfolderitems exchangelib-4.6.1/exchangelib/services/unsubscribe.py000066400000000000000000000013251414601472700227740ustar00rootroot00000000000000from .common import EWSAccountService, add_xml_child from ..util import create_element class Unsubscribe(EWSAccountService): """Unsubscribing is only valid for pull and streaming notifications. MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/unsubscribe-operation """ SERVICE_NAME = 'Unsubscribe' returns_elements = False def call(self, subscription_id): return self._get_elements(payload=self.get_payload(subscription_id=subscription_id)) def get_payload(self, subscription_id): unsubscribe = create_element('m:%s' % self.SERVICE_NAME) add_xml_child(unsubscribe, 'm:SubscriptionId', subscription_id) return unsubscribe exchangelib-4.6.1/exchangelib/services/update_folder.py000066400000000000000000000106601414601472700232670ustar00rootroot00000000000000from .common import EWSAccountService, parse_folder_elem, to_item_id from ..fields import FieldPath from ..util import create_element, set_xml_value, MNS class UpdateFolder(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updatefolder-operation""" SERVICE_NAME = 'UpdateFolder' element_container_name = '{%s}Folders' % MNS def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.folders = [] # A hack to communicate parsing args to _elems_to_objs() def call(self, folders): # We can't easily find the correct folder class from the returned XML. Instead, return objects with the same # class as the folder instance it was requested with. self.folders = list(folders) # Convert to a list, in case 'folders' is a generator. We're iterating twice. return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=self.folders)) def _elems_to_objs(self, elems): for (folder, _), elem in zip(self.folders, elems): if isinstance(elem, Exception): yield elem continue yield parse_folder_elem(elem=elem, folder=folder, account=self.account) @staticmethod def _sort_fieldnames(folder_model, fieldnames): # Take a list of fieldnames and return the fields in the order they are mentioned in folder_model.FIELDS. # Loop over FIELDS and not supported_fields(). Upstream should make sure not to update a non-supported field. for f in folder_model.FIELDS: if f.name in fieldnames: yield f.name def _set_folder_elem(self, folder_model, field_path, value): setfolderfield = create_element('t:SetFolderField') set_xml_value(setfolderfield, field_path, version=self.account.version) folder = create_element(folder_model.request_tag()) field_elem = field_path.field.to_xml(value, version=self.account.version) set_xml_value(folder, field_elem, version=self.account.version) setfolderfield.append(folder) return setfolderfield def _delete_folder_elem(self, field_path): deletefolderfield = create_element('t:DeleteFolderField') return set_xml_value(deletefolderfield, field_path, version=self.account.version) def _get_folder_update_elems(self, folder, fieldnames): folder_model = folder.__class__ fieldnames_set = set(fieldnames) for fieldname in self._sort_fieldnames(folder_model=folder_model, fieldnames=fieldnames_set): field = folder_model.get_field_by_fieldname(fieldname) if field.is_read_only: raise ValueError('%s is a read-only field' % field.name) value = field.clean(getattr(folder, field.name), version=self.account.version) # Make sure the value is OK if value is None or (field.is_list and not value): # A value of None or [] means we want to remove this field from the item if field.is_required or field.is_required_after_save: raise ValueError('%s is a required field and may not be deleted' % field.name) for field_path in FieldPath(field=field).expand(version=self.account.version): yield self._delete_folder_elem(field_path=field_path) continue yield self._set_folder_elem(folder_model=folder_model, field_path=FieldPath(field=field), value=value) def get_payload(self, folders): from ..folders import BaseFolder, FolderId updatefolder = create_element('m:%s' % self.SERVICE_NAME) folderchanges = create_element('m:FolderChanges') version = self.account.version for folder, fieldnames in folders: folderchange = create_element('t:FolderChange') if not isinstance(folder, (BaseFolder, FolderId)): folder = to_item_id(folder, FolderId, version=version) set_xml_value(folderchange, folder, version=version) updates = create_element('t:Updates') for elem in self._get_folder_update_elems(folder=folder, fieldnames=fieldnames): updates.append(elem) folderchange.append(updates) folderchanges.append(folderchange) if not len(folderchanges): raise ValueError('"folders" must not be empty') updatefolder.append(folderchanges) return updatefolder exchangelib-4.6.1/exchangelib/services/update_item.py000066400000000000000000000244211414601472700227520ustar00rootroot00000000000000from collections import OrderedDict from .common import EWSAccountService, to_item_id from ..ewsdatetime import EWSDate from ..fields import FieldPath, IndexedField from ..properties import ItemId from ..util import create_element, set_xml_value, MNS from ..version import EXCHANGE_2013_SP1 class UpdateItem(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation""" SERVICE_NAME = 'UpdateItem' element_container_name = '{%s}Items' % MNS def call(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, suppress_read_receipts): from ..items import CONFLICT_RESOLUTION_CHOICES, MESSAGE_DISPOSITION_CHOICES, \ SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES, SEND_ONLY if conflict_resolution not in CONFLICT_RESOLUTION_CHOICES: raise ValueError("'conflict_resolution' %s must be one of %s" % ( conflict_resolution, CONFLICT_RESOLUTION_CHOICES )) if message_disposition not in MESSAGE_DISPOSITION_CHOICES: raise ValueError("'message_disposition' %s must be one of %s" % ( message_disposition, MESSAGE_DISPOSITION_CHOICES )) if send_meeting_invitations_or_cancellations not in SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES: raise ValueError("'send_meeting_invitations_or_cancellations' %s must be one of %s" % ( send_meeting_invitations_or_cancellations, SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES )) if suppress_read_receipts not in (True, False): raise ValueError("'suppress_read_receipts' %s must be True or False" % suppress_read_receipts) if message_disposition == SEND_ONLY: raise ValueError('Cannot send-only existing objects. Use SendItem service instead') return self._elems_to_objs(self._chunked_get_elements( self.get_payload, items=items, 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 _elems_to_objs(self, elems): from ..items import Item for elem in elems: if isinstance(elem, (Exception, type(None))): yield elem continue yield Item.id_from_xml(elem) def _delete_item_elem(self, field_path): deleteitemfield = create_element('t:DeleteItemField') return set_xml_value(deleteitemfield, field_path, version=self.account.version) def _set_item_elem(self, item_model, field_path, value): setitemfield = create_element('t:SetItemField') set_xml_value(setitemfield, field_path, version=self.account.version) item_elem = create_element(item_model.request_tag()) field_elem = field_path.field.to_xml(value, version=self.account.version) set_xml_value(item_elem, field_elem, version=self.account.version) setitemfield.append(item_elem) return setitemfield @staticmethod def _sorted_fields(item_model, fieldnames): # Take a list of fieldnames and return the (unique) fields in the order they are mentioned in item_class.FIELDS. # Checks that all fieldnames are valid. unique_fieldnames = list(OrderedDict.fromkeys(fieldnames)) # Make field names unique ,but keep ordering # Loop over FIELDS and not supported_fields(). Upstream should make sure not to update a non-supported field. for f in item_model.FIELDS: if f.name in unique_fieldnames: unique_fieldnames.remove(f.name) yield f if unique_fieldnames: raise ValueError("Field name(s) %s are not valid for a '%s' item" % ( ', '.join("'%s'" % f for f in unique_fieldnames), item_model.__name__)) def _get_item_update_elems(self, item, fieldnames): from ..items import CalendarItem fieldnames_copy = list(fieldnames) if item.__class__ == CalendarItem: # For CalendarItem items where we update 'start' or 'end', we want to update internal timezone fields item.clean_timezone_fields(version=self.account.version) # Possibly also sets timezone values for field_name in ('start', 'end'): if field_name in fieldnames_copy: tz_field_name = item.tz_field_for_field_name(field_name).name if tz_field_name not in fieldnames_copy: fieldnames_copy.append(tz_field_name) for field in self._sorted_fields(item_model=item.__class__, fieldnames=fieldnames_copy): if field.is_read_only: raise ValueError('%s is a read-only field' % field.name) value = self._get_item_value(item, field) if value is None or (field.is_list and not value): # A value of None or [] means we want to remove this field from the item yield from self._get_delete_item_elems(field=field) else: yield from self._get_set_item_elems(item_model=item.__class__, field=field, value=value) def _get_item_value(self, item, field): from ..items import CalendarItem value = field.clean(getattr(item, field.name), version=self.account.version) # Make sure the value is OK if item.__class__ == CalendarItem: # For CalendarItem items where we update 'start' or 'end', we want to send values in the local timezone if field.name in ('start', 'end'): if type(value) is EWSDate: # EWS always expects a datetime return item.date_to_datetime(field_name=field.name) tz_field_name = item.tz_field_for_field_name(field.name).name return value.astimezone(getattr(item, tz_field_name)) return value def _get_delete_item_elems(self, field): if field.is_required or field.is_required_after_save: raise ValueError('%s is a required field and may not be deleted' % field.name) for field_path in FieldPath(field=field).expand(version=self.account.version): yield self._delete_item_elem(field_path=field_path) def _get_set_item_elems(self, item_model, field, value): if isinstance(field, IndexedField): # Generate either set or delete elements for all combinations of labels and subfields supported_labels = field.value_cls.get_field_by_fieldname('label')\ .supported_choices(version=self.account.version) seen_labels = set() subfields = field.value_cls.supported_fields(version=self.account.version) for v in value: seen_labels.add(v.label) for subfield in subfields: field_path = FieldPath(field=field, label=v.label, subfield=subfield) subfield_value = getattr(v, subfield.name) if not subfield_value: # Generate delete elements for blank subfield values yield self._delete_item_elem(field_path=field_path) else: # Generate set elements for non-null subfield values yield self._set_item_elem( item_model=item_model, field_path=field_path, value=field.value_cls(**{'label': v.label, subfield.name: subfield_value}), ) # Generate delete elements for all subfields of all labels not mentioned in the list of values for label in (label for label in supported_labels if label not in seen_labels): for subfield in subfields: yield self._delete_item_elem(field_path=FieldPath(field=field, label=label, subfield=subfield)) else: yield self._set_item_elem(item_model=item_model, field_path=FieldPath(field=field), value=value) def get_payload(self, items, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, suppress_read_receipts): # Takes a list of (Item, fieldnames) tuples where 'Item' is a instance of a subclass of Item and 'fieldnames' # are the attribute names that were updated. Returns the XML for an UpdateItem call. # an UpdateItem request. if self.account.version.build >= EXCHANGE_2013_SP1: updateitem = create_element( 'm:%s' % self.SERVICE_NAME, attrs=OrderedDict([ ('ConflictResolution', conflict_resolution), ('MessageDisposition', message_disposition), ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations), ('SuppressReadReceipts', 'true' if suppress_read_receipts else 'false'), ]) ) else: updateitem = create_element( 'm:%s' % self.SERVICE_NAME, attrs=OrderedDict([ ('ConflictResolution', conflict_resolution), ('MessageDisposition', message_disposition), ('SendMeetingInvitationsOrCancellations', send_meeting_invitations_or_cancellations), ]) ) itemchanges = create_element('m:ItemChanges') version = self.account.version for item, fieldnames in items: if not item.account: item.account = self.account if not fieldnames: raise ValueError('"fieldnames" must not be empty') itemchange = create_element('t:ItemChange') set_xml_value(itemchange, to_item_id(item, ItemId, version=version), version=version) updates = create_element('t:Updates') for elem in self._get_item_update_elems(item=item, fieldnames=fieldnames): updates.append(elem) itemchange.append(updates) itemchanges.append(itemchange) if not len(itemchanges): raise ValueError('"items" must not be empty') updateitem.append(itemchanges) return updateitem exchangelib-4.6.1/exchangelib/services/update_user_configuration.py000066400000000000000000000013631414601472700257210ustar00rootroot00000000000000from .common import EWSAccountService from ..util import create_element, set_xml_value class UpdateUserConfiguration(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateuserconfiguration-operation """ SERVICE_NAME = 'UpdateUserConfiguration' returns_elements = False def call(self, user_configuration): return self._get_elements(payload=self.get_payload(user_configuration=user_configuration)) def get_payload(self, user_configuration): updateuserconfiguration = create_element('m:%s' % self.SERVICE_NAME) set_xml_value(updateuserconfiguration, user_configuration, version=self.account.version) return updateuserconfiguration exchangelib-4.6.1/exchangelib/services/upload_items.py000066400000000000000000000045041414601472700231370ustar00rootroot00000000000000from .common import EWSAccountService, to_item_id from ..properties import ItemId, ParentFolderId from ..util import create_element, set_xml_value, add_xml_child, MNS class UploadItems(EWSAccountService): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/uploaditems-operation """ SERVICE_NAME = 'UploadItems' element_container_name = '{%s}ItemId' % MNS def call(self, items): # _pool_requests expects 'items', not 'data' return self._elems_to_objs(self._chunked_get_elements(self.get_payload, items=items)) def get_payload(self, items): """Upload given items to given account. 'items' is an iterable of tuples where the first element is a Folder instance representing the ParentFolder that the item will be placed in and the second element is a tuple containing an optional ItemId, an optional Item.is_associated boolean, and a Data string returned from an ExportItems. call. :param items: """ uploaditems = create_element('m:%s' % self.SERVICE_NAME) itemselement = create_element('m:Items') uploaditems.append(itemselement) for parent_folder, (item_id, is_associated, data_str) in items: # TODO: The full spec also allows the "UpdateOrCreate" create action. item = create_element('t:Item', attrs=dict(CreateAction='Update' if item_id else 'CreateNew')) if is_associated is not None: item.set('IsAssociated', 'true' if is_associated else 'false') parentfolderid = ParentFolderId(parent_folder.id, parent_folder.changekey) set_xml_value(item, parentfolderid, version=self.account.version) if item_id: itemid = to_item_id(item_id, ItemId, version=self.account.version) set_xml_value(item, itemid, version=self.account.version) add_xml_child(item, 't:Data', data_str) itemselement.append(item) return uploaditems def _elems_to_objs(self, elems): for elem in elems: if isinstance(elem, Exception): yield elem continue yield elem.get(ItemId.ID_ATTR), elem.get(ItemId.CHANGEKEY_ATTR) @classmethod def _get_elements_in_container(cls, container): return [container] exchangelib-4.6.1/exchangelib/settings.py000066400000000000000000000077021414601472700204720ustar00rootroot00000000000000import datetime from .ewsdatetime import UTC from .fields import DateTimeField, MessageField, ChoiceField, Choice from .properties import EWSElement, OutOfOffice from .util import create_element, set_xml_value class OofSettings(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/oofsettings""" ELEMENT_NAME = 'OofSettings' REQUEST_ELEMENT_NAME = 'UserOofSettings' ENABLED = 'Enabled' SCHEDULED = 'Scheduled' DISABLED = 'Disabled' STATE_CHOICES = (ENABLED, SCHEDULED, DISABLED) state = ChoiceField(field_uri='OofState', is_required=True, choices={Choice(c) for c in STATE_CHOICES}) external_audience = ChoiceField(field_uri='ExternalAudience', choices={Choice('None'), Choice('Known'), Choice('All')}, default='All') start = DateTimeField(field_uri='StartTime') end = DateTimeField(field_uri='EndTime') internal_reply = MessageField(field_uri='InternalReply') external_reply = MessageField(field_uri='ExternalReply') def clean(self, version=None): super().clean(version=version) if self.state == self.SCHEDULED: if not self.start or not self.end: raise ValueError("'start' and 'end' must be set when state is '%s'" % self.SCHEDULED) if self.start >= self.end: raise ValueError("'start' must be before 'end'") if self.end < datetime.datetime.now(tz=UTC): raise ValueError("'end' must be in the future") if self.state != self.DISABLED and (not self.internal_reply or not self.external_reply): raise ValueError("'internal_reply' and 'external_reply' must be set when state is not '%s'" % self.DISABLED) @classmethod def from_xml(cls, elem, account): kwargs = {} for attr in ('state', 'external_audience', 'internal_reply', 'external_reply'): f = cls.get_field_by_fieldname(attr) kwargs[attr] = f.from_xml(elem=elem, account=account) kwargs.update(OutOfOffice.duration_to_start_end(elem=elem, account=account)) cls._clear(elem) return cls(**kwargs) def to_xml(self, version): self.clean(version=version) elem = create_element('t:%s' % self.REQUEST_ELEMENT_NAME) for attr in ('state', 'external_audience'): value = getattr(self, attr) if value is None: continue f = self.get_field_by_fieldname(attr) set_xml_value(elem, f.to_xml(value, version=version), version=version) if self.start or self.end: duration = create_element('t:Duration') if self.start: f = self.get_field_by_fieldname('start') set_xml_value(duration, f.to_xml(self.start, version=version), version) if self.end: f = self.get_field_by_fieldname('end') set_xml_value(duration, f.to_xml(self.end, version=version), version) elem.append(duration) for attr in ('internal_reply', 'external_reply'): value = getattr(self, attr) if value is None: value = '' # The value can be empty, but the XML element must always be present f = self.get_field_by_fieldname(attr) set_xml_value(elem, f.to_xml(value, version=version), version) return elem def __hash__(self): # Customize comparison if self.state == self.DISABLED: # All values except state are ignored by the server relevant_attrs = ('state',) elif self.state != self.SCHEDULED: # 'start' and 'end' values are ignored by the server, and the server always returns today's date relevant_attrs = tuple(f.name for f in self.FIELDS if f.name not in ('start', 'end')) else: relevant_attrs = tuple(f.name for f in self.FIELDS) return hash(tuple(getattr(self, attr) for attr in relevant_attrs)) exchangelib-4.6.1/exchangelib/transport.py000066400000000000000000000215331414601472700206640ustar00rootroot00000000000000import logging import time import requests.auth import requests_ntlm import requests_oauthlib from .errors import UnauthorizedError, TransportError from .util import create_element, add_xml_child, xml_to_str, ns_translation, _back_off_if_needed, \ _retry_after, DummyResponse, CONNECTION_ERRORS log = logging.getLogger(__name__) # Authentication method enums NOAUTH = 'no authentication' NTLM = 'NTLM' BASIC = 'basic' DIGEST = 'digest' GSSAPI = 'gssapi' SSPI = 'sspi' OAUTH2 = 'OAuth 2.0' CBA = 'CBA' # Certificate Based Authentication # The auth types that must be accompanied by a credentials object CREDENTIALS_REQUIRED = (NTLM, BASIC, DIGEST, OAUTH2) AUTH_TYPE_MAP = { NTLM: requests_ntlm.HttpNtlmAuth, BASIC: requests.auth.HTTPBasicAuth, DIGEST: requests.auth.HTTPDigestAuth, OAUTH2: requests_oauthlib.OAuth2, CBA: None, NOAUTH: None, } try: import requests_gssapi AUTH_TYPE_MAP[GSSAPI] = requests_gssapi.HTTPSPNEGOAuth except ImportError: # Kerberos auth is optional pass try: import requests_negotiate_sspi AUTH_TYPE_MAP[SSPI] = requests_negotiate_sspi.HttpNegotiateAuth except ImportError: # SSPI auth is optional pass DEFAULT_ENCODING = 'utf-8' DEFAULT_HEADERS = {'Content-Type': 'text/xml; charset=%s' % DEFAULT_ENCODING, 'Accept-Encoding': 'gzip, deflate'} def wrap(content, api_version, account_to_impersonate=None, timezone=None): """Generate the necessary boilerplate XML for a raw SOAP request. The XML is specific to the server version. ExchangeImpersonation allows to act as the user we want to impersonate. RequestServerVersion element on MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/requestserverversion ExchangeImpersonation element on MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/exchangeimpersonation TimeZoneContent element on MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/timezonecontext :param content: :param api_version: :param account_to_impersonate: (Default value = None) :param timezone: (Default value = None) """ envelope = create_element('s:Envelope', nsmap=ns_translation) header = create_element('s:Header') requestserverversion = create_element('t:RequestServerVersion', attrs=dict(Version=api_version)) header.append(requestserverversion) if account_to_impersonate: exchangeimpersonation = create_element('t:ExchangeImpersonation') connectingsid = create_element('t:ConnectingSID') # 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 for attr, tag in ( ('sid', 'SID'), ('upn', 'PrincipalName'), ('smtp_address', 'SmtpAddress'), ('primary_smtp_address', 'PrimarySmtpAddress'), ): val = getattr(account_to_impersonate, attr) if val: add_xml_child(connectingsid, 't:%s' % tag, val) break exchangeimpersonation.append(connectingsid) header.append(exchangeimpersonation) if timezone: timezonecontext = create_element('t:TimeZoneContext') timezonedefinition = create_element('t:TimeZoneDefinition', attrs=dict(Id=timezone.ms_id)) timezonecontext.append(timezonedefinition) header.append(timezonecontext) envelope.append(header) body = create_element('s:Body') body.append(content) envelope.append(body) return xml_to_str(envelope, encoding=DEFAULT_ENCODING, xml_declaration=True) def get_auth_instance(auth_type, **kwargs): """Return an *Auth instance suitable for the requests package. :param auth_type: :param kwargs: """ model = AUTH_TYPE_MAP[auth_type] if model is None: return None if auth_type == GSSAPI: # Kerberos auth relies on credentials supplied via a ticket available externally to this library return model() if auth_type == SSPI: # SSPI auth does not require credentials, but can have it return model(**kwargs) return model(**kwargs) def get_service_authtype(service_endpoint, retry_policy, api_versions, name): # Get auth type by tasting headers from the server. Only do POST requests. HEAD is too error prone, and some servers # are set up to redirect to OWA on all requests except POST to /EWS/Exchange.asmx # # We don't know the API version yet, but we need it to create a valid request because some Exchange servers only # respond when given a valid request. Try all known versions. Gross. from .protocol import BaseProtocol retry = 0 wait = 10 # seconds t_start = time.monotonic() headers = DEFAULT_HEADERS.copy() for api_version in api_versions: data = dummy_xml(api_version=api_version, name=name) log.debug('Requesting %s from %s', data, service_endpoint) while True: _back_off_if_needed(retry_policy.back_off_until) log.debug('Trying to get service auth type for %s', service_endpoint) with BaseProtocol.raw_session(service_endpoint) as s: try: r = s.post(url=service_endpoint, headers=headers, data=data, allow_redirects=False, timeout=BaseProtocol.TIMEOUT) r.close() # Release memory break except CONNECTION_ERRORS as e: # Don't retry on TLS errors. They will most likely be persistent. total_wait = time.monotonic() - t_start r = DummyResponse(url=service_endpoint, headers={}, request_headers=headers) if retry_policy.may_retry_on_error(response=r, wait=total_wait): wait = _retry_after(r, wait) log.info("Connection error on URL %s (retry %s, error: %s). Cool down %s secs", service_endpoint, retry, e, wait) retry_policy.back_off(wait) retry += 1 continue raise TransportError(str(e)) from e if r.status_code not in (200, 401): log.debug('Unexpected response: %s %s', r.status_code, r.reason) continue try: auth_type = get_auth_method_from_response(response=r) log.debug('Auth type is %s', auth_type) return auth_type, api_version except UnauthorizedError: continue raise TransportError('Failed to get auth type from service') def get_auth_method_from_response(response): # First, get the auth method from headers. Then, test credentials. Don't handle redirects - burden is on caller. log.debug('Request headers: %s', response.request.headers) log.debug('Response headers: %s', response.headers) if response.status_code == 200: return NOAUTH # Get auth type from headers for key, val in response.headers.items(): if key.lower() == 'www-authenticate': # Requests will combine multiple HTTP headers into one in 'request.headers' vals = _tokenize(val.lower()) for v in vals: if v.startswith('realm'): realm = v.split('=')[1].strip('"') log.debug('realm: %s', realm) # Prefer most secure auth method if more than one is offered. See discussion at # http://docs.oracle.com/javase/7/docs/technotes/guides/net/http-auth.html if 'digest' in vals: return DIGEST if 'ntlm' in vals: return NTLM if 'basic' in vals: return BASIC raise UnauthorizedError('No compatible auth type was reported by server') def _tokenize(val): # Splits cookie auth values auth_methods = [] auth_method = '' quote = False for c in val: if c in (' ', ',') and not quote: if auth_method not in ('', ','): auth_methods.append(auth_method) auth_method = '' continue if c == '"': auth_method += c if quote: auth_methods.append(auth_method) auth_method = '' quote = not quote continue auth_method += c if auth_method: auth_methods.append(auth_method) return auth_methods def dummy_xml(api_version, name): # Generate a minimal, valid EWS request from .services import ResolveNames # Avoid circular import return wrap(content=ResolveNames(protocol=None).get_payload( unresolved_entries=[name], parent_folders=None, return_full_contact_data=False, search_scope=None, contact_data_shape=None, ), api_version=api_version) exchangelib-4.6.1/exchangelib/util.py000066400000000000000000001072701414601472700176100ustar00rootroot00000000000000import datetime import io import itertools import logging import re import socket import time import xml.sax.handler # nosec from base64 import b64decode, b64encode from codecs import BOM_UTF8 from collections import OrderedDict from decimal import Decimal from functools import wraps from threading import get_ident from urllib.parse import urlparse import isodate import lxml.etree # nosec import requests.exceptions from defusedxml.expatreader import DefusedExpatParser from defusedxml.sax import _InputSource from oauthlib.oauth2 import TokenExpiredError from pygments import highlight from pygments.formatters.terminal import TerminalFormatter from pygments.lexers.html import XmlLexer from .errors import TransportError, RateLimitError, RedirectError, RelativeRedirect, MalformedResponseError log = logging.getLogger(__name__) xml_log = logging.getLogger('%s.xml' % __name__) def require_account(f): @wraps(f) def wrapper(self, *args, **kwargs): if not self.account: raise ValueError('%s must have an account' % self.__class__.__name__) return f(self, *args, **kwargs) return wrapper def require_id(f): @wraps(f) def wrapper(self, *args, **kwargs): if not self.account: raise ValueError('%s must have an account' % self.__class__.__name__) if not self.id: raise ValueError('%s must have an ID' % self.__class__.__name__) return f(self, *args, **kwargs) return wrapper class ParseError(lxml.etree.ParseError): """Used to wrap lxml ParseError in our own class.""" class ElementNotFound(Exception): """Raised when the expected element was not found in a response stream.""" def __init__(self, msg, data): super().__init__(msg) self.data = data # Regex of UTF-8 control characters that are illegal in XML 1.0 (and XML 1.1) _ILLEGAL_XML_CHARS_RE = re.compile('[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]') # XML namespaces SOAPNS = 'http://schemas.xmlsoap.org/soap/envelope/' MNS = 'http://schemas.microsoft.com/exchange/services/2006/messages' TNS = 'http://schemas.microsoft.com/exchange/services/2006/types' ENS = 'http://schemas.microsoft.com/exchange/services/2006/errors' AUTODISCOVER_BASE_NS = 'http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006' AUTODISCOVER_REQUEST_NS = 'http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006' AUTODISCOVER_RESPONSE_NS = 'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a' ns_translation = OrderedDict([ ('s', SOAPNS), ('m', MNS), ('t', TNS), ]) for item in ns_translation.items(): lxml.etree.register_namespace(*item) def is_iterable(value, generators_allowed=False): """Check if value is a list-like object. Don't match generators and generator-like objects here by default, because callers don't necessarily guarantee that they only iterate the value once. Take care to not match string types and bytes. :param value: any type of object :param generators_allowed: if True, generators will be treated as iterable (Default value = False) :return: True or False """ if generators_allowed: if not isinstance(value, (bytes, str)) and hasattr(value, '__iter__'): return True else: if isinstance(value, (tuple, list, set)): return True return False def chunkify(iterable, chunksize): """Split an iterable into chunks of size ``chunksize``. The last chunk may be smaller than ``chunksize``. :param iterable: :param chunksize: :return: """ from .queryset import QuerySet if hasattr(iterable, '__getitem__') and not isinstance(iterable, QuerySet): # tuple, list. QuerySet has __getitem__ but that evaluates the entire query greedily. We don't want that here. for i in range(0, len(iterable), chunksize): yield iterable[i:i + chunksize] else: # generator, set, map, QuerySet chunk = [] for i in iterable: chunk.append(i) if len(chunk) == chunksize: yield chunk chunk = [] if chunk: yield chunk def peek(iterable): """Check if an iterable is empty and return status and the rewinded iterable. :param iterable: :return: """ if hasattr(iterable, '__len__'): # tuple, list, set return not iterable, iterable # generator try: first = next(iterable) except StopIteration: return True, iterable # We can't rewind a generator. Instead, chain the first element and the rest of the generator return False, itertools.chain([first], iterable) def xml_to_str(tree, encoding=None, xml_declaration=False): """Serialize an XML tree. Returns unicode if 'encoding' is None. Otherwise, we return encoded 'bytes'. :param tree: :param encoding: (Default value = None) :param xml_declaration: (Default value = False) :return: """ if xml_declaration and not encoding: raise ValueError("'xml_declaration' is not supported when 'encoding' is None") if encoding: return lxml.etree.tostring(tree, encoding=encoding, xml_declaration=True) return lxml.etree.tostring(tree, encoding=str, xml_declaration=False) def get_xml_attr(tree, name): elem = tree.find(name) if elem is None: # Must compare with None, see XML docs return None return elem.text or None def get_xml_attrs(tree, name): return [elem.text for elem in tree.findall(name) if elem.text is not None] def value_to_xml_text(value): from .ewsdatetime import EWSTimeZone, EWSDateTime, EWSDate from .indexed_properties import PhoneNumber, EmailAddress from .properties import Mailbox, AssociatedCalendarItemId, Attendee, ConversationId # We can't just create a map and look up with type(value) because we want to support subtypes if isinstance(value, str): return safe_xml_value(value) if isinstance(value, bool): return '1' if value else '0' if isinstance(value, bytes): return b64encode(value).decode('ascii') if isinstance(value, (int, Decimal)): return str(value) if isinstance(value, datetime.time): return value.isoformat() if isinstance(value, EWSTimeZone): return value.ms_id if isinstance(value, EWSDateTime): return value.ewsformat() if isinstance(value, EWSDate): return value.ewsformat() if isinstance(value, PhoneNumber): return value.phone_number if isinstance(value, EmailAddress): return value.email if isinstance(value, Mailbox): return value.email_address if isinstance(value, Attendee): return value.mailbox.email_address if isinstance(value, ConversationId): return value.id if isinstance(value, AssociatedCalendarItemId): return value.id raise TypeError('Unsupported type: %s (%s)' % (type(value), value)) def xml_text_to_value(value, value_type): from .ewsdatetime import EWSDate, EWSDateTime if value_type == str: return value if value_type == bool: try: return { 'true': True, 'on': True, 'false': False, 'off': False, }[value.lower()] except KeyError: return None return { bytes: safe_b64decode, int: int, Decimal: Decimal, datetime.timedelta: isodate.parse_duration, EWSDate: EWSDate.from_string, EWSDateTime: EWSDateTime.from_string, }[value_type](value) def set_xml_value(elem, value, version): from .ewsdatetime import EWSDateTime, EWSDate from .fields import FieldPath, FieldOrder from .properties import EWSElement from .version import Version if isinstance(value, (str, bool, bytes, int, Decimal, datetime.time, EWSDate, EWSDateTime)): elem.text = value_to_xml_text(value) elif isinstance(value, _element_class): elem.append(value) elif is_iterable(value, generators_allowed=True): for v in value: if isinstance(v, (FieldPath, FieldOrder)): elem.append(v.to_xml()) elif isinstance(v, EWSElement): if not isinstance(version, Version): raise ValueError("'version' %r must be a Version instance" % version) elem.append(v.to_xml(version=version)) elif isinstance(v, _element_class): elem.append(v) elif isinstance(v, str): add_xml_child(elem, 't:String', v) else: raise ValueError('Unsupported type %s for list element %s on elem %s' % (type(v), v, elem)) elif isinstance(value, (FieldPath, FieldOrder)): elem.append(value.to_xml()) elif isinstance(value, EWSElement): if not isinstance(version, Version): raise ValueError("'version' %r must be a Version instance" % version) elem.append(value.to_xml(version=version)) else: raise ValueError('Unsupported type %s for value %s on elem %s' % (type(value), value, elem)) return elem def safe_xml_value(value, replacement='?'): return _ILLEGAL_XML_CHARS_RE.sub(replacement, value) def create_element(name, attrs=None, nsmap=None): # Python versions prior to 3.6 do not preserve dict or kwarg ordering, so we cannot pull in attrs as **kwargs if we # also want stable XML attribute output. Instead, let callers supply us with an OrderedDict instance. if ':' in name: ns, name = name.split(':') name = '{%s}%s' % (ns_translation[ns], name) elem = _forgiving_parser.makeelement(name, nsmap=nsmap) if attrs: # Try hard to keep attribute order, to ensure deterministic output. This simplifies testing. for k, v in attrs.items(): elem.set(k, v) return elem def add_xml_child(tree, name, value): # We're calling add_xml_child many places where we don't have the version handy. Don't pass EWSElement or list of # EWSElement to this function! tree.append(set_xml_value(elem=create_element(name), value=value, version=None)) class StreamingContentHandler(xml.sax.handler.ContentHandler): """A SAX content handler that returns a character data for a single element back to the parser. The parser must have a 'buffer' attribute we can append data to. """ def __init__(self, parser, ns, element_name): xml.sax.handler.ContentHandler.__init__(self) self._parser = parser self._ns = ns self._element_name = element_name self._parsing = False def startElementNS(self, name, qname, attrs): if name == (self._ns, self._element_name): # we can expect element data next self._parsing = True self._parser.element_found = True def endElementNS(self, name, qname): if name == (self._ns, self._element_name): # all element data received self._parsing = False def characters(self, content): if not self._parsing: return self._parser.buffer.append(content) def prepare_input_source(source): # Extracted from xml.sax.expatreader.saxutils.prepare_input_source f = source source = _InputSource() source.setByteStream(f) return source def safe_b64decode(data): # Incoming base64-encoded data is not always padded to a multiple of 4. Python's parser is more strict and requires # padding. Add padding if it's needed. overflow = len(data) % 4 if overflow: if isinstance(data, str): padding = '=' * (4 - overflow) else: padding = b'=' * (4 - overflow) data += padding return b64decode(data) class StreamingBase64Parser(DefusedExpatParser): """A SAX parser that returns a generator of base64-decoded character content.""" def __init__(self, *args, **kwargs): DefusedExpatParser.__init__(self, *args, **kwargs) self._namespaces = True self.buffer = None self.element_found = None def parse(self, r): raw_source = r.raw # Like upstream but yields the return value of self.feed() raw_source = prepare_input_source(raw_source) self.prepareParser(raw_source) file = raw_source.getByteStream() self.buffer = [] self.element_found = False buffer = file.read(self._bufsize) collected_data = [] while buffer: if not self.element_found: collected_data += buffer yield from self.feed(buffer) buffer = file.read(self._bufsize) # Any remaining data in self.buffer should be padding chars now self.buffer = None self.close() if not self.element_found: data = bytes(collected_data) raise ElementNotFound('The element to be streamed from was not found', data=bytes(data)) def feed(self, data, isFinal=0): """Yield the current content of the character buffer.""" DefusedExpatParser.feed(self, data=data, isFinal=isFinal) return self._decode_buffer() def _decode_buffer(self): remainder = '' for data in self.buffer: available = len(remainder) + len(data) overflow = available % 4 # Make sure we always decode a multiple of 4 if remainder: data = (remainder + data) remainder = '' if overflow: remainder, data = data[-overflow:], data[:-overflow] if data: yield b64decode(data) self.buffer = [remainder] if remainder else [] _forgiving_parser = lxml.etree.XMLParser( resolve_entities=False, # This setting is recommended by lxml for safety recover=True, # This setting is non-default huge_tree=True, # This setting enables parsing huge attachments, mime_content and other large data ) _element_class = _forgiving_parser.makeelement('x').__class__ class BytesGeneratorIO(io.RawIOBase): """A BytesIO that can produce bytes from a streaming HTTP request. Expects r.iter_content() as input lxml tries to be smart by calling `getvalue` when present, assuming that the entire string is in memory. Omitting `getvalue` forces lxml to stream the request through `read` avoiding the memory duplication. """ def __init__(self, bytes_generator): self._bytes_generator = bytes_generator self._next = bytearray() self._tell = 0 super().__init__() def readable(self): return not self.closed def tell(self): return self._tell def read(self, size=-1): # requests `iter_content()` auto-adjusts the number of bytes based on bandwidth # can't assume how many bytes next returns so stash any extra in `self._next` if self.closed: raise ValueError("read from a closed file") if self._next is None: return b'' if size is None: size = -1 res = self._next while size < 0 or len(res) < size: try: res.extend(next(self._bytes_generator)) except StopIteration: self._next = None break if size > 0 and self._next is not None: self._next = res[size:] res = res[:size] self._tell += len(res) return bytes(res) def close(self): if not self.closed: self._bytes_generator.close() super().close() class DocumentYielder: """Look for XML documents in a streaming HTTP response and yield them as they become available from the stream.""" def __init__(self, content_iterator, document_tag='Envelope'): self._iterator = content_iterator self._start_token = b'<%s' % document_tag.encode('utf-8') self._end_token = b'/%s>' % document_tag.encode('utf-8') def get_tag(self, stop_byte): tag_buffer = [b'<'] while True: try: c = next(self._iterator) except StopIteration: break tag_buffer.append(c) if c == stop_byte: break return b''.join(tag_buffer) def __iter__(self): """Consumes the content iterator, looking for start and end tags. Returns each document when we have fully collected it. """ doc_started = False buffer = [] try: while True: c = next(self._iterator) if not doc_started and c == b'<': tag = self.get_tag(stop_byte=b' ') if tag.startswith(self._start_token): # Start of document. Collect bytes from this point buffer.append(tag) doc_started = True elif doc_started and c == b'<': tag = self.get_tag(stop_byte=b'>') buffer.append(tag) if tag.endswith(self._end_token): # End of document. Yield a valid document and reset the buffer yield b"\n%s" % b''.join(buffer) doc_started = False buffer = [] elif doc_started: buffer.append(c) except StopIteration: return def to_xml(bytes_content): """Convert bytes or a generator of bytes to an XML tree.""" # Exchange servers may spit out the weirdest XML. lxml is pretty good at recovering from errors if isinstance(bytes_content, bytes): stream = io.BytesIO(bytes_content) else: stream = BytesGeneratorIO(bytes_content) try: res = lxml.etree.parse(stream, parser=_forgiving_parser) # nosec except AssertionError as e: raise ParseError(e.args[0], '', -1, 0) except lxml.etree.ParseError as e: if hasattr(e, 'position'): e.lineno, e.offset = e.position if not e.lineno: raise ParseError(str(e), '', e.lineno, e.offset) try: stream.seek(0) offending_line = stream.read().splitlines()[e.lineno - 1] except (IndexError, io.UnsupportedOperation): raise ParseError(str(e), '', e.lineno, e.offset) else: offending_excerpt = offending_line[max(0, e.offset - 20):e.offset + 20] msg = '%s\nOffending text: [...]%s[...]' % (str(e), offending_excerpt) raise ParseError(msg, '', e.lineno, e.offset) except TypeError: try: stream.seek(0) except (IndexError, io.UnsupportedOperation): pass raise ParseError('This is not XML: %r' % stream.read(), '', -1, 0) if res.getroot() is None: try: stream.seek(0) msg = 'No root element found: %r' % stream.read() except (IndexError, io.UnsupportedOperation): msg = 'No root element found' raise ParseError(msg, '', -1, 0) return res def is_xml(text, expected_prefix=b' 0: log.warning('Server requested back off until %s. Sleeping %s seconds', back_off_until, sleep_secs) time.sleep(sleep_secs) return True return False def _need_new_credentials(response): return response.status_code == 401 \ and response.headers.get('TokenExpiredError') def _redirect_or_fail(response, redirects, allow_redirects): # Retry with no delay. If we let requests handle redirects automatically, it would issue a GET to that # URL. We still want to POST. try: redirect_url = get_redirect_url(response=response, allow_relative=False) except RelativeRedirect as e: log.debug("'allow_redirects' only supports relative redirects (%s -> %s)", response.url, e.value) raise RedirectError(url=e.value) if not allow_redirects: raise TransportError('Redirect not allowed but we were redirected (%s -> %s)' % (response.url, redirect_url)) log.debug('HTTP redirected to %s', redirect_url) redirects += 1 if redirects > MAX_REDIRECTS: raise TransportError('Max redirect count exceeded') return redirect_url, redirects def _retry_after(r, wait): """Either return the Retry-After header value or the default wait, whichever is larger.""" try: retry_after = int(r.headers.get('Retry-After', '0')) except ValueError: pass else: if retry_after > wait: return retry_after return wait exchangelib-4.6.1/exchangelib/version.py000066400000000000000000000303701414601472700203140ustar00rootroot00000000000000import logging import re from .errors import TransportError, ResponseMessageError from .util import xml_to_str, TNS log = logging.getLogger(__name__) # Legend for dict: # Key: shortname # Values: (EWS API version ID, full name) # 'shortname' comes from types.xsd and is the official version of the server, corresponding to the version numbers # supplied in SOAP headers. 'API version' is the version name supplied in the RequestServerVersion element in SOAP # headers and describes the EWS API version the server implements. Valid values for this element are described here: # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/requestserverversion VERSIONS = { 'Exchange2007': ('Exchange2007', 'Microsoft Exchange Server 2007'), 'Exchange2007_SP1': ('Exchange2007_SP1', 'Microsoft Exchange Server 2007 SP1'), 'Exchange2007_SP2': ('Exchange2007_SP1', 'Microsoft Exchange Server 2007 SP2'), 'Exchange2007_SP3': ('Exchange2007_SP1', 'Microsoft Exchange Server 2007 SP3'), 'Exchange2010': ('Exchange2010', 'Microsoft Exchange Server 2010'), 'Exchange2010_SP1': ('Exchange2010_SP1', 'Microsoft Exchange Server 2010 SP1'), 'Exchange2010_SP2': ('Exchange2010_SP2', 'Microsoft Exchange Server 2010 SP2'), 'Exchange2010_SP3': ('Exchange2010_SP2', 'Microsoft Exchange Server 2010 SP3'), 'Exchange2013': ('Exchange2013', 'Microsoft Exchange Server 2013'), 'Exchange2013_SP1': ('Exchange2013_SP1', 'Microsoft Exchange Server 2013 SP1'), 'Exchange2015': ('Exchange2015', 'Microsoft Exchange Server 2015'), 'Exchange2015_SP1': ('Exchange2015_SP1', 'Microsoft Exchange Server 2015 SP1'), 'Exchange2016': ('Exchange2016', 'Microsoft Exchange Server 2016'), 'Exchange2019': ('Exchange2019', 'Microsoft Exchange Server 2019'), } # Build a list of unique API versions, used when guessing API version supported by the server. Use reverse order so we # get the newest API version supported by the server. API_VERSIONS = sorted({v[0] for v in VERSIONS.values()}, reverse=True) class Build: """Holds methods for working with build numbers.""" # List of build numbers here: https://docs.microsoft.com/en-us/exchange/new-features/build-numbers-and-release-dates API_VERSION_MAP = { 8: { 0: 'Exchange2007', 1: 'Exchange2007_SP1', 2: 'Exchange2007_SP1', 3: 'Exchange2007_SP1', }, 14: { 0: 'Exchange2010', 1: 'Exchange2010_SP1', 2: 'Exchange2010_SP2', 3: 'Exchange2010_SP2', }, 15: { 0: 'Exchange2013', # Minor builds starting from 847 are Exchange2013_SP1, see api_version() 1: 'Exchange2016', 2: 'Exchange2019', 20: 'Exchange2016', # This is Office365. See issue #221 }, } __slots__ = 'major_version', 'minor_version', 'major_build', 'minor_build' def __init__(self, major_version, minor_version, major_build=0, minor_build=0): if not isinstance(major_version, int): raise ValueError("'major_version' must be an integer") if not isinstance(minor_version, int): raise ValueError("'minor_version' must be an integer") if not isinstance(major_build, int): raise ValueError("'major_build' must be an integer") if not isinstance(minor_build, int): raise ValueError("'minor_build' must be an integer") self.major_version = major_version self.minor_version = minor_version self.major_build = major_build self.minor_build = minor_build if major_version < 8: raise ValueError("Exchange major versions below 8 don't support EWS (%s)" % self) @classmethod def from_xml(cls, elem): xml_elems_map = { 'major_version': 'MajorVersion', 'minor_version': 'MinorVersion', 'major_build': 'MajorBuildNumber', 'minor_build': 'MinorBuildNumber', } kwargs = {} for k, xml_elem in xml_elems_map.items(): v = elem.get(xml_elem) if v is None: raise ValueError() kwargs[k] = int(v) # Also raises ValueError return cls(**kwargs) @classmethod def from_hex_string(cls, s): """Parse a server version string as returned in an autodiscover response. The process is described here: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/serverversion-pox#example The string is a hex string that, converted to a 32-bit binary, encodes the server version. The rules are: * The first 4 bits contain the version number structure version. Can be ignored * The next 6 bits contain the major version number * The next 6 bits contain the minor version number * The next bit contains a flag. Can be ignored * The next 15 bits contain the major build number :param s: """ bin_s = '{:032b}'.format(int(s, 16)) # Convert string to 32-bit binary string major_version = int(bin_s[4:10], 2) minor_version = int(bin_s[10:16], 2) build_number = int(bin_s[17:32], 2) return cls(major_version=major_version, minor_version=minor_version, major_build=build_number) def api_version(self): if EXCHANGE_2013_SP1 <= self < EXCHANGE_2016: return 'Exchange2013_SP1' try: return self.API_VERSION_MAP[self.major_version][self.minor_version] except KeyError: raise ValueError('API version for build %s is unknown' % self) def fullname(self): return VERSIONS[self.api_version()][1] def __cmp__(self, other): # __cmp__ is not a magic method in Python3. We'll just use it here to implement comparison operators c = (self.major_version > other.major_version) - (self.major_version < other.major_version) if c != 0: return c c = (self.minor_version > other.minor_version) - (self.minor_version < other.minor_version) if c != 0: return c c = (self.major_build > other.major_build) - (self.major_build < other.major_build) if c != 0: return c return (self.minor_build > other.minor_build) - (self.minor_build < other.minor_build) def __eq__(self, other): return self.__cmp__(other) == 0 def __hash__(self): return hash(repr(self)) def __ne__(self, other): return self.__cmp__(other) != 0 def __lt__(self, other): return self.__cmp__(other) < 0 def __le__(self, other): return self.__cmp__(other) <= 0 def __gt__(self, other): return self.__cmp__(other) > 0 def __ge__(self, other): return self.__cmp__(other) >= 0 def __str__(self): return '%s.%s.%s.%s' % (self.major_version, self.minor_version, self.major_build, self.minor_build) def __repr__(self): return self.__class__.__name__ \ + repr((self.major_version, self.minor_version, self.major_build, self.minor_build)) # Helpers for comparison operations elsewhere in this package EXCHANGE_2007 = Build(8, 0) EXCHANGE_2007_SP1 = Build(8, 1) EXCHANGE_2010 = Build(14, 0) EXCHANGE_2010_SP1 = Build(14, 1) EXCHANGE_2010_SP2 = Build(14, 2) EXCHANGE_2013 = Build(15, 0) EXCHANGE_2013_SP1 = Build(15, 0, 847) EXCHANGE_2016 = Build(15, 1) EXCHANGE_2019 = Build(15, 2) EXCHANGE_O365 = Build(15, 20) class Version: """Holds information about the server version.""" __slots__ = 'build', 'api_version' def __init__(self, build, api_version=None): if not isinstance(build, (Build, type(None))): raise ValueError("'build' must be a Build instance") self.build = build if api_version is None: self.api_version = build.api_version() else: if not isinstance(api_version, str): raise ValueError("'api_version' must be a string") self.api_version = api_version @property def fullname(self): return VERSIONS[self.api_version][1] @classmethod def guess(cls, protocol, api_version_hint=None): """Ask the server which version it has. We haven't set up an Account object yet, so we generate requests by hand. We only need a response header containing a ServerVersionInfo element. To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this package supports, until we get a valid response. :param protocol: :param api_version_hint: (Default value = None) """ from .services import ResolveNames # The protocol doesn't have a version yet, so default to latest supported version if we don't have a hint. api_version = api_version_hint or API_VERSIONS[0] log.debug('Asking server for version info using API version %s', api_version) # We don't know the build version yet. Hopefully, the server will report it in the SOAP header. Lots of # places expect a version to have a build, so this is a bit dangerous, but passing a fake build around is also # dangerous. Make sure the call to ResolveNames does not require a version build. protocol.config.version = Version(build=None, api_version=api_version) # Use ResolveNames as a minimal request to the server to test if the version is correct. If not, ResolveNames # will try to guess the version automatically. name = str(protocol.credentials) if protocol.credentials and str(protocol.credentials) else 'DUMMY' try: list(ResolveNames(protocol=protocol).call(unresolved_entries=[name])) except ResponseMessageError as e: # We may have survived long enough to get a new version if not protocol.config.version.build: raise TransportError('No valid version headers found in response (%r)' % e) if not protocol.config.version.build: raise TransportError('No valid version headers found in response') return protocol.version @staticmethod def _is_invalid_version_string(version): # Check if a version string is bogus, e.g. V2_, V2015_ or V2018_ return re.match(r'V[0-9]{1,4}_.*', version) @classmethod def from_soap_header(cls, requested_api_version, header): info = header.find('{%s}ServerVersionInfo' % TNS) if info is None: raise TransportError('No ServerVersionInfo in header: %r' % xml_to_str(header)) try: build = Build.from_xml(elem=info) except ValueError: raise TransportError('Bad ServerVersionInfo in response: %r' % xml_to_str(header)) # Not all Exchange servers send the Version element api_version_from_server = info.get('Version') or build.api_version() if api_version_from_server != requested_api_version: if cls._is_invalid_version_string(api_version_from_server): # For unknown reasons, Office 365 may respond with an API version strings that is invalid in a request. # Detect these so we can fallback to a valid version string. log.debug('API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version, api_version_from_server, requested_api_version) api_version_from_server = requested_api_version else: # Trust API version from server response log.debug('API version "%s" worked but server reports version "%s". Using "%s"', requested_api_version, api_version_from_server, api_version_from_server) return cls(build=build, api_version=api_version_from_server) def __eq__(self, other): if self.api_version != other.api_version: return False if self.build and not other.build: return False if other.build and not self.build: return False return self.build == other.build def __repr__(self): return self.__class__.__name__ + repr((self.build, self.api_version)) def __str__(self): return 'Build=%s, API=%s, Fullname=%s' % (self.build, self.api_version, self.fullname) exchangelib-4.6.1/exchangelib/winzone.py000066400000000000000000001124121414601472700203160ustar00rootroot00000000000000"""A dict to translate from IANA location name to Windows timezone name. Translations taken from http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml """ import re import requests from .util import to_xml CLDR_WINZONE_URL = 'https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml' DEFAULT_TERRITORY = '001' CLDR_WINZONE_TYPE_VERSION = '2021a' CLDR_WINZONE_OTHER_VERSION = '7e11800' def generate_map(timeout=10): """Create a new CLDR_TO_MS_TIMEZONE_MAP map from the CLDR data. Used when the CLDR database is updated. :param timeout: (Default value = 10) :return: """ r = requests.get(CLDR_WINZONE_URL, timeout=timeout) if r.status_code != 200: raise ValueError('Unexpected response: %s' % r) tz_map = {} timezones_elem = to_xml(r.content).find('windowsZones').find('mapTimezones') type_version = timezones_elem.get('typeVersion') other_version = timezones_elem.get('otherVersion') for e in timezones_elem.findall('mapZone'): for location in re.split(r'\s+', e.get('type')): if e.get('territory') == DEFAULT_TERRITORY or location not in tz_map: # Prefer default territory. This is so MS_TIMEZONE_TO_IANA_MAP maps from MS timezone ID back to the # "preferred" region/location timezone name. if not location: raise ValueError('Expected location') tz_map[location] = e.get('other'), e.get('territory') return type_version, other_version, tz_map # This map is generated irregularly from generate_map(). Do not edit manually - make corrections to # IANA_TO_MS_TIMEZONE_MAP instead. We provide this map to avoid hammering the CLDR_WINZONE_URL. # # This list was generated from CLDR_WINZONE_URL version CLDR_WINZONE_VERSION. CLDR_TO_MS_TIMEZONE_MAP = { 'Africa/Abidjan': ('Greenwich Standard Time', 'CI'), 'Africa/Accra': ('Greenwich Standard Time', 'GH'), 'Africa/Addis_Ababa': ('E. Africa Standard Time', 'ET'), 'Africa/Algiers': ('W. Central Africa Standard Time', 'DZ'), 'Africa/Asmera': ('E. Africa Standard Time', 'ER'), 'Africa/Bamako': ('Greenwich Standard Time', 'ML'), 'Africa/Bangui': ('W. Central Africa Standard Time', 'CF'), 'Africa/Banjul': ('Greenwich Standard Time', 'GM'), 'Africa/Bissau': ('Greenwich Standard Time', 'GW'), 'Africa/Blantyre': ('South Africa Standard Time', 'MW'), 'Africa/Brazzaville': ('W. Central Africa Standard Time', 'CG'), 'Africa/Bujumbura': ('South Africa Standard Time', 'BI'), 'Africa/Cairo': ('Egypt Standard Time', '001'), 'Africa/Casablanca': ('Morocco Standard Time', '001'), 'Africa/Ceuta': ('Romance Standard Time', 'ES'), 'Africa/Conakry': ('Greenwich Standard Time', 'GN'), 'Africa/Dakar': ('Greenwich Standard Time', 'SN'), 'Africa/Dar_es_Salaam': ('E. Africa Standard Time', 'TZ'), 'Africa/Djibouti': ('E. Africa Standard Time', 'DJ'), 'Africa/Douala': ('W. Central Africa Standard Time', 'CM'), 'Africa/El_Aaiun': ('Morocco Standard Time', 'EH'), 'Africa/Freetown': ('Greenwich Standard Time', 'SL'), 'Africa/Gaborone': ('South Africa Standard Time', 'BW'), 'Africa/Harare': ('South Africa Standard Time', 'ZW'), 'Africa/Johannesburg': ('South Africa Standard Time', '001'), 'Africa/Juba': ('South Sudan Standard Time', '001'), 'Africa/Kampala': ('E. Africa Standard Time', 'UG'), 'Africa/Khartoum': ('Sudan Standard Time', '001'), 'Africa/Kigali': ('South Africa Standard Time', 'RW'), 'Africa/Kinshasa': ('W. Central Africa Standard Time', 'CD'), 'Africa/Lagos': ('W. Central Africa Standard Time', '001'), 'Africa/Libreville': ('W. Central Africa Standard Time', 'GA'), 'Africa/Lome': ('Greenwich Standard Time', 'TG'), 'Africa/Luanda': ('W. Central Africa Standard Time', 'AO'), 'Africa/Lubumbashi': ('South Africa Standard Time', 'CD'), 'Africa/Lusaka': ('South Africa Standard Time', 'ZM'), 'Africa/Malabo': ('W. Central Africa Standard Time', 'GQ'), 'Africa/Maputo': ('South Africa Standard Time', 'MZ'), 'Africa/Maseru': ('South Africa Standard Time', 'LS'), 'Africa/Mbabane': ('South Africa Standard Time', 'SZ'), 'Africa/Mogadishu': ('E. Africa Standard Time', 'SO'), 'Africa/Monrovia': ('Greenwich Standard Time', 'LR'), 'Africa/Nairobi': ('E. Africa Standard Time', '001'), 'Africa/Ndjamena': ('W. Central Africa Standard Time', 'TD'), 'Africa/Niamey': ('W. Central Africa Standard Time', 'NE'), 'Africa/Nouakchott': ('Greenwich Standard Time', 'MR'), 'Africa/Ouagadougou': ('Greenwich Standard Time', 'BF'), 'Africa/Porto-Novo': ('W. Central Africa Standard Time', 'BJ'), 'Africa/Sao_Tome': ('Sao Tome Standard Time', '001'), 'Africa/Tripoli': ('Libya Standard Time', '001'), 'Africa/Tunis': ('W. Central Africa Standard Time', 'TN'), 'Africa/Windhoek': ('Namibia Standard Time', '001'), 'America/Adak': ('Aleutian Standard Time', '001'), 'America/Anchorage': ('Alaskan Standard Time', '001'), 'America/Anguilla': ('SA Western Standard Time', 'AI'), 'America/Antigua': ('SA Western Standard Time', 'AG'), 'America/Araguaina': ('Tocantins Standard Time', '001'), 'America/Argentina/La_Rioja': ('Argentina Standard Time', 'AR'), 'America/Argentina/Rio_Gallegos': ('Argentina Standard Time', 'AR'), 'America/Argentina/Salta': ('Argentina Standard Time', 'AR'), 'America/Argentina/San_Juan': ('Argentina Standard Time', 'AR'), 'America/Argentina/San_Luis': ('Argentina Standard Time', 'AR'), 'America/Argentina/Tucuman': ('Argentina Standard Time', 'AR'), 'America/Argentina/Ushuaia': ('Argentina Standard Time', 'AR'), 'America/Aruba': ('SA Western Standard Time', 'AW'), 'America/Asuncion': ('Paraguay Standard Time', '001'), 'America/Bahia': ('Bahia Standard Time', '001'), 'America/Bahia_Banderas': ('Central Standard Time (Mexico)', 'MX'), 'America/Barbados': ('SA Western Standard Time', 'BB'), 'America/Belem': ('SA Eastern Standard Time', 'BR'), 'America/Belize': ('Central America Standard Time', 'BZ'), 'America/Blanc-Sablon': ('SA Western Standard Time', 'CA'), 'America/Boa_Vista': ('SA Western Standard Time', 'BR'), 'America/Bogota': ('SA Pacific Standard Time', '001'), 'America/Boise': ('Mountain Standard Time', 'US'), 'America/Buenos_Aires': ('Argentina Standard Time', '001'), 'America/Cambridge_Bay': ('Mountain Standard Time', 'CA'), 'America/Campo_Grande': ('Central Brazilian Standard Time', 'BR'), 'America/Cancun': ('Eastern Standard Time (Mexico)', '001'), 'America/Caracas': ('Venezuela Standard Time', '001'), 'America/Catamarca': ('Argentina Standard Time', 'AR'), 'America/Cayenne': ('SA Eastern Standard Time', '001'), 'America/Cayman': ('SA Pacific Standard Time', 'KY'), 'America/Chicago': ('Central Standard Time', '001'), 'America/Chihuahua': ('Mountain Standard Time (Mexico)', '001'), 'America/Coral_Harbour': ('SA Pacific Standard Time', 'CA'), 'America/Cordoba': ('Argentina Standard Time', 'AR'), 'America/Costa_Rica': ('Central America Standard Time', 'CR'), 'America/Creston': ('US Mountain Standard Time', 'CA'), 'America/Cuiaba': ('Central Brazilian Standard Time', '001'), 'America/Curacao': ('SA Western Standard Time', 'CW'), 'America/Danmarkshavn': ('Greenwich Standard Time', 'GL'), 'America/Dawson': ('Yukon Standard Time', 'CA'), 'America/Dawson_Creek': ('US Mountain Standard Time', 'CA'), 'America/Denver': ('Mountain Standard Time', '001'), 'America/Detroit': ('Eastern Standard Time', 'US'), 'America/Dominica': ('SA Western Standard Time', 'DM'), 'America/Edmonton': ('Mountain Standard Time', 'CA'), 'America/Eirunepe': ('SA Pacific Standard Time', 'BR'), 'America/El_Salvador': ('Central America Standard Time', 'SV'), 'America/Fort_Nelson': ('US Mountain Standard Time', 'CA'), 'America/Fortaleza': ('SA Eastern Standard Time', 'BR'), 'America/Glace_Bay': ('Atlantic Standard Time', 'CA'), 'America/Godthab': ('Greenland Standard Time', '001'), 'America/Goose_Bay': ('Atlantic Standard Time', 'CA'), 'America/Grand_Turk': ('Turks And Caicos Standard Time', '001'), 'America/Grenada': ('SA Western Standard Time', 'GD'), 'America/Guadeloupe': ('SA Western Standard Time', 'GP'), 'America/Guatemala': ('Central America Standard Time', '001'), 'America/Guayaquil': ('SA Pacific Standard Time', 'EC'), 'America/Guyana': ('SA Western Standard Time', 'GY'), 'America/Halifax': ('Atlantic Standard Time', '001'), 'America/Havana': ('Cuba Standard Time', '001'), 'America/Hermosillo': ('US Mountain Standard Time', 'MX'), 'America/Indiana/Knox': ('Central Standard Time', 'US'), 'America/Indiana/Marengo': ('US Eastern Standard Time', 'US'), 'America/Indiana/Petersburg': ('Eastern Standard Time', 'US'), 'America/Indiana/Tell_City': ('Central Standard Time', 'US'), 'America/Indiana/Vevay': ('US Eastern Standard Time', 'US'), 'America/Indiana/Vincennes': ('Eastern Standard Time', 'US'), 'America/Indiana/Winamac': ('Eastern Standard Time', 'US'), 'America/Indianapolis': ('US Eastern Standard Time', '001'), 'America/Inuvik': ('Mountain Standard Time', 'CA'), 'America/Iqaluit': ('Eastern Standard Time', 'CA'), 'America/Jamaica': ('SA Pacific Standard Time', 'JM'), 'America/Jujuy': ('Argentina Standard Time', 'AR'), 'America/Juneau': ('Alaskan Standard Time', 'US'), 'America/Kentucky/Monticello': ('Eastern Standard Time', 'US'), 'America/Kralendijk': ('SA Western Standard Time', 'BQ'), 'America/La_Paz': ('SA Western Standard Time', '001'), 'America/Lima': ('SA Pacific Standard Time', 'PE'), 'America/Los_Angeles': ('Pacific Standard Time', '001'), 'America/Louisville': ('Eastern Standard Time', 'US'), 'America/Lower_Princes': ('SA Western Standard Time', 'SX'), 'America/Maceio': ('SA Eastern Standard Time', 'BR'), 'America/Managua': ('Central America Standard Time', 'NI'), 'America/Manaus': ('SA Western Standard Time', 'BR'), 'America/Marigot': ('SA Western Standard Time', 'MF'), 'America/Martinique': ('SA Western Standard Time', 'MQ'), 'America/Matamoros': ('Central Standard Time', 'MX'), 'America/Mazatlan': ('Mountain Standard Time (Mexico)', 'MX'), 'America/Mendoza': ('Argentina Standard Time', 'AR'), 'America/Menominee': ('Central Standard Time', 'US'), 'America/Merida': ('Central Standard Time (Mexico)', 'MX'), 'America/Metlakatla': ('Alaskan Standard Time', 'US'), 'America/Mexico_City': ('Central Standard Time (Mexico)', '001'), 'America/Miquelon': ('Saint Pierre Standard Time', '001'), 'America/Moncton': ('Atlantic Standard Time', 'CA'), 'America/Monterrey': ('Central Standard Time (Mexico)', 'MX'), 'America/Montevideo': ('Montevideo Standard Time', '001'), 'America/Montreal': ('Eastern Standard Time', 'CA'), 'America/Montserrat': ('SA Western Standard Time', 'MS'), 'America/Nassau': ('Eastern Standard Time', 'BS'), 'America/New_York': ('Eastern Standard Time', '001'), 'America/Nipigon': ('Eastern Standard Time', 'CA'), 'America/Nome': ('Alaskan Standard Time', 'US'), 'America/Noronha': ('UTC-02', 'BR'), 'America/North_Dakota/Beulah': ('Central Standard Time', 'US'), 'America/North_Dakota/Center': ('Central Standard Time', 'US'), 'America/North_Dakota/New_Salem': ('Central Standard Time', 'US'), 'America/Ojinaga': ('Mountain Standard Time', 'MX'), 'America/Panama': ('SA Pacific Standard Time', 'PA'), 'America/Pangnirtung': ('Eastern Standard Time', 'CA'), 'America/Paramaribo': ('SA Eastern Standard Time', 'SR'), 'America/Phoenix': ('US Mountain Standard Time', '001'), 'America/Port-au-Prince': ('Haiti Standard Time', '001'), 'America/Port_of_Spain': ('SA Western Standard Time', 'TT'), 'America/Porto_Velho': ('SA Western Standard Time', 'BR'), 'America/Puerto_Rico': ('SA Western Standard Time', 'PR'), 'America/Punta_Arenas': ('Magallanes Standard Time', '001'), 'America/Rainy_River': ('Central Standard Time', 'CA'), 'America/Rankin_Inlet': ('Central Standard Time', 'CA'), 'America/Recife': ('SA Eastern Standard Time', 'BR'), 'America/Regina': ('Canada Central Standard Time', '001'), 'America/Resolute': ('Central Standard Time', 'CA'), 'America/Rio_Branco': ('SA Pacific Standard Time', 'BR'), 'America/Santa_Isabel': ('Pacific Standard Time (Mexico)', 'MX'), 'America/Santarem': ('SA Eastern Standard Time', 'BR'), 'America/Santiago': ('Pacific SA Standard Time', '001'), 'America/Santo_Domingo': ('SA Western Standard Time', 'DO'), 'America/Sao_Paulo': ('E. South America Standard Time', '001'), 'America/Scoresbysund': ('Azores Standard Time', 'GL'), 'America/Sitka': ('Alaskan Standard Time', 'US'), 'America/St_Barthelemy': ('SA Western Standard Time', 'BL'), 'America/St_Johns': ('Newfoundland Standard Time', '001'), 'America/St_Kitts': ('SA Western Standard Time', 'KN'), 'America/St_Lucia': ('SA Western Standard Time', 'LC'), 'America/St_Thomas': ('SA Western Standard Time', 'VI'), 'America/St_Vincent': ('SA Western Standard Time', 'VC'), 'America/Swift_Current': ('Canada Central Standard Time', 'CA'), 'America/Tegucigalpa': ('Central America Standard Time', 'HN'), 'America/Thule': ('Atlantic Standard Time', 'GL'), 'America/Thunder_Bay': ('Eastern Standard Time', 'CA'), 'America/Tijuana': ('Pacific Standard Time (Mexico)', '001'), 'America/Toronto': ('Eastern Standard Time', 'CA'), 'America/Tortola': ('SA Western Standard Time', 'VG'), 'America/Vancouver': ('Pacific Standard Time', 'CA'), 'America/Whitehorse': ('Yukon Standard Time', '001'), 'America/Winnipeg': ('Central Standard Time', 'CA'), 'America/Yakutat': ('Alaskan Standard Time', 'US'), 'America/Yellowknife': ('Mountain Standard Time', 'CA'), 'Antarctica/Casey': ('Central Pacific Standard Time', 'AQ'), 'Antarctica/Davis': ('SE Asia Standard Time', 'AQ'), 'Antarctica/DumontDUrville': ('West Pacific Standard Time', 'AQ'), 'Antarctica/Macquarie': ('Tasmania Standard Time', 'AU'), 'Antarctica/Mawson': ('West Asia Standard Time', 'AQ'), 'Antarctica/McMurdo': ('New Zealand Standard Time', 'AQ'), 'Antarctica/Palmer': ('SA Eastern Standard Time', 'AQ'), 'Antarctica/Rothera': ('SA Eastern Standard Time', 'AQ'), 'Antarctica/Syowa': ('E. Africa Standard Time', 'AQ'), 'Antarctica/Vostok': ('Central Asia Standard Time', 'AQ'), 'Arctic/Longyearbyen': ('W. Europe Standard Time', 'SJ'), 'Asia/Aden': ('Arab Standard Time', 'YE'), 'Asia/Almaty': ('Central Asia Standard Time', '001'), 'Asia/Amman': ('Jordan Standard Time', '001'), 'Asia/Anadyr': ('Russia Time Zone 11', 'RU'), 'Asia/Aqtau': ('West Asia Standard Time', 'KZ'), 'Asia/Aqtobe': ('West Asia Standard Time', 'KZ'), 'Asia/Ashgabat': ('West Asia Standard Time', 'TM'), 'Asia/Atyrau': ('West Asia Standard Time', 'KZ'), 'Asia/Baghdad': ('Arabic Standard Time', '001'), 'Asia/Bahrain': ('Arab Standard Time', 'BH'), 'Asia/Baku': ('Azerbaijan Standard Time', '001'), 'Asia/Bangkok': ('SE Asia Standard Time', '001'), 'Asia/Barnaul': ('Altai Standard Time', '001'), 'Asia/Beirut': ('Middle East Standard Time', '001'), 'Asia/Bishkek': ('Central Asia Standard Time', 'KG'), 'Asia/Brunei': ('Singapore Standard Time', 'BN'), 'Asia/Calcutta': ('India Standard Time', '001'), 'Asia/Chita': ('Transbaikal Standard Time', '001'), 'Asia/Choibalsan': ('Ulaanbaatar Standard Time', 'MN'), 'Asia/Colombo': ('Sri Lanka Standard Time', '001'), 'Asia/Damascus': ('Syria Standard Time', '001'), 'Asia/Dhaka': ('Bangladesh Standard Time', '001'), 'Asia/Dili': ('Tokyo Standard Time', 'TL'), 'Asia/Dubai': ('Arabian Standard Time', '001'), 'Asia/Dushanbe': ('West Asia Standard Time', 'TJ'), 'Asia/Famagusta': ('GTB Standard Time', 'CY'), 'Asia/Gaza': ('West Bank Standard Time', 'PS'), 'Asia/Hebron': ('West Bank Standard Time', '001'), 'Asia/Hong_Kong': ('China Standard Time', 'HK'), 'Asia/Hovd': ('W. Mongolia Standard Time', '001'), 'Asia/Irkutsk': ('North Asia East Standard Time', '001'), 'Asia/Jakarta': ('SE Asia Standard Time', 'ID'), 'Asia/Jayapura': ('Tokyo Standard Time', 'ID'), 'Asia/Jerusalem': ('Israel Standard Time', '001'), 'Asia/Kabul': ('Afghanistan Standard Time', '001'), 'Asia/Kamchatka': ('Russia Time Zone 11', '001'), 'Asia/Karachi': ('Pakistan Standard Time', '001'), 'Asia/Katmandu': ('Nepal Standard Time', '001'), 'Asia/Khandyga': ('Yakutsk Standard Time', 'RU'), 'Asia/Krasnoyarsk': ('North Asia Standard Time', '001'), 'Asia/Kuala_Lumpur': ('Singapore Standard Time', 'MY'), 'Asia/Kuching': ('Singapore Standard Time', 'MY'), 'Asia/Kuwait': ('Arab Standard Time', 'KW'), 'Asia/Macau': ('China Standard Time', 'MO'), 'Asia/Magadan': ('Magadan Standard Time', '001'), 'Asia/Makassar': ('Singapore Standard Time', 'ID'), 'Asia/Manila': ('Singapore Standard Time', 'PH'), 'Asia/Muscat': ('Arabian Standard Time', 'OM'), 'Asia/Nicosia': ('GTB Standard Time', 'CY'), 'Asia/Novokuznetsk': ('North Asia Standard Time', 'RU'), 'Asia/Novosibirsk': ('N. Central Asia Standard Time', '001'), 'Asia/Omsk': ('Omsk Standard Time', '001'), 'Asia/Oral': ('West Asia Standard Time', 'KZ'), 'Asia/Phnom_Penh': ('SE Asia Standard Time', 'KH'), 'Asia/Pontianak': ('SE Asia Standard Time', 'ID'), 'Asia/Pyongyang': ('North Korea Standard Time', '001'), 'Asia/Qatar': ('Arab Standard Time', 'QA'), 'Asia/Qostanay': ('Central Asia Standard Time', 'KZ'), 'Asia/Qyzylorda': ('Qyzylorda Standard Time', '001'), 'Asia/Rangoon': ('Myanmar Standard Time', '001'), 'Asia/Riyadh': ('Arab Standard Time', '001'), 'Asia/Saigon': ('SE Asia Standard Time', 'VN'), 'Asia/Sakhalin': ('Sakhalin Standard Time', '001'), 'Asia/Samarkand': ('West Asia Standard Time', 'UZ'), 'Asia/Seoul': ('Korea Standard Time', '001'), 'Asia/Shanghai': ('China Standard Time', '001'), 'Asia/Singapore': ('Singapore Standard Time', '001'), 'Asia/Srednekolymsk': ('Russia Time Zone 10', '001'), 'Asia/Taipei': ('Taipei Standard Time', '001'), 'Asia/Tashkent': ('West Asia Standard Time', '001'), 'Asia/Tbilisi': ('Georgian Standard Time', '001'), 'Asia/Tehran': ('Iran Standard Time', '001'), 'Asia/Thimphu': ('Bangladesh Standard Time', 'BT'), 'Asia/Tokyo': ('Tokyo Standard Time', '001'), 'Asia/Tomsk': ('Tomsk Standard Time', '001'), 'Asia/Ulaanbaatar': ('Ulaanbaatar Standard Time', '001'), 'Asia/Urumqi': ('Central Asia Standard Time', 'CN'), 'Asia/Ust-Nera': ('Vladivostok Standard Time', 'RU'), 'Asia/Vientiane': ('SE Asia Standard Time', 'LA'), 'Asia/Vladivostok': ('Vladivostok Standard Time', '001'), 'Asia/Yakutsk': ('Yakutsk Standard Time', '001'), 'Asia/Yekaterinburg': ('Ekaterinburg Standard Time', '001'), 'Asia/Yerevan': ('Caucasus Standard Time', '001'), 'Atlantic/Azores': ('Azores Standard Time', '001'), 'Atlantic/Bermuda': ('Atlantic Standard Time', 'BM'), 'Atlantic/Canary': ('GMT Standard Time', 'ES'), 'Atlantic/Cape_Verde': ('Cape Verde Standard Time', '001'), 'Atlantic/Faeroe': ('GMT Standard Time', 'FO'), 'Atlantic/Madeira': ('GMT Standard Time', 'PT'), 'Atlantic/Reykjavik': ('Greenwich Standard Time', '001'), 'Atlantic/South_Georgia': ('UTC-02', 'GS'), 'Atlantic/St_Helena': ('Greenwich Standard Time', 'SH'), 'Atlantic/Stanley': ('SA Eastern Standard Time', 'FK'), 'Australia/Adelaide': ('Cen. Australia Standard Time', '001'), 'Australia/Brisbane': ('E. Australia Standard Time', '001'), 'Australia/Broken_Hill': ('Cen. Australia Standard Time', 'AU'), 'Australia/Currie': ('Tasmania Standard Time', 'AU'), 'Australia/Darwin': ('AUS Central Standard Time', '001'), 'Australia/Eucla': ('Aus Central W. Standard Time', '001'), 'Australia/Hobart': ('Tasmania Standard Time', '001'), 'Australia/Lindeman': ('E. Australia Standard Time', 'AU'), 'Australia/Lord_Howe': ('Lord Howe Standard Time', '001'), 'Australia/Melbourne': ('AUS Eastern Standard Time', 'AU'), 'Australia/Perth': ('W. Australia Standard Time', '001'), 'Australia/Sydney': ('AUS Eastern Standard Time', '001'), 'CST6CDT': ('Central Standard Time', 'ZZ'), 'EST5EDT': ('Eastern Standard Time', 'ZZ'), 'Etc/GMT': ('UTC', 'ZZ'), 'Etc/GMT+1': ('Cape Verde Standard Time', 'ZZ'), 'Etc/GMT+10': ('Hawaiian Standard Time', 'ZZ'), 'Etc/GMT+11': ('UTC-11', '001'), 'Etc/GMT+12': ('Dateline Standard Time', '001'), 'Etc/GMT+2': ('UTC-02', '001'), 'Etc/GMT+3': ('SA Eastern Standard Time', 'ZZ'), 'Etc/GMT+4': ('SA Western Standard Time', 'ZZ'), 'Etc/GMT+5': ('SA Pacific Standard Time', 'ZZ'), 'Etc/GMT+6': ('Central America Standard Time', 'ZZ'), 'Etc/GMT+7': ('US Mountain Standard Time', 'ZZ'), 'Etc/GMT+8': ('UTC-08', '001'), 'Etc/GMT+9': ('UTC-09', '001'), 'Etc/GMT-1': ('W. Central Africa Standard Time', 'ZZ'), 'Etc/GMT-10': ('West Pacific Standard Time', 'ZZ'), 'Etc/GMT-11': ('Central Pacific Standard Time', 'ZZ'), 'Etc/GMT-12': ('UTC+12', '001'), 'Etc/GMT-13': ('UTC+13', '001'), 'Etc/GMT-14': ('Line Islands Standard Time', 'ZZ'), 'Etc/GMT-2': ('South Africa Standard Time', 'ZZ'), 'Etc/GMT-3': ('E. Africa Standard Time', 'ZZ'), 'Etc/GMT-4': ('Arabian Standard Time', 'ZZ'), 'Etc/GMT-5': ('West Asia Standard Time', 'ZZ'), 'Etc/GMT-6': ('Central Asia Standard Time', 'ZZ'), 'Etc/GMT-7': ('SE Asia Standard Time', 'ZZ'), 'Etc/GMT-8': ('Singapore Standard Time', 'ZZ'), 'Etc/GMT-9': ('Tokyo Standard Time', 'ZZ'), 'Etc/UTC': ('UTC', '001'), 'Europe/Amsterdam': ('W. Europe Standard Time', 'NL'), 'Europe/Andorra': ('W. Europe Standard Time', 'AD'), 'Europe/Astrakhan': ('Astrakhan Standard Time', '001'), 'Europe/Athens': ('GTB Standard Time', 'GR'), 'Europe/Belgrade': ('Central Europe Standard Time', 'RS'), 'Europe/Berlin': ('W. Europe Standard Time', '001'), 'Europe/Bratislava': ('Central Europe Standard Time', 'SK'), 'Europe/Brussels': ('Romance Standard Time', 'BE'), 'Europe/Bucharest': ('GTB Standard Time', '001'), 'Europe/Budapest': ('Central Europe Standard Time', '001'), 'Europe/Busingen': ('W. Europe Standard Time', 'DE'), 'Europe/Chisinau': ('E. Europe Standard Time', '001'), 'Europe/Copenhagen': ('Romance Standard Time', 'DK'), 'Europe/Dublin': ('GMT Standard Time', 'IE'), 'Europe/Gibraltar': ('W. Europe Standard Time', 'GI'), 'Europe/Guernsey': ('GMT Standard Time', 'GG'), 'Europe/Helsinki': ('FLE Standard Time', 'FI'), 'Europe/Isle_of_Man': ('GMT Standard Time', 'IM'), 'Europe/Istanbul': ('Turkey Standard Time', '001'), 'Europe/Jersey': ('GMT Standard Time', 'JE'), 'Europe/Kaliningrad': ('Kaliningrad Standard Time', '001'), 'Europe/Kiev': ('FLE Standard Time', '001'), 'Europe/Kirov': ('Russian Standard Time', 'RU'), 'Europe/Lisbon': ('GMT Standard Time', 'PT'), 'Europe/Ljubljana': ('Central Europe Standard Time', 'SI'), 'Europe/London': ('GMT Standard Time', '001'), 'Europe/Luxembourg': ('W. Europe Standard Time', 'LU'), 'Europe/Madrid': ('Romance Standard Time', 'ES'), 'Europe/Malta': ('W. Europe Standard Time', 'MT'), 'Europe/Mariehamn': ('FLE Standard Time', 'AX'), 'Europe/Minsk': ('Belarus Standard Time', '001'), 'Europe/Monaco': ('W. Europe Standard Time', 'MC'), 'Europe/Moscow': ('Russian Standard Time', '001'), 'Europe/Oslo': ('W. Europe Standard Time', 'NO'), 'Europe/Paris': ('Romance Standard Time', '001'), 'Europe/Podgorica': ('Central Europe Standard Time', 'ME'), 'Europe/Prague': ('Central Europe Standard Time', 'CZ'), 'Europe/Riga': ('FLE Standard Time', 'LV'), 'Europe/Rome': ('W. Europe Standard Time', 'IT'), 'Europe/Samara': ('Russia Time Zone 3', '001'), 'Europe/San_Marino': ('W. Europe Standard Time', 'SM'), 'Europe/Sarajevo': ('Central European Standard Time', 'BA'), 'Europe/Saratov': ('Saratov Standard Time', '001'), 'Europe/Simferopol': ('Russian Standard Time', 'UA'), 'Europe/Skopje': ('Central European Standard Time', 'MK'), 'Europe/Sofia': ('FLE Standard Time', 'BG'), 'Europe/Stockholm': ('W. Europe Standard Time', 'SE'), 'Europe/Tallinn': ('FLE Standard Time', 'EE'), 'Europe/Tirane': ('Central Europe Standard Time', 'AL'), 'Europe/Ulyanovsk': ('Astrakhan Standard Time', 'RU'), 'Europe/Uzhgorod': ('FLE Standard Time', 'UA'), 'Europe/Vaduz': ('W. Europe Standard Time', 'LI'), 'Europe/Vatican': ('W. Europe Standard Time', 'VA'), 'Europe/Vienna': ('W. Europe Standard Time', 'AT'), 'Europe/Vilnius': ('FLE Standard Time', 'LT'), 'Europe/Volgograd': ('Volgograd Standard Time', '001'), 'Europe/Warsaw': ('Central European Standard Time', '001'), 'Europe/Zagreb': ('Central European Standard Time', 'HR'), 'Europe/Zaporozhye': ('FLE Standard Time', 'UA'), 'Europe/Zurich': ('W. Europe Standard Time', 'CH'), 'Indian/Antananarivo': ('E. Africa Standard Time', 'MG'), 'Indian/Chagos': ('Central Asia Standard Time', 'IO'), 'Indian/Christmas': ('SE Asia Standard Time', 'CX'), 'Indian/Cocos': ('Myanmar Standard Time', 'CC'), 'Indian/Comoro': ('E. Africa Standard Time', 'KM'), 'Indian/Kerguelen': ('West Asia Standard Time', 'TF'), 'Indian/Mahe': ('Mauritius Standard Time', 'SC'), 'Indian/Maldives': ('West Asia Standard Time', 'MV'), 'Indian/Mauritius': ('Mauritius Standard Time', '001'), 'Indian/Mayotte': ('E. Africa Standard Time', 'YT'), 'Indian/Reunion': ('Mauritius Standard Time', 'RE'), 'MST7MDT': ('Mountain Standard Time', 'ZZ'), 'PST8PDT': ('Pacific Standard Time', 'ZZ'), 'Pacific/Apia': ('Samoa Standard Time', '001'), 'Pacific/Auckland': ('New Zealand Standard Time', '001'), 'Pacific/Bougainville': ('Bougainville Standard Time', '001'), 'Pacific/Chatham': ('Chatham Islands Standard Time', '001'), 'Pacific/Easter': ('Easter Island Standard Time', '001'), 'Pacific/Efate': ('Central Pacific Standard Time', 'VU'), 'Pacific/Enderbury': ('UTC+13', 'KI'), 'Pacific/Fakaofo': ('UTC+13', 'TK'), 'Pacific/Fiji': ('Fiji Standard Time', '001'), 'Pacific/Funafuti': ('UTC+12', 'TV'), 'Pacific/Galapagos': ('Central America Standard Time', 'EC'), 'Pacific/Gambier': ('UTC-09', 'PF'), 'Pacific/Guadalcanal': ('Central Pacific Standard Time', '001'), 'Pacific/Guam': ('West Pacific Standard Time', 'GU'), 'Pacific/Honolulu': ('Hawaiian Standard Time', '001'), 'Pacific/Johnston': ('Hawaiian Standard Time', 'UM'), 'Pacific/Kiritimati': ('Line Islands Standard Time', '001'), 'Pacific/Kosrae': ('Central Pacific Standard Time', 'FM'), 'Pacific/Kwajalein': ('UTC+12', 'MH'), 'Pacific/Majuro': ('UTC+12', 'MH'), 'Pacific/Marquesas': ('Marquesas Standard Time', '001'), 'Pacific/Midway': ('UTC-11', 'UM'), 'Pacific/Nauru': ('UTC+12', 'NR'), 'Pacific/Niue': ('UTC-11', 'NU'), 'Pacific/Norfolk': ('Norfolk Standard Time', '001'), 'Pacific/Noumea': ('Central Pacific Standard Time', 'NC'), 'Pacific/Pago_Pago': ('UTC-11', 'AS'), 'Pacific/Palau': ('Tokyo Standard Time', 'PW'), 'Pacific/Pitcairn': ('UTC-08', 'PN'), 'Pacific/Ponape': ('Central Pacific Standard Time', 'FM'), 'Pacific/Port_Moresby': ('West Pacific Standard Time', '001'), 'Pacific/Rarotonga': ('Hawaiian Standard Time', 'CK'), 'Pacific/Saipan': ('West Pacific Standard Time', 'MP'), 'Pacific/Tahiti': ('Hawaiian Standard Time', 'PF'), 'Pacific/Tarawa': ('UTC+12', 'KI'), 'Pacific/Tongatapu': ('Tonga Standard Time', '001'), 'Pacific/Truk': ('West Pacific Standard Time', 'FM'), 'Pacific/Wake': ('UTC+12', 'UM'), 'Pacific/Wallis': ('UTC+12', 'WF'), } # Timezone names used by IANA but not mentioned in the CLDR. All of them have an alias in CLDR. This is essentially # all timezone names that zoneinfo.available_timezones() contains but CLDR doesn't. Aliases were found # at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones IANA_TO_MS_TIMEZONE_MAP = dict( CLDR_TO_MS_TIMEZONE_MAP, **{ 'Africa/Asmara': CLDR_TO_MS_TIMEZONE_MAP['Africa/Nairobi'], 'Africa/Timbuktu': CLDR_TO_MS_TIMEZONE_MAP['Africa/Abidjan'], 'America/Argentina/Buenos_Aires': CLDR_TO_MS_TIMEZONE_MAP['America/Buenos_Aires'], 'America/Argentina/Catamarca': CLDR_TO_MS_TIMEZONE_MAP['America/Catamarca'], 'America/Argentina/ComodRivadavia': CLDR_TO_MS_TIMEZONE_MAP['America/Catamarca'], 'America/Argentina/Cordoba': CLDR_TO_MS_TIMEZONE_MAP['America/Cordoba'], 'America/Argentina/Jujuy': CLDR_TO_MS_TIMEZONE_MAP['America/Jujuy'], 'America/Argentina/Mendoza': CLDR_TO_MS_TIMEZONE_MAP['America/Mendoza'], 'America/Atikokan': CLDR_TO_MS_TIMEZONE_MAP['America/Coral_Harbour'], 'America/Atka': CLDR_TO_MS_TIMEZONE_MAP['America/Adak'], 'America/Ensenada': CLDR_TO_MS_TIMEZONE_MAP['America/Tijuana'], 'America/Fort_Wayne': CLDR_TO_MS_TIMEZONE_MAP['America/Indianapolis'], 'America/Indiana/Indianapolis': CLDR_TO_MS_TIMEZONE_MAP['America/Indianapolis'], 'America/Kentucky/Louisville': CLDR_TO_MS_TIMEZONE_MAP['America/Louisville'], 'America/Knox_IN': CLDR_TO_MS_TIMEZONE_MAP['America/Indiana/Knox'], 'America/Nuuk': CLDR_TO_MS_TIMEZONE_MAP['America/Godthab'], 'America/Porto_Acre': CLDR_TO_MS_TIMEZONE_MAP['America/Rio_Branco'], 'America/Rosario': CLDR_TO_MS_TIMEZONE_MAP['America/Cordoba'], 'America/Shiprock': CLDR_TO_MS_TIMEZONE_MAP['America/Denver'], 'America/Virgin': CLDR_TO_MS_TIMEZONE_MAP['America/Port_of_Spain'], 'Antarctica/South_Pole': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Auckland'], 'Antarctica/Troll': CLDR_TO_MS_TIMEZONE_MAP['Europe/Oslo'], 'Asia/Ashkhabad': CLDR_TO_MS_TIMEZONE_MAP['Asia/Ashgabat'], 'Asia/Chongqing': CLDR_TO_MS_TIMEZONE_MAP['Asia/Shanghai'], 'Asia/Chungking': CLDR_TO_MS_TIMEZONE_MAP['Asia/Shanghai'], 'Asia/Dacca': CLDR_TO_MS_TIMEZONE_MAP['Asia/Dhaka'], 'Asia/Harbin': CLDR_TO_MS_TIMEZONE_MAP['Asia/Shanghai'], 'Asia/Ho_Chi_Minh': CLDR_TO_MS_TIMEZONE_MAP['Asia/Saigon'], 'Asia/Istanbul': CLDR_TO_MS_TIMEZONE_MAP['Europe/Istanbul'], 'Asia/Kashgar': CLDR_TO_MS_TIMEZONE_MAP['Asia/Urumqi'], 'Asia/Kathmandu': CLDR_TO_MS_TIMEZONE_MAP['Asia/Katmandu'], 'Asia/Kolkata': CLDR_TO_MS_TIMEZONE_MAP['Asia/Calcutta'], 'Asia/Macao': CLDR_TO_MS_TIMEZONE_MAP['Asia/Macau'], 'Asia/Tel_Aviv': CLDR_TO_MS_TIMEZONE_MAP['Asia/Jerusalem'], 'Asia/Thimbu': CLDR_TO_MS_TIMEZONE_MAP['Asia/Thimphu'], 'Asia/Ujung_Pandang': CLDR_TO_MS_TIMEZONE_MAP['Asia/Makassar'], 'Asia/Ulan_Bator': CLDR_TO_MS_TIMEZONE_MAP['Asia/Ulaanbaatar'], 'Asia/Yangon': CLDR_TO_MS_TIMEZONE_MAP['Asia/Rangoon'], 'Atlantic/Faroe': CLDR_TO_MS_TIMEZONE_MAP['Atlantic/Faeroe'], 'Atlantic/Jan_Mayen': CLDR_TO_MS_TIMEZONE_MAP['Europe/Oslo'], 'Australia/ACT': CLDR_TO_MS_TIMEZONE_MAP['Australia/Sydney'], 'Australia/Canberra': CLDR_TO_MS_TIMEZONE_MAP['Australia/Sydney'], 'Australia/LHI': CLDR_TO_MS_TIMEZONE_MAP['Australia/Lord_Howe'], 'Australia/NSW': CLDR_TO_MS_TIMEZONE_MAP['Australia/Sydney'], 'Australia/North': CLDR_TO_MS_TIMEZONE_MAP['Australia/Darwin'], 'Australia/Queensland': CLDR_TO_MS_TIMEZONE_MAP['Australia/Brisbane'], 'Australia/South': CLDR_TO_MS_TIMEZONE_MAP['Australia/Adelaide'], 'Australia/Tasmania': CLDR_TO_MS_TIMEZONE_MAP['Australia/Hobart'], 'Australia/Victoria': CLDR_TO_MS_TIMEZONE_MAP['Australia/Melbourne'], 'Australia/West': CLDR_TO_MS_TIMEZONE_MAP['Australia/Perth'], 'Australia/Yancowinna': CLDR_TO_MS_TIMEZONE_MAP['Australia/Broken_Hill'], 'Brazil/Acre': CLDR_TO_MS_TIMEZONE_MAP['America/Rio_Branco'], 'Brazil/DeNoronha': CLDR_TO_MS_TIMEZONE_MAP['America/Noronha'], 'Brazil/East': CLDR_TO_MS_TIMEZONE_MAP['America/Sao_Paulo'], 'Brazil/West': CLDR_TO_MS_TIMEZONE_MAP['America/Manaus'], 'CET': CLDR_TO_MS_TIMEZONE_MAP['Europe/Paris'], 'Canada/Atlantic': CLDR_TO_MS_TIMEZONE_MAP['America/Halifax'], 'Canada/Central': CLDR_TO_MS_TIMEZONE_MAP['America/Winnipeg'], 'Canada/Eastern': CLDR_TO_MS_TIMEZONE_MAP['America/Toronto'], 'Canada/Mountain': CLDR_TO_MS_TIMEZONE_MAP['America/Edmonton'], 'Canada/Newfoundland': CLDR_TO_MS_TIMEZONE_MAP['America/St_Johns'], 'Canada/Pacific': CLDR_TO_MS_TIMEZONE_MAP['America/Vancouver'], 'Canada/Saskatchewan': CLDR_TO_MS_TIMEZONE_MAP['America/Regina'], 'Canada/Yukon': CLDR_TO_MS_TIMEZONE_MAP['America/Whitehorse'], 'Chile/Continental': CLDR_TO_MS_TIMEZONE_MAP['America/Santiago'], 'Chile/EasterIsland': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Easter'], 'Cuba': CLDR_TO_MS_TIMEZONE_MAP['America/Havana'], 'EET': CLDR_TO_MS_TIMEZONE_MAP['Europe/Sofia'], 'EST': CLDR_TO_MS_TIMEZONE_MAP['America/Cancun'], 'Egypt': CLDR_TO_MS_TIMEZONE_MAP['Africa/Cairo'], 'Eire': CLDR_TO_MS_TIMEZONE_MAP['Europe/Dublin'], 'Etc/GMT+0': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'], 'Etc/GMT-0': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'], 'Etc/GMT0': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'], 'Etc/Greenwich': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'], 'Etc/UCT': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'], 'Etc/Universal': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'], 'Etc/Zulu': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'], 'Europe/Belfast': CLDR_TO_MS_TIMEZONE_MAP['Europe/London'], 'Europe/Nicosia': CLDR_TO_MS_TIMEZONE_MAP['Asia/Nicosia'], 'Europe/Tiraspol': CLDR_TO_MS_TIMEZONE_MAP['Europe/Chisinau'], 'Factory': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'], 'GB': CLDR_TO_MS_TIMEZONE_MAP['Europe/London'], 'GB-Eire': CLDR_TO_MS_TIMEZONE_MAP['Europe/London'], 'GMT': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'], 'GMT+0': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'], 'GMT-0': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'], 'GMT0': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'], 'Greenwich': CLDR_TO_MS_TIMEZONE_MAP['Etc/GMT'], 'HST': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Honolulu'], 'Hongkong': CLDR_TO_MS_TIMEZONE_MAP['Asia/Hong_Kong'], 'Iceland': CLDR_TO_MS_TIMEZONE_MAP['Atlantic/Reykjavik'], 'Iran': CLDR_TO_MS_TIMEZONE_MAP['Asia/Tehran'], 'Israel': CLDR_TO_MS_TIMEZONE_MAP['Asia/Jerusalem'], 'Jamaica': CLDR_TO_MS_TIMEZONE_MAP['America/Jamaica'], 'Japan': CLDR_TO_MS_TIMEZONE_MAP['Asia/Tokyo'], 'Kwajalein': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Kwajalein'], 'Libya': CLDR_TO_MS_TIMEZONE_MAP['Africa/Tripoli'], 'MET': CLDR_TO_MS_TIMEZONE_MAP['Europe/Paris'], 'MST': CLDR_TO_MS_TIMEZONE_MAP['America/Phoenix'], 'Mexico/BajaNorte': CLDR_TO_MS_TIMEZONE_MAP['America/Tijuana'], 'Mexico/BajaSur': CLDR_TO_MS_TIMEZONE_MAP['America/Mazatlan'], 'Mexico/General': CLDR_TO_MS_TIMEZONE_MAP['America/Mexico_City'], 'NZ': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Auckland'], 'NZ-CHAT': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Chatham'], 'Navajo': CLDR_TO_MS_TIMEZONE_MAP['America/Denver'], 'PRC': CLDR_TO_MS_TIMEZONE_MAP['Asia/Shanghai'], 'Pacific/Chuuk': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Truk'], 'Pacific/Kanton': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Enderbury'], 'Pacific/Pohnpei': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Ponape'], 'Pacific/Samoa': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Pago_Pago'], 'Pacific/Yap': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Truk'], 'Poland': CLDR_TO_MS_TIMEZONE_MAP['Europe/Warsaw'], 'Portugal': CLDR_TO_MS_TIMEZONE_MAP['Europe/Lisbon'], 'ROC': CLDR_TO_MS_TIMEZONE_MAP['Asia/Taipei'], 'ROK': CLDR_TO_MS_TIMEZONE_MAP['Asia/Seoul'], 'Singapore': CLDR_TO_MS_TIMEZONE_MAP['Asia/Singapore'], 'Turkey': CLDR_TO_MS_TIMEZONE_MAP['Europe/Istanbul'], 'UCT': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'], 'US/Alaska': CLDR_TO_MS_TIMEZONE_MAP['America/Anchorage'], 'US/Aleutian': CLDR_TO_MS_TIMEZONE_MAP['America/Adak'], 'US/Arizona': CLDR_TO_MS_TIMEZONE_MAP['America/Phoenix'], 'US/Central': CLDR_TO_MS_TIMEZONE_MAP['America/Chicago'], 'US/East-Indiana': CLDR_TO_MS_TIMEZONE_MAP['America/Indianapolis'], 'US/Eastern': CLDR_TO_MS_TIMEZONE_MAP['America/New_York'], 'US/Hawaii': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Honolulu'], 'US/Indiana-Starke': CLDR_TO_MS_TIMEZONE_MAP['America/Indiana/Knox'], 'US/Michigan': CLDR_TO_MS_TIMEZONE_MAP['America/Detroit'], 'US/Mountain': CLDR_TO_MS_TIMEZONE_MAP['America/Denver'], 'US/Pacific': CLDR_TO_MS_TIMEZONE_MAP['America/Los_Angeles'], 'US/Samoa': CLDR_TO_MS_TIMEZONE_MAP['Pacific/Pago_Pago'], 'UTC': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'], 'Universal': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'], 'W-SU': CLDR_TO_MS_TIMEZONE_MAP['Europe/Moscow'], 'WET': CLDR_TO_MS_TIMEZONE_MAP['Europe/Lisbon'], 'Zulu': CLDR_TO_MS_TIMEZONE_MAP['Etc/UTC'], } ) # Reverse map from Microsoft timezone ID to IANA timezone name. Non-IANA timezone ID's can be added here. MS_TIMEZONE_TO_IANA_MAP = dict( # Use the CLDR map because the IANA map contains deprecated aliases that not all systems support {v[0]: k for k, v in CLDR_TO_MS_TIMEZONE_MAP.items() if v[1] == DEFAULT_TERRITORY}, **{ 'tzone://Microsoft/Utc': 'UTC', } ) exchangelib-4.6.1/scripts/000077500000000000000000000000001414601472700154705ustar00rootroot00000000000000exchangelib-4.6.1/scripts/notifier.py000066400000000000000000000061141414601472700176630ustar00rootroot00000000000000""" This script is an example of 'exchangelib' usage. It will give you email and appointment notifications from your Exchange account on your Ubuntu desktop. Usage: notifier.py [notify_interval] You need to install the `libxml2-dev` `libxslt1-dev` packages for 'exchangelib' to work on Ubuntu. Login and password is fetched from `~/.netrc`. Add an entry like this: machine office365 login MY_INITIALS@example.com password MY_PASSWORD You can keep the notifier running by adding this to your shell startup script: start-stop-daemon \ --pidfile ~/office365-notifier/notify.pid \ --make-pidfile --start --background \ --startas ~/office365-notifier/notify.sh Where `~/office365-notifier/notify.sh` contains this: cd "$( dirname "$0" )" if [ ! -d "office365_env" ]; then virtualenv -p python3 office365_env fi source office365_env/bin/activate pip3 install sh exchangelib > /dev/null sleep=${1:-600} while true do python3 notifier.py $sleep sleep $sleep done """ from datetime import timedelta, datetime from netrc import netrc import sys import warnings from exchangelib import DELEGATE, Credentials, Account, EWSTimeZone import sh if '--insecure' in sys.argv: # Disable TLS when Office365 can't get their certificate act together from exchangelib.protocol import BaseProtocol, NoVerifyHTTPAdapter BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter # Disable insecure TLS warnings warnings.filterwarnings("ignore") # Use notify-send for email notifications and zenity for calendar notifications notify = sh.Command('/usr/bin/notify-send') zenity = sh.Command('/usr/bin/zenity') # Get the local timezone tz = EWSTimeZone.localzone() sleep = int(sys.argv[1]) # 1st arg to this script is the number of seconds to look back in the inbox now = datetime.now(tz=tz) emails_since = now - timedelta(seconds=sleep) cal_items_before = now + timedelta(seconds=sleep * 4) # Longer notice of upcoming appointments than new emails username, _, password = netrc().authenticators('office365') c = Credentials(username, password) a = Account(primary_smtp_address=c.username, credentials=c, access_type=DELEGATE, autodiscover=True) for msg in a.calendar.view(start=now, end=cal_items_before)\ .only('start', 'end', 'subject', 'location')\ .order_by('start', 'end'): if msg.start < now: continue minutes_to_appointment = int((msg.start - now).total_seconds() / 60) subj = 'You have a meeting in %s minutes' % minutes_to_appointment body = '%s-%s: %s\n%s' % ( msg.start.astimezone(tz).strftime('%H:%M'), msg.end.astimezone(tz).strftime('%H:%M'), msg.subject[:150], msg.location ) zenity(**{'info': None, 'no-markup': None, 'title': subj, 'text': body}) for msg in a.inbox.filter(datetime_received__gt=emails_since, is_read=False)\ .only('datetime_received', 'subject', 'text_body')\ .order_by('datetime_received')[:10]: subj = 'New mail: %s' % msg.subject clean_body = '\n'.join(line for line in msg.text_body.split('\n') if line) notify(subj, clean_body[:200]) exchangelib-4.6.1/scripts/optimize.py000077500000000000000000000060021414601472700177030ustar00rootroot00000000000000#!/usr/bin/env python # Measures bulk create and delete performance for different session pool sizes and payload chunksizes import copy import datetime import logging import os import time try: import zoneinfo except ImportError: from backports import zoneinfo from yaml import safe_load from exchangelib import DELEGATE, Configuration, Account, CalendarItem, Credentials, FaultTolerance logging.basicConfig(level=logging.WARNING) try: with open(os.path.join(os.path.dirname(__file__), '../settings.yml')) as f: settings = safe_load(f) except FileNotFoundError: print('Copy settings.yml.sample to settings.yml and enter values for your test server') raise categories = ['perftest'] tz = zoneinfo.ZoneInfo('America/New_York') verify_ssl = settings.get('verify_ssl', True) if not verify_ssl: from exchangelib.protocol import BaseProtocol, NoVerifyHTTPAdapter BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter config = Configuration( server=settings['server'], credentials=Credentials(settings['username'], settings['password']), retry_policy=FaultTolerance(), ) print('Exchange server: %s' % config.service_endpoint) account = Account(config=config, primary_smtp_address=settings['account'], access_type=DELEGATE) # Remove leftovers from earlier tests account.calendar.filter(categories__contains=categories).delete() # Calendar item generator def generate_items(count): start = datetime.datetime(2000, 3, 1, 8, 30, 0, tzinfo=tz) end = datetime.datetime(2000, 3, 1, 9, 15, 0, tzinfo=tz) tpl_item = CalendarItem( start=start, end=end, body='This is a performance optimization test of server %s intended to find the optimal batch size and ' 'concurrent connection pool size of this server.' % account.protocol.server, location="It's safe to delete this", categories=categories, ) for j in range(count): item = copy.copy(tpl_item) item.subject = 'Performance optimization test %s by exchangelib' % j, yield item # Worker def test(items, chunk_size): t1 = time.monotonic() ids = account.calendar.bulk_create(items=items, chunk_size=chunk_size) t2 = time.monotonic() account.bulk_delete(ids=ids, chunk_size=chunk_size) t3 = time.monotonic() delta1 = t2 - t1 rate1 = len(ids) / delta1 delta2 = t3 - t2 rate2 = len(ids) / delta2 print(('Time to process %s items (batchsize %s, poolsize %s): %s / %s (%s / %s per sec)' % ( len(ids), chunk_size, account.protocol.poolsize, delta1, delta2, rate1, rate2))) # Generate items calitems = list(generate_items(500)) print('\nTesting batch size') for i in range(1, 11): chunk_size = 25 * i account.protocol.poolsize = 5 test(calitems, chunk_size) time.sleep(60) # Sleep 1 minute. Performance will deteriorate over time if we give the server tie to recover print('\nTesting pool size') for i in range(1, 11): chunk_size = 10 account.protocol.poolsize = i test(calitems, chunk_size) time.sleep(60) exchangelib-4.6.1/scripts/wipe_test_account.py000066400000000000000000000001251414601472700215570ustar00rootroot00000000000000from tests.common import EWSTest t = EWSTest() t.setUpClass() t.wipe_test_account() exchangelib-4.6.1/settings.yml.enc000066400000000000000000000004201414601472700171240ustar00rootroot00000000000000>e `WXG5+2ijt=5gT*4)JQCD#!m90zI /4_eyCgYdzl qB|v>i()kjZ)FQ5|6kvơsyҕCwq0 *b(o-5Ҩ۱2{a3fkɧ0iD}PBw[ kO1aVWm1 ½Z[|tQŖNW8#OŕhBU!AM;exchangelib-4.6.1/settings.yml.ghenc000066400000000000000000000004401414601472700174450ustar00rootroot00000000000000Salted__ִ@F1E rB4%y \`U8UQoOzjOQ؝_wOPn'iLᠪ2SdŎ; _2X'pPWh{7P ɘ){zi|JQ݂>LHqTVV-\etz4^&_tZH/_*)#^<>Fѱ3BPefW81S9699 Y(l:`Ar=0.6.0', 'dnspython>=2.0.0', 'isodate', 'lxml>3.0', 'oauthlib', 'pygments', 'requests>=2.7', 'requests_ntlm>=0.2.0', 'requests_oauthlib', 'tzdata', 'tzlocal', ], extras_require={ 'kerberos': ['requests_gssapi'], 'sspi': ['requests_negotiate_sspi'], # Only for Win32 environments 'complete': ['requests_gssapi', 'requests_negotiate_sspi'], # Only for Win32 environments }, packages=find_packages(exclude=('tests', 'tests.*')), tests_require=['flake8', 'psutil', 'python-dateutil', 'pytz', 'PyYAML', 'requests_mock'], python_requires=">=3.6", test_suite='tests', zip_safe=False, url='https://github.com/ecederstrand/exchangelib', project_urls={ "Bug Tracker": "https://github.com/ecederstrand/exchangelib/issues", "Documentation": "https://ecederstrand.github.io/exchangelib/", "Source Code": "https://github.com/ecederstrand/exchangelib", }, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Topic :: Communications', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 3', ], ) exchangelib-4.6.1/test-requirements.txt000066400000000000000000000001441414601472700202410ustar00rootroot00000000000000coverage coveralls flake8 psutil python-dateutil pytz PyYAML requests_mock unittest-parallel>=1.3.0 exchangelib-4.6.1/tests/000077500000000000000000000000001414601472700151435ustar00rootroot00000000000000exchangelib-4.6.1/tests/__init__.py000066400000000000000000000016131414601472700172550ustar00rootroot00000000000000import logging import random import os from unittest import TestLoader, TestSuite import unittest.util from exchangelib.util import PrettyXmlHandler class RandomTestSuite(TestSuite): def __iter__(self): tests = list(super().__iter__()) random.shuffle(tests) return iter(tests) # Execute test classes in random order TestLoader.suiteClass = RandomTestSuite # Execute test methods in random order within each test class TestLoader.sortTestMethodsUsing = lambda _, x, y: random.choice((1, -1)) # Make sure we're also random in multiprocess test runners random.seed() # Always show full repr() output for object instances in unittest error messages unittest.util._MAX_LENGTH = 2000 if os.environ.get('DEBUG', '').lower() in ('1', 'yes', 'true'): logging.basicConfig(level=logging.DEBUG, handlers=[PrettyXmlHandler()]) else: logging.basicConfig(level=logging.CRITICAL) exchangelib-4.6.1/tests/common.py000066400000000000000000000400771414601472700170150ustar00rootroot00000000000000import abc from collections import namedtuple import datetime from decimal import Decimal import os import random import string import time import unittest import unittest.util from yaml import safe_load try: import zoneinfo except ImportError: from backports import zoneinfo from exchangelib.account import Account from exchangelib.attachments import FileAttachment from exchangelib.configuration import Configuration from exchangelib.credentials import DELEGATE, Credentials from exchangelib.errors import UnknownTimeZone from exchangelib.ewsdatetime import EWSTimeZone from exchangelib.fields import BooleanField, IntegerField, DecimalField, TextField, EmailAddressField, URIField, \ ChoiceField, BodyField, DateTimeField, Base64Field, PhoneNumberField, EmailAddressesField, TimeZoneField, \ PhysicalAddressField, ExtendedPropertyField, MailboxField, AttendeesField, AttachmentField, CharListField, \ MailboxListField, EWSElementField, CultureField, CharField, TextListField, PermissionSetField, MimeContentField, \ DateField, DateTimeBackedDateField from exchangelib.indexed_properties import EmailAddress, PhysicalAddress, PhoneNumber from exchangelib.properties import Attendee, Mailbox, PermissionSet, Permission, UserId, CompleteName,\ ReminderMessageData from exchangelib.protocol import BaseProtocol, NoVerifyHTTPAdapter, FaultTolerance from exchangelib.recurrence import Recurrence, TaskRecurrence, DailyPattern, DailyRegeneration from exchangelib.util import DummyResponse mock_account = namedtuple('mock_account', ('protocol', 'version')) mock_protocol = namedtuple('mock_protocol', ('version', 'service_endpoint')) mock_version = namedtuple('mock_version', ('build',)) def mock_post(url, status_code, headers, text=''): return lambda **kwargs: DummyResponse( url=url, headers=headers, request_headers={}, content=text.encode('utf-8'), status_code=status_code ) def mock_session_exception(exc_cls): def raise_exc(**kwargs): raise exc_cls() return raise_exc class TimedTestCase(unittest.TestCase, metaclass=abc.ABCMeta): SLOW_TEST_DURATION = 5 # Log tests that are slower than this value (in seconds) def setUp(self): self.maxDiff = None self.t1 = time.monotonic() def tearDown(self): t2 = time.monotonic() - self.t1 if t2 > self.SLOW_TEST_DURATION: print("{:07.3f} : {}".format(t2, self.id())) class EWSTest(TimedTestCase, metaclass=abc.ABCMeta): @classmethod def setUpClass(cls): # There's no official Exchange server we can test against, and we can't really provide credentials for our # own test server to everyone on the Internet. Travis-CI uses the encrypted settings.yml.enc for testing. # # If you want to test against your own server and account, create your own settings.yml with credentials for # that server. 'settings.yml.sample' is provided as a template. try: with open(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'settings.yml')) as f: settings = safe_load(f) except FileNotFoundError: print('Skipping %s - no settings.yml file found' % cls.__name__) print('Copy settings.yml.sample to settings.yml and enter values for your test server') raise unittest.SkipTest('Skipping %s - no settings.yml file found' % cls.__name__) cls.settings = settings cls.verify_ssl = settings.get('verify_ssl', True) if not cls.verify_ssl: # Allow unverified TLS if requested in settings file BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter # Create an account shared by all tests tz = zoneinfo.ZoneInfo('Europe/Copenhagen') cls.retry_policy = FaultTolerance(max_wait=600) config = Configuration( server=settings['server'], credentials=Credentials(settings['username'], settings['password']), retry_policy=cls.retry_policy, ) cls.account = Account(primary_smtp_address=settings['account'], access_type=DELEGATE, config=config, locale='da_DK', default_timezone=tz) def setUp(self): super().setUp() # Create a random category for each test to avoid crosstalk self.categories = [get_random_string(length=16, spaces=False, special=False)] def wipe_test_account(self): # Deletes up all deletable items in the test account. Not run in a normal test run self.account.root.wipe(page_size=100) def bulk_delete(self, ids): # Clean up items and check return values for res in self.account.bulk_delete(ids): self.assertEqual(res, True) def random_val(self, field): if isinstance(field, ExtendedPropertyField): if field.value_cls.property_type == 'StringArray': return [get_random_string(255) for _ in range(random.randint(1, 4))] if field.value_cls.property_type == 'IntegerArray': return [get_random_int(0, 256) for _ in range(random.randint(1, 4))] if field.value_cls.property_type == 'BinaryArray': return [get_random_string(255).encode() for _ in range(random.randint(1, 4))] if field.value_cls.property_type == 'String': return get_random_string(255) if field.value_cls.property_type == 'Integer': return get_random_int(0, 256) if field.value_cls.property_type == 'Binary': # In the test_extended_distinguished_property test, EWS rull return 4 NULL bytes after char 16 if we # send a longer bytes sequence. return get_random_string(16).encode() raise ValueError('Unsupported field %s' % field) if isinstance(field, URIField): return get_random_url() if isinstance(field, EmailAddressField): return get_random_email() if isinstance(field, ChoiceField): return get_random_choice(field.supported_choices(version=self.account.version)) if isinstance(field, CultureField): return get_random_choice(['da-DK', 'de-DE', 'en-US', 'es-ES', 'fr-CA', 'nl-NL', 'ru-RU', 'sv-SE']) if isinstance(field, BodyField): return get_random_string(400) if isinstance(field, CharListField): return [get_random_string(16) for _ in range(random.randint(1, 4))] if isinstance(field, TextListField): return [get_random_string(400) for _ in range(random.randint(1, 4))] if isinstance(field, CharField): return get_random_string(field.max_length) if isinstance(field, TextField): return get_random_string(400) if isinstance(field, MimeContentField): return get_random_string(400).encode('utf-8') if isinstance(field, Base64Field): return get_random_bytes(400) if isinstance(field, BooleanField): return get_random_bool() if isinstance(field, DecimalField): return get_random_decimal(field.min or 1, field.max or 99) if isinstance(field, IntegerField): return get_random_int(field.min or 0, field.max or 256) if isinstance(field, DateField): return get_random_date() if isinstance(field, DateTimeBackedDateField): return get_random_date() if isinstance(field, DateTimeField): return get_random_datetime(tz=self.account.default_timezone) if isinstance(field, AttachmentField): return [FileAttachment(name='my_file.txt', content=get_random_string(400).encode('utf-8'))] if isinstance(field, MailboxListField): # email_address must be a real account on the server(?) # TODO: Mailbox has multiple optional args but vals must match server account, so we can't easily test if get_random_bool(): return [Mailbox(email_address=self.account.primary_smtp_address)] return [self.account.primary_smtp_address] if isinstance(field, MailboxField): # email_address must be a real account on the server(?) # TODO: Mailbox has multiple optional args but vals must match server account, so we can't easily test if get_random_bool(): return Mailbox(email_address=self.account.primary_smtp_address) return self.account.primary_smtp_address if isinstance(field, AttendeesField): # Attendee must refer to a real mailbox on the server(?). We're only sure to have one if get_random_bool(): mbx = Mailbox(email_address=self.account.primary_smtp_address) else: mbx = self.account.primary_smtp_address with_last_response_time = get_random_bool() if with_last_response_time: return [ Attendee(mailbox=mbx, response_type='Accept', last_response_time=get_random_datetime(tz=self.account.default_timezone)) ] if get_random_bool(): return [Attendee(mailbox=mbx, response_type='Accept')] return [self.account.primary_smtp_address] if isinstance(field, EmailAddressesField): addrs = [] for label in EmailAddress.get_field_by_fieldname('label').supported_choices(version=self.account.version): addr = EmailAddress(email=get_random_email()) addr.label = label addrs.append(addr) return addrs if isinstance(field, PhysicalAddressField): addrs = [] for label in PhysicalAddress.get_field_by_fieldname('label')\ .supported_choices(version=self.account.version): addr = PhysicalAddress(street=get_random_string(32), city=get_random_string(32), state=get_random_string(32), country=get_random_string(32), zipcode=get_random_string(8)) addr.label = label addrs.append(addr) return addrs if isinstance(field, PhoneNumberField): pns = [] for label in PhoneNumber.get_field_by_fieldname('label').supported_choices(version=self.account.version): pn = PhoneNumber(phone_number=get_random_string(16)) pn.label = label pns.append(pn) return pns if isinstance(field, EWSElementField): if field.value_cls == Recurrence: return Recurrence(pattern=DailyPattern(interval=5), start=get_random_date(), number=7) if field.value_cls == TaskRecurrence: return TaskRecurrence(pattern=DailyRegeneration(interval=5), start=get_random_date(), number=7) if field.value_cls == ReminderMessageData: start = get_random_time() end = get_random_time(start_time=start) return ReminderMessageData( reminder_text=get_random_string(16), location=get_random_string(16), start_time=start, end_time=end, ) if field.value_cls == CompleteName: return CompleteName( title=get_random_string(16), first_name=get_random_string(16), middle_name=get_random_string(16), last_name=get_random_string(16), suffix=get_random_string(16), initials=get_random_string(16), full_name=get_random_string(16), nickname=get_random_string(16), yomi_first_name=get_random_string(16), yomi_last_name=get_random_string(16), ) if isinstance(field, TimeZoneField): while True: tz = zoneinfo.ZoneInfo(random.choice(tuple(zoneinfo.available_timezones()))) try: EWSTimeZone.from_zoneinfo(tz) except UnknownTimeZone: continue return tz if isinstance(field, PermissionSetField): return PermissionSet( permissions=[ Permission( user_id=UserId(primary_smtp_address=self.account.primary_smtp_address), ) ] ) raise ValueError('Unknown field %s' % field) def get_random_bool(): return bool(random.randint(0, 1)) def get_random_int(min_val=0, max_val=2147483647): return random.randint(min_val, max_val) def get_random_decimal(min_val=0, max_val=100): precision = 2 val = get_random_int(min_val, max_val * 10**precision) / 10.0**precision return Decimal('{:.2f}'.format(val)) def get_random_choice(choices): return random.sample(tuple(choices), 1)[0] def get_random_string(length, spaces=True, special=True): chars = string.ascii_letters + string.digits if special: chars += ':.-_' if spaces: chars += ' ' # We want random strings that don't end in spaces - Exchange strips these res = ''.join(map(lambda i: random.choice(chars), range(length))).strip() if len(res) < length: # If strip() made the string shorter, make sure to fill it up res += get_random_string(length - len(res), spaces=False) return res def get_random_byte(): return get_random_bytes(1) def get_random_bytes(length): return bytes(get_random_int(max_val=255) for _ in range(length)) def get_random_url(): path_len = random.randint(1, 16) domain_len = random.randint(1, 30) tld_len = random.randint(2, 4) return 'http://%s.%s/%s.html' % tuple(map( lambda i: get_random_string(i, spaces=False, special=False).lower(), (domain_len, tld_len, path_len) )) def get_random_email(): account_len = random.randint(1, 6) domain_len = random.randint(1, 30) tld_len = random.randint(2, 4) return '%s@%s.%s' % tuple(map( lambda i: get_random_string(i, spaces=False, special=False).lower(), (account_len, domain_len, tld_len) )) def _total_minutes(tm): return (tm.hour * 60) + tm.minute def get_random_time(start_time=datetime.time.min, end_time=datetime.time.max): # Create a random time with minute precision. random_minutes = random.randint(_total_minutes(start_time), _total_minutes(end_time)) return datetime.time(hour=random_minutes // 60, minute=random_minutes % 60) # The timezone we're testing (CET/CEST) had a DST date change in 1996 (see # https://en.wikipedia.org/wiki/Summer_Time_in_Europe). The Microsoft timezone definition on the server # does not observe that, but IANA does. So random datetimes before 1996 will fail tests randomly. RANDOM_DATE_MIN = datetime.date(1996, 1, 1) RANDOM_DATE_MAX = datetime.date(2030, 1, 1) UTC = zoneinfo.ZoneInfo('UTC') def get_random_date(start_date=RANDOM_DATE_MIN, end_date=RANDOM_DATE_MAX): # Keep with a reasonable date range. A wider date range is unstable WRT timezones return datetime.date.fromordinal(random.randint(start_date.toordinal(), end_date.toordinal())) def get_random_datetime(start_date=RANDOM_DATE_MIN, end_date=RANDOM_DATE_MAX, tz=UTC): # Create a random datetime with minute precision. Both dates are inclusive. # Keep with a reasonable date range. A wider date range than the default values is unstable WRT timezones. random_date = get_random_date(start_date=start_date, end_date=end_date) random_datetime = datetime.datetime.combine(random_date, datetime.time.min) \ + datetime.timedelta(minutes=random.randint(0, 60 * 24)) return random_datetime.replace(tzinfo=tz) def get_random_datetime_range(start_date=RANDOM_DATE_MIN, end_date=RANDOM_DATE_MAX, tz=UTC): # Create two random datetimes. Both dates are inclusive. # Keep with a reasonable date range. A wider date range than the default values is unstable WRT timezones. # Calendar items raise ErrorCalendarDurationIsTooLong if duration is > 5 years. return sorted([ get_random_datetime(start_date=start_date, end_date=end_date, tz=tz), get_random_datetime(start_date=start_date, end_date=end_date, tz=tz), ]) exchangelib-4.6.1/tests/test_account.py000066400000000000000000000242571414601472700202220ustar00rootroot00000000000000from collections import namedtuple import pickle from exchangelib.account import Account from exchangelib.attachments import FileAttachment from exchangelib.configuration import Configuration from exchangelib.credentials import Credentials, DELEGATE from exchangelib.errors import ErrorAccessDenied, ErrorFolderNotFound, UnauthorizedError from exchangelib.folders import Calendar from exchangelib.items import Message from exchangelib.properties import DelegateUser, UserId, DelegatePermissions from exchangelib.protocol import Protocol, FaultTolerance from exchangelib.services import GetDelegate from exchangelib.version import Version, EXCHANGE_2007_SP1 from .common import EWSTest class AccountTest(EWSTest): """Test features of the Account object.""" def test_magic(self): self.account.fullname = 'John Doe' self.assertIn(self.account.primary_smtp_address, str(self.account)) self.assertIn(self.account.fullname, str(self.account)) def test_validation(self): with self.assertRaises(ValueError) as e: # Must have valid email address Account(primary_smtp_address='blah') self.assertEqual(str(e.exception), "primary_smtp_address 'blah' is not an email address") with self.assertRaises(AttributeError) as e: # Non-autodiscover requires a config Account(primary_smtp_address='blah@example.com', autodiscover=False) self.assertEqual(str(e.exception), 'non-autodiscover requires a config') with self.assertRaises(ValueError) as e: # access type must be one of ACCESS_TYPES Account(primary_smtp_address='blah@example.com', access_type=123) self.assertEqual(str(e.exception), "'access_type' 123 must be one of ('impersonation', 'delegate')") with self.assertRaises(ValueError) as e: # locale must be a string Account(primary_smtp_address='blah@example.com', locale=123) self.assertEqual(str(e.exception), "Expected 'locale' to be a string, got 123") with self.assertRaises(ValueError) as e: # default timezone must be an EWSTimeZone Account(primary_smtp_address='blah@example.com', default_timezone=123) self.assertEqual(str(e.exception), "Expected 'default_timezone' to be an EWSTimeZone, got 123") with self.assertRaises(ValueError) as e: # config must be a Configuration Account(primary_smtp_address='blah@example.com', config=123) self.assertEqual(str(e.exception), "Expected 'config' to be a Configuration, got 123") def test_get_default_folder(self): # Test a normal folder lookup with GetFolder folder = self.account.root.get_default_folder(Calendar) self.assertIsInstance(folder, Calendar) self.assertNotEqual(folder.id, None) self.assertEqual(folder.name.lower(), Calendar.localized_names(self.account.locale)[0]) class MockCalendar1(Calendar): @classmethod def get_distinguished(cls, root): raise ErrorAccessDenied('foo') # Test an indirect folder lookup with FindItems folder = self.account.root.get_default_folder(MockCalendar1) self.assertIsInstance(folder, MockCalendar1) self.assertEqual(folder.id, None) self.assertEqual(folder.name, MockCalendar1.DISTINGUISHED_FOLDER_ID) class MockCalendar2(Calendar): @classmethod def get_distinguished(cls, root): raise ErrorFolderNotFound('foo') # Test using the one folder of this folder type with self.assertRaises(ErrorFolderNotFound): # This fails because there are no folders of type MockCalendar self.account.root.get_default_folder(MockCalendar2) _orig = Calendar.get_distinguished try: Calendar.get_distinguished = MockCalendar2.get_distinguished folder = self.account.root.get_default_folder(Calendar) self.assertIsInstance(folder, Calendar) self.assertNotEqual(folder.id, None) self.assertEqual(folder.name.lower(), MockCalendar2.localized_names(self.account.locale)[0]) finally: Calendar.get_distinguished = _orig def test_pickle(self): # Test that we can pickle various objects item = Message(folder=self.account.inbox, subject='XXX', categories=self.categories).save() attachment = FileAttachment(name='pickle_me.txt', content=b'') for o in ( FaultTolerance(max_wait=3600), self.account.protocol, attachment, self.account.root, self.account.inbox, self.account, item, ): with self.subTest(o=o): pickled_o = pickle.dumps(o) unpickled_o = pickle.loads(pickled_o) self.assertIsInstance(unpickled_o, type(o)) if not isinstance(o, (Account, Protocol, FaultTolerance)): # __eq__ is not defined on some classes self.assertEqual(o, unpickled_o) def test_mail_tips(self): # Test that mail tips work self.assertEqual(self.account.mail_tips.recipient_address, self.account.primary_smtp_address) def test_delegate(self): # The test server does not have any delegate info. Test that account.delegates works, and mock to test parsing # of a non-empty response. self.assertGreaterEqual( len(self.account.delegates), 0 ) self.assertGreaterEqual( len(list(GetDelegate(account=self.account).call(user_ids=['foo@example.com'], include_permissions=True))), 0 ) xml = b'''\ NoError NoError SOME_SID foo@example.com Foo Bar Author Reviewer false true DelegatesAndMe ''' MockTZ = namedtuple('EWSTimeZone', ['ms_id']) MockAccount = namedtuple('Account', ['access_type', 'primary_smtp_address', 'default_timezone', 'protocol']) MockProtocol = namedtuple('Protocol', ['version']) p = MockProtocol(version=Version(build=EXCHANGE_2007_SP1)) a = MockAccount(DELEGATE, 'foo@example.com', MockTZ('XXX'), protocol=p) ws = GetDelegate(account=a) delegates = list(ws.parse(xml)) self.assertListEqual( delegates, [ DelegateUser( user_id=UserId(sid='SOME_SID', primary_smtp_address='foo@example.com', display_name='Foo Bar'), delegate_permissions=DelegatePermissions( calendar_folder_permission_level='Author', inbox_folder_permission_level='Reviewer', contacts_folder_permission_level='None', notes_folder_permission_level='None', journal_folder_permission_level='None', tasks_folder_permission_level='None', ), receive_copies_of_meeting_messages=False, view_private_items=True, ) ] ) def test_login_failure_and_credentials_update(self): # Create an account that does not need to create any connections account = Account( primary_smtp_address=self.account.primary_smtp_address, access_type=DELEGATE, config=Configuration( service_endpoint=self.account.protocol.service_endpoint, credentials=Credentials(self.account.protocol.credentials.username, 'WRONG_PASSWORD'), version=self.account.version, auth_type=self.account.protocol.auth_type, retry_policy=self.retry_policy, ), autodiscover=False, locale='da_DK', ) # Should fail when credentials are wrong, but UnauthorizedError is caught and retried. Mock the needed methods class Mock1(FaultTolerance): def may_retry_on_error(self, response, wait): if response.status_code == 401: return False return super().may_retry_on_error(response, wait) def raise_response_errors(self, response): if response.status_code == 401: raise UnauthorizedError('Invalid credentials for %s' % response.url) return super().raise_response_errors(response) try: account.protocol.config.retry_policy = Mock1() with self.assertRaises(UnauthorizedError): account.root.refresh() finally: account.protocol.config.retry_policy = self.retry_policy # Cannot update from Configuration object with self.assertRaises(AttributeError): account.protocol.config.credentials = self.account.protocol.credentials # Should succeed after credentials update account.protocol.credentials = self.account.protocol.credentials account.root.refresh() exchangelib-4.6.1/tests/test_attachments.py000066400000000000000000000302401414601472700210660ustar00rootroot00000000000000from exchangelib.attachments import FileAttachment, ItemAttachment, AttachmentId from exchangelib.errors import ErrorItemNotFound, ErrorInvalidIdMalformed from exchangelib.folders import Inbox from exchangelib.items import Item, Message from exchangelib.util import chunkify from .test_items.test_basics import BaseItemTest from .common import get_random_string class AttachmentsTest(BaseItemTest): TEST_FOLDER = 'inbox' FOLDER_CLASS = Inbox ITEM_CLASS = Message def test_attachment_failure(self): att1 = FileAttachment(name='my_file_1.txt', content='Hello from unicode æøå'.encode('utf-8')) att1.attachment_id = 'XXX' with self.assertRaises(ValueError): att1.attach() # Cannot have an attachment ID att1.attachment_id = None with self.assertRaises(ValueError): att1.attach() # Must have a parent item att1.parent_item = Item() with self.assertRaises(ValueError): att1.attach() # Parent item must have an account att1.parent_item = None with self.assertRaises(ValueError): att1.detach() # Must have an attachment ID att1.attachment_id = 'XXX' with self.assertRaises(ValueError): att1.detach() # Must have a parent item att1.parent_item = Item() with self.assertRaises(ValueError): att1.detach() # Parent item must have an account att1.parent_item = None att1.attachment_id = None def test_file_attachment_properties(self): binary_file_content = 'Hello from unicode æøå'.encode('utf-8') att1 = FileAttachment(name='my_file_1.txt', content=binary_file_content) self.assertIn("name='my_file_1.txt'", str(att1)) att1.content = binary_file_content # Test property setter self.assertEqual(att1.content, binary_file_content) # Test property getter att1.attachment_id = 'xxx' self.assertEqual(att1.content, binary_file_content) # Test property getter when attachment_id is set att1._content = None with self.assertRaises(ValueError): print(att1.content) # Test property getter when we need to fetch the content def test_item_attachment_properties(self): attached_item1 = self.get_test_item(folder=self.test_folder) att1 = ItemAttachment(name='attachment1', item=attached_item1) self.assertIn("name='attachment1'", str(att1)) att1.item = attached_item1 # Test property setter self.assertEqual(att1.item, attached_item1) # Test property getter self.assertEqual(att1.item, attached_item1) # Test property getter att1.attachment_id = 'xxx' self.assertEqual(att1.item, attached_item1) # Test property getter when attachment_id is set att1._item = None with self.assertRaises(ValueError): print(att1.item) # Test property getter when we need to fetch the item def test_item_attachments(self): item = self.get_test_item(folder=self.test_folder) attached_item1 = self.get_test_item(folder=self.test_folder) att1 = ItemAttachment(name='attachment1', item=attached_item1) # Test __init__(attachments=...) and attach() on new item self.assertEqual(len(item.attachments), 0) item.attach(att1) self.assertEqual(len(item.attachments), 1) item.save() fresh_item = self.get_item_by_id(item) self.assertEqual(len(fresh_item.attachments), 1) fresh_attachments = sorted(fresh_item.attachments, key=lambda a: a.name) self.assertEqual(fresh_attachments[0].name, 'attachment1') self.assertEqual(fresh_attachments[0].item.subject, attached_item1.subject) self.assertEqual(fresh_attachments[0].item.body, attached_item1.body) # Same as 'body' because 'body' doesn't contain HTML self.assertEqual(fresh_attachments[0].item.text_body, attached_item1.body) # Test attach on saved object att2 = ItemAttachment(name='attachment2', item=attached_item1) self.assertEqual(len(item.attachments), 1) item.attach(att2) self.assertEqual(len(item.attachments), 2) fresh_item = self.get_item_by_id(item) self.assertEqual(len(fresh_item.attachments), 2) fresh_attachments = sorted(fresh_item.attachments, key=lambda a: a.name) self.assertEqual(fresh_attachments[0].name, 'attachment1') self.assertEqual(fresh_attachments[0].item.subject, attached_item1.subject) self.assertEqual(fresh_attachments[0].item.body, attached_item1.body) self.assertEqual(fresh_attachments[1].name, 'attachment2') self.assertEqual(fresh_attachments[1].item.subject, attached_item1.subject) self.assertEqual(fresh_attachments[1].item.body, attached_item1.body) # Test detach item.detach(att1) self.assertTrue(att1.attachment_id is None) self.assertTrue(att1.parent_item is None) fresh_item = self.get_item_by_id(item) self.assertEqual(len(fresh_item.attachments), 1) fresh_attachments = sorted(fresh_item.attachments, key=lambda a: a.name) self.assertEqual(fresh_attachments[0].name, 'attachment2') self.assertEqual(fresh_attachments[0].item.subject, attached_item1.subject) self.assertEqual(fresh_attachments[0].item.body, attached_item1.body) def test_file_attachments(self): item = self.get_test_item(folder=self.test_folder) # Test __init__(attachments=...) and attach() on new item binary_file_content = 'Hello from unicode æøå'.encode('utf-8') att1 = FileAttachment(name='my_file_1.txt', content=binary_file_content) self.assertEqual(len(item.attachments), 0) item.attach(att1) self.assertEqual(len(item.attachments), 1) item.save() fresh_item = self.get_item_by_id(item) self.assertEqual(len(fresh_item.attachments), 1) fresh_attachments = sorted(fresh_item.attachments, key=lambda a: a.name) self.assertEqual(fresh_attachments[0].name, 'my_file_1.txt') self.assertEqual(fresh_attachments[0].content, binary_file_content) # Test attach on saved object att2 = FileAttachment(name='my_file_2.txt', content=binary_file_content) self.assertEqual(len(item.attachments), 1) item.attach(att2) self.assertEqual(len(item.attachments), 2) fresh_item = self.get_item_by_id(item) self.assertEqual(len(fresh_item.attachments), 2) fresh_attachments = sorted(fresh_item.attachments, key=lambda a: a.name) self.assertEqual(fresh_attachments[0].name, 'my_file_1.txt') self.assertEqual(fresh_attachments[0].content, binary_file_content) self.assertEqual(fresh_attachments[1].name, 'my_file_2.txt') self.assertEqual(fresh_attachments[1].content, binary_file_content) # Test detach item.detach(att1) self.assertTrue(att1.attachment_id is None) self.assertTrue(att1.parent_item is None) fresh_item = self.get_item_by_id(item) self.assertEqual(len(fresh_item.attachments), 1) fresh_attachments = sorted(fresh_item.attachments, key=lambda a: a.name) self.assertEqual(fresh_attachments[0].name, 'my_file_2.txt') self.assertEqual(fresh_attachments[0].content, binary_file_content) def test_streaming_file_attachments(self): item = self.get_test_item(folder=self.test_folder) large_binary_file_content = get_random_string(2**10).encode('utf-8') large_att = FileAttachment(name='my_large_file.txt', content=large_binary_file_content) item.attach(large_att) item.save() # Test streaming file content fresh_item = self.get_item_by_id(item) with fresh_item.attachments[0].fp as fp: self.assertEqual(fp.read(), large_binary_file_content) # Test partial reads of streaming file content fresh_item = self.get_item_by_id(item) with fresh_item.attachments[0].fp as fp: chunked_reads = [] buffer = fp.read(7) while buffer: chunked_reads.append(buffer) buffer = fp.read(7) self.assertListEqual(chunked_reads, list(chunkify(large_binary_file_content, 7))) def test_streaming_file_attachment_error(self): # Test that we can parse XML error responses in streaming mode. # Try to stram an attachment with malformed ID att = FileAttachment( parent_item=self.get_test_item(folder=self.test_folder), attachment_id=AttachmentId(id='AAMk='), name='dummy.txt', content=b'', ) with self.assertRaises(ErrorInvalidIdMalformed): with att.fp as fp: fp.read() # Try to stream a non-existent attachment att.attachment_id.id = \ 'AAMkADQyYzZmYmUxLTJiYjItNDg2Ny1iMzNjLTIzYWE1NDgxNmZhNABGAAAAAADUebQDarW2Q7G2Ji8hKofPBwAl9iKCsfCfS' \ 'a9cmjh+JCrCAAPJcuhjAABioKiOUTCQRI6Q5sRzi0pJAAHnDV3CAAABEgAQAN0zlxDrzlxAteU+kt84qOM=' with self.assertRaises(ErrorItemNotFound): with att.fp as fp: fp.read() def test_empty_file_attachment(self): item = self.get_test_item(folder=self.test_folder) att1 = FileAttachment(name='empty_file.txt', content=b'') item.attach(att1) item.save() fresh_item = self.get_item_by_id(item) self.assertEqual( fresh_item.attachments[0].content, b'' ) def test_both_attachment_types(self): item = self.get_test_item(folder=self.test_folder) attached_item = self.get_test_item(folder=self.test_folder).save() item_attachment = ItemAttachment(name='item_attachment', item=attached_item) file_attachment = FileAttachment(name='file_attachment', content=b'file_attachment') item.attach(item_attachment) item.attach(file_attachment) item.save() fresh_item = self.get_item_by_id(item) self.assertSetEqual( {a.name for a in fresh_item.attachments}, {'item_attachment', 'file_attachment'} ) def test_recursive_attachments(self): # Test that we can handle an item which has an attached item, which has an attached item... item = self.get_test_item(folder=self.test_folder) attached_item_level_1 = self.get_test_item(folder=self.test_folder) attached_item_level_2 = self.get_test_item(folder=self.test_folder) attached_item_level_3 = self.get_test_item(folder=self.test_folder) attached_item_level_3.save() attachment_level_3 = ItemAttachment(name='attached_item_level_3', item=attached_item_level_3) attached_item_level_2.attach(attachment_level_3) attached_item_level_2.save() attachment_level_2 = ItemAttachment(name='attached_item_level_2', item=attached_item_level_2) attached_item_level_1.attach(attachment_level_2) attached_item_level_1.save() attachment_level_1 = ItemAttachment(name='attached_item_level_1', item=attached_item_level_1) item.attach(attachment_level_1) item.save() self.assertEqual( item.attachments[0].item.attachments[0].item.attachments[0].item.subject, attached_item_level_3.subject ) # Also test a fresh item new_item = self.test_folder.get(id=item.id, changekey=item.changekey) self.assertEqual( new_item.attachments[0].item.attachments[0].item.attachments[0].item.subject, attached_item_level_3.subject ) def test_detach_all(self): # Make sure that we can detach all by passing item.attachments item = self.get_test_item(folder=self.test_folder).save() item.attach([FileAttachment(name='empty_file.txt', content=b'') for _ in range(6)]) self.assertEqual(len(item.attachments), 6) item.detach(item.attachments) self.assertEqual(len(item.attachments), 0) def test_detach_with_refresh(self): # Make sure that we can detach after refresh item = self.get_test_item(folder=self.test_folder).save() item.attach(FileAttachment(name='empty_file.txt', content=b'')) item.refresh() item.detach(item.attachments) exchangelib-4.6.1/tests/test_autodiscover.py000066400000000000000000000650601414601472700212720ustar00rootroot00000000000000from collections import namedtuple import glob from types import MethodType import dns import requests_mock from exchangelib.account import Account from exchangelib.credentials import Credentials, DELEGATE import exchangelib.autodiscover.discovery from exchangelib.autodiscover import close_connections, clear_cache, autodiscover_cache, AutodiscoverProtocol, \ Autodiscovery from exchangelib.autodiscover.properties import Autodiscover from exchangelib.configuration import Configuration from exchangelib.errors import ErrorNonExistentMailbox, AutoDiscoverCircularRedirect, AutoDiscoverFailed from exchangelib.protocol import FaultTolerance, FailFast from exchangelib.transport import NTLM from exchangelib.util import get_domain from .common import EWSTest, get_random_string class AutodiscoverTest(EWSTest): def setUp(self): super().setUp() # Enable retries, to make tests more robust Autodiscovery.INITIAL_RETRY_POLICY = FaultTolerance(max_wait=5) Autodiscovery.RETRY_WAIT = 5 # Each test should start with a clean autodiscover cache clear_cache() # Some mocking helpers self.domain = get_domain(self.account.primary_smtp_address) self.dummy_ad_endpoint = 'https://%s/Autodiscover/Autodiscover.xml' % self.domain self.dummy_ews_endpoint = 'https://expr.example.com/EWS/Exchange.asmx' self.dummy_ad_response = b'''\ %s email settings EXPR %s ''' % (self.account.primary_smtp_address.encode(), self.dummy_ews_endpoint.encode()) self.dummy_ews_response = b'''\ NoError ''' @requests_mock.mock(real_http=False) # Just make sure we don't issue any real HTTP here def test_magic(self, m): # Just test we don't fail when calling repr() and str(). Insert a dummy cache entry for testing c = Credentials(get_random_string(8), get_random_string(8)) autodiscover_cache[('example.com', c)] = AutodiscoverProtocol(config=Configuration( service_endpoint='https://example.com/Autodiscover/Autodiscover.xml', credentials=c, auth_type=NTLM, retry_policy=FailFast(), )) self.assertEqual(len(autodiscover_cache), 1) str(autodiscover_cache) repr(autodiscover_cache) for protocol in autodiscover_cache._protocols.values(): str(protocol) repr(protocol) def test_autodiscover_empty_cache(self): # A live test of the entire process with an empty cache ad_response, protocol = exchangelib.autodiscover.discovery.discover( email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials, retry_policy=self.retry_policy, ) self.assertEqual(ad_response.autodiscover_smtp_address, self.account.primary_smtp_address) self.assertEqual(protocol.service_endpoint.lower(), self.account.protocol.service_endpoint.lower()) self.assertEqual(protocol.version.build, self.account.protocol.version.build) def test_autodiscover_failure(self): # A live test that errors can be raised. Here, we try to aútodiscover a non-existing email address if not self.settings.get('autodiscover_server'): self.skipTest("Skipping %s - no 'autodiscover_server' entry in settings.yml" % self.__class__.__name__) # Autodiscovery may take a long time. Prime the cache with the autodiscover server from the config file ad_endpoint = 'https://%s/Autodiscover/Autodiscover.xml' % self.settings['autodiscover_server'] cache_key = (self.domain, self.account.protocol.credentials) autodiscover_cache[cache_key] = AutodiscoverProtocol(config=Configuration( service_endpoint=ad_endpoint, credentials=self.account.protocol.credentials, auth_type=NTLM, retry_policy=self.retry_policy, )) with self.assertRaises(ErrorNonExistentMailbox): exchangelib.autodiscover.discovery.discover( email='XXX.' + self.account.primary_smtp_address, credentials=self.account.protocol.credentials, retry_policy=self.retry_policy, ) def test_failed_login_via_account(self): with self.assertRaises(AutoDiscoverFailed): Account( primary_smtp_address=self.account.primary_smtp_address, access_type=DELEGATE, credentials=Credentials(self.account.protocol.credentials.username, 'WRONG_PASSWORD'), autodiscover=True, locale='da_DK', ) @requests_mock.mock(real_http=False) # Just make sure we don't issue any real HTTP here def test_close_autodiscover_connections(self, m): # A live test that we can close TCP connections c = Credentials(get_random_string(8), get_random_string(8)) autodiscover_cache[('example.com', c)] = AutodiscoverProtocol(config=Configuration( service_endpoint='https://example.com/Autodiscover/Autodiscover.xml', credentials=c, auth_type=NTLM, retry_policy=FailFast(), )) self.assertEqual(len(autodiscover_cache), 1) close_connections() @requests_mock.mock(real_http=False) # Just make sure we don't issue any real HTTP here def test_autodiscover_direct_gc(self, m): # Test garbage collection of the autodiscover cache c = Credentials(get_random_string(8), get_random_string(8)) autodiscover_cache[('example.com', c)] = AutodiscoverProtocol(config=Configuration( service_endpoint='https://example.com/Autodiscover/Autodiscover.xml', credentials=c, auth_type=NTLM, retry_policy=FailFast(), )) self.assertEqual(len(autodiscover_cache), 1) autodiscover_cache.__del__() @requests_mock.mock(real_http=False) def test_autodiscover_cache(self, m): # Mock the default endpoint that we test in step 1 of autodiscovery m.post(self.dummy_ad_endpoint, status_code=200, content=self.dummy_ad_response) # Also mock the EWS URL. We try to guess its auth method as part of autodiscovery m.post(self.dummy_ews_endpoint, status_code=200) discovery = Autodiscovery( email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials, retry_policy=self.retry_policy, ) # Not cached self.assertNotIn(discovery._cache_key, autodiscover_cache) discovery.discover() # Now it's cached self.assertIn(discovery._cache_key, autodiscover_cache) # Make sure the cache can be looked by value, not by id(). This is important for multi-threading/processing self.assertIn(( self.account.primary_smtp_address.split('@')[1], Credentials(self.account.protocol.credentials.username, self.account.protocol.credentials.password), True ), autodiscover_cache) # Poison the cache with a failing autodiscover endpoint. discover() must handle this and rebuild the cache autodiscover_cache[discovery._cache_key] = AutodiscoverProtocol(config=Configuration( service_endpoint='https://example.com/Autodiscover/Autodiscover.xml', credentials=Credentials(get_random_string(8), get_random_string(8)), auth_type=NTLM, retry_policy=FailFast(), )) m.post('https://example.com/Autodiscover/Autodiscover.xml', status_code=404) discovery.discover() self.assertIn(discovery._cache_key, autodiscover_cache) # Make sure that the cache is actually used on the second call to discover() _orig = discovery._step_1 def _mock(slf, *args, **kwargs): raise NotImplementedError() discovery._step_1 = MethodType(_mock, discovery) discovery.discover() # Fake that another thread added the cache entry into the persistent storage but we don't have it in our # in-memory cache. The cache should work anyway. autodiscover_cache._protocols.clear() discovery.discover() discovery._step_1 = _orig # Make sure we can delete cache entries even though we don't have it in our in-memory cache autodiscover_cache._protocols.clear() del autodiscover_cache[discovery._cache_key] # This should also work if the cache does not contain the entry anymore del autodiscover_cache[discovery._cache_key] @requests_mock.mock(real_http=False) # Just make sure we don't issue any real HTTP here def test_corrupt_autodiscover_cache(self, m): # Insert a fake Protocol instance into the cache and test that we can recover key = (2, 'foo', 4) autodiscover_cache[key] = namedtuple('P', ['service_endpoint', 'auth_type', 'retry_policy'])(1, 'bar', 'baz') # Check that it exists. 'in' goes directly to the file self.assertTrue(key in autodiscover_cache) # Check that we can recover from a destroyed file for db_file in glob.glob(autodiscover_cache._storage_file + '*'): with open(db_file, 'w') as f: f.write('XXX') self.assertFalse(key in autodiscover_cache) # Check that we can recover from an empty file for db_file in glob.glob(autodiscover_cache._storage_file + '*'): with open(db_file, 'wb') as f: f.write(b'') self.assertFalse(key in autodiscover_cache) @requests_mock.mock(real_http=False) # Just make sure we don't issue any real HTTP here def test_autodiscover_from_account(self, m): # Test that autodiscovery via account creation works # Mock the default endpoint that we test in step 1 of autodiscovery m.post(self.dummy_ad_endpoint, status_code=200, content=self.dummy_ad_response) # Also mock the EWS URL. We try to guess its auth method as part of autodiscovery m.post(self.dummy_ews_endpoint, status_code=200, content=self.dummy_ews_response) self.assertEqual(len(autodiscover_cache), 0) account = Account( primary_smtp_address=self.account.primary_smtp_address, config=Configuration( credentials=self.account.protocol.credentials, retry_policy=self.retry_policy, ), autodiscover=True, locale='da_DK', ) self.assertEqual(account.primary_smtp_address, self.account.primary_smtp_address) self.assertEqual(account.protocol.service_endpoint.lower(), self.dummy_ews_endpoint.lower()) # Make sure cache is full self.assertEqual(len(autodiscover_cache), 1) self.assertTrue((account.domain, self.account.protocol.credentials, True) in autodiscover_cache) # Test that autodiscover works with a full cache account = Account( primary_smtp_address=self.account.primary_smtp_address, config=Configuration( credentials=self.account.protocol.credentials, retry_policy=self.retry_policy, ), autodiscover=True, locale='da_DK', ) self.assertEqual(account.primary_smtp_address, self.account.primary_smtp_address) # Test cache manipulation key = (account.domain, self.account.protocol.credentials, True) self.assertTrue(key in autodiscover_cache) del autodiscover_cache[key] self.assertFalse(key in autodiscover_cache) @requests_mock.mock(real_http=False) def test_autodiscover_redirect(self, m): # Test various aspects of autodiscover redirection. Mock all HTTP responses because we can't force a live server # to send us into the correct code paths. # Mock the default endpoint that we test in step 1 of autodiscovery m.post(self.dummy_ad_endpoint, status_code=200, content=self.dummy_ad_response) # Also mock the EWS URL. We try to guess its auth method as part of autodiscovery m.post(self.dummy_ews_endpoint, status_code=200) discovery = Autodiscovery( email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials, retry_policy=self.retry_policy, ) discovery.discover() # Make sure we discover a different return address m.post(self.dummy_ad_endpoint, status_code=200, content=b'''\ john@example.com email settings EXPR https://expr.example.com/EWS/Exchange.asmx ''') # Also mock the EWS URL. We try to guess its auth method as part of autodiscovery m.post('https://expr.example.com/EWS/Exchange.asmx', status_code=200) ad_response, _ = discovery.discover() self.assertEqual(ad_response.autodiscover_smtp_address, 'john@example.com') # Make sure we discover an address redirect to the same domain. We have to mock the same URL with two different # responses. We do that with a response list. redirect_addr_content = b'''\ redirectAddr redirect_me@%s ''' % self.domain.encode() settings_content = b'''\ redirected@%s email settings EXPR https://redirected.%s/EWS/Exchange.asmx ''' % (self.domain.encode(), self.domain.encode()) # Also mock the EWS URL. We try to guess its auth method as part of autodiscovery m.post('https://redirected.%s/EWS/Exchange.asmx' % self.domain, status_code=200) m.post(self.dummy_ad_endpoint, [ dict(status_code=200, content=redirect_addr_content), dict(status_code=200, content=settings_content), ]) ad_response, _ = discovery.discover() self.assertEqual(ad_response.autodiscover_smtp_address, 'redirected@%s' % self.domain) self.assertEqual(ad_response.ews_url, 'https://redirected.%s/EWS/Exchange.asmx' % self.domain) # Test that we catch circular redirects on the same domain with a primed cache. Just mock the endpoint to # return the same redirect response on every request. self.assertEqual(len(autodiscover_cache), 1) m.post(self.dummy_ad_endpoint, status_code=200, content=b'''\ redirectAddr foo@%s ''' % self.domain.encode()) self.assertEqual(len(autodiscover_cache), 1) with self.assertRaises(AutoDiscoverCircularRedirect): discovery.discover() # Test that we also catch circular redirects when cache is empty clear_cache() self.assertEqual(len(autodiscover_cache), 0) with self.assertRaises(AutoDiscoverCircularRedirect): discovery.discover() # Test that we can handle being asked to redirect to an address on a different domain # Don't use example.com to redirect - it does not resolve or answer on all ISPs m.post(self.dummy_ad_endpoint, status_code=200, content=b'''\ redirectAddr john@httpbin.org ''') m.post('https://httpbin.org/Autodiscover/Autodiscover.xml', status_code=200, content=b'''\ john@redirected.httpbin.org email settings EXPR https://httpbin.org/EWS/Exchange.asmx ''') # Also mock the EWS URL. We try to guess its auth method as part of autodiscovery m.post('https://httpbin.org/EWS/Exchange.asmx', status_code=200) ad_response, _ = discovery.discover() self.assertEqual(ad_response.autodiscover_smtp_address, 'john@redirected.httpbin.org') self.assertEqual(ad_response.ews_url, 'https://httpbin.org/EWS/Exchange.asmx') def test_get_srv_records(self): from exchangelib.autodiscover.discovery import SrvRecord ad = Autodiscovery('foo@example.com') # Unknown domain self.assertEqual(ad._get_srv_records('example.XXXXX'), []) # No SRV record self.assertEqual(ad._get_srv_records('example.com'), []) # Finding a real server that has a correct SRV record is not easy. Mock it _orig = dns.resolver.Resolver class _Mock1: @staticmethod def resolve(hostname, cat): class A: @staticmethod def to_text(): # Return a valid record return '1 2 3 example.com.' return [A()] dns.resolver.Resolver = _Mock1 del ad.resolver # Test a valid record self.assertEqual( ad._get_srv_records('example.com.'), [SrvRecord(priority=1, weight=2, port=3, srv='example.com')] ) class _Mock2: @staticmethod def resolve(hostname, cat): class A: @staticmethod def to_text(): # Return malformed data return 'XXXXXXX' return [A()] dns.resolver.Resolver = _Mock2 del ad.resolver # Test an invalid record self.assertEqual(ad._get_srv_records('example.com'), []) dns.resolver.Resolver = _orig del ad.resolver def test_select_srv_host(self): from exchangelib.autodiscover.discovery import _select_srv_host, SrvRecord with self.assertRaises(ValueError): # Empty list _select_srv_host([]) with self.assertRaises(ValueError): # No records with TLS port _select_srv_host([SrvRecord(priority=1, weight=2, port=3, srv='example.com')]) # One record self.assertEqual( _select_srv_host([SrvRecord(priority=1, weight=2, port=443, srv='example.com')]), 'example.com' ) # Highest priority record self.assertEqual( _select_srv_host([ SrvRecord(priority=10, weight=2, port=443, srv='10.example.com'), SrvRecord(priority=1, weight=2, port=443, srv='1.example.com'), ]), '10.example.com' ) # Highest priority record no matter how it's sorted self.assertEqual( _select_srv_host([ SrvRecord(priority=1, weight=2, port=443, srv='1.example.com'), SrvRecord(priority=10, weight=2, port=443, srv='10.example.com'), ]), '10.example.com' ) def test_parse_response(self): # Test parsing of various XML responses with self.assertRaises(ValueError): Autodiscover.from_bytes(b'XXX') # Invalid response xml = b'''bar''' with self.assertRaises(ValueError): Autodiscover.from_bytes(xml) # Invalid XML response # Redirect to different email address xml = b'''\ john@demo.affect-it.dk redirectAddr foo@example.com ''' self.assertEqual(Autodiscover.from_bytes(xml).response.redirect_address, 'foo@example.com') # Redirect to different URL xml = b'''\ john@demo.affect-it.dk redirectUrl https://example.com/foo.asmx ''' self.assertEqual(Autodiscover.from_bytes(xml).response.redirect_url, 'https://example.com/foo.asmx') # Select EXPR if it's there, and there are multiple available xml = b'''\ john@demo.affect-it.dk email settings EXCH https://exch.example.com/EWS/Exchange.asmx EXPR https://expr.example.com/EWS/Exchange.asmx ''' self.assertEqual( Autodiscover.from_bytes(xml).response.ews_url, 'https://expr.example.com/EWS/Exchange.asmx' ) # Select EXPR if EXPR is unavailable xml = b'''\ john@demo.affect-it.dk email settings EXCH https://exch.example.com/EWS/Exchange.asmx ''' self.assertEqual( Autodiscover.from_bytes(xml).response.ews_url, 'https://exch.example.com/EWS/Exchange.asmx' ) # Fail if neither EXPR nor EXPR are unavailable xml = b'''\ john@demo.affect-it.dk email settings XXX https://xxx.example.com/EWS/Exchange.asmx ''' with self.assertRaises(ValueError): Autodiscover.from_bytes(xml).response.ews_url exchangelib-4.6.1/tests/test_build.py000066400000000000000000000034121414601472700176530ustar00rootroot00000000000000from exchangelib.version import Build from .common import TimedTestCase class BuildTest(TimedTestCase): def test_magic(self): with self.assertRaises(ValueError): Build(7, 0) self.assertEqual(str(Build(9, 8, 7, 6)), '9.8.7.6') def test_compare(self): self.assertEqual(Build(15, 0, 1, 2), Build(15, 0, 1, 2)) self.assertNotEqual(Build(15, 0, 1, 2), Build(15, 0, 1, 3)) self.assertLess(Build(15, 0, 1, 2), Build(15, 0, 1, 3)) self.assertLess(Build(15, 0, 1, 2), Build(15, 0, 2, 2)) self.assertLess(Build(15, 0, 1, 2), Build(15, 1, 1, 2)) self.assertLess(Build(15, 0, 1, 2), Build(16, 0, 1, 2)) self.assertLessEqual(Build(15, 0, 1, 2), Build(15, 0, 1, 2)) self.assertGreater(Build(15, 0, 1, 2), Build(15, 0, 1, 1)) self.assertGreater(Build(15, 0, 1, 2), Build(15, 0, 0, 2)) self.assertGreater(Build(15, 1, 1, 2), Build(15, 0, 1, 2)) self.assertGreater(Build(15, 0, 1, 2), Build(14, 0, 1, 2)) self.assertGreaterEqual(Build(15, 0, 1, 2), Build(15, 0, 1, 2)) def test_api_version(self): self.assertEqual(Build(8, 0).api_version(), 'Exchange2007') self.assertEqual(Build(8, 1).api_version(), 'Exchange2007_SP1') self.assertEqual(Build(8, 2).api_version(), 'Exchange2007_SP1') self.assertEqual(Build(8, 3).api_version(), 'Exchange2007_SP1') self.assertEqual(Build(15, 0, 1, 1).api_version(), 'Exchange2013') self.assertEqual(Build(15, 0, 1, 1).api_version(), 'Exchange2013') self.assertEqual(Build(15, 0, 847, 0).api_version(), 'Exchange2013_SP1') with self.assertRaises(ValueError): Build(16, 0).api_version() with self.assertRaises(ValueError): Build(15, 4).api_version() exchangelib-4.6.1/tests/test_configuration.py000066400000000000000000000073321414601472700214300ustar00rootroot00000000000000import datetime import math import time import requests_mock from exchangelib.configuration import Configuration from exchangelib.credentials import Credentials from exchangelib.transport import NTLM, AUTH_TYPE_MAP from exchangelib.protocol import FailFast, FaultTolerance from exchangelib.version import Version, Build from .common import TimedTestCase, get_random_string class ConfigurationTest(TimedTestCase): def test_init(self): with self.assertRaises(ValueError) as e: Configuration(credentials='foo') self.assertEqual(e.exception.args[0], "'credentials' 'foo' must be a Credentials instance") with self.assertRaises(AttributeError) as e: Configuration(server='foo', service_endpoint='bar') self.assertEqual(e.exception.args[0], "Only one of 'server' or 'service_endpoint' must be provided") with self.assertRaises(ValueError) as e: Configuration(auth_type='foo') self.assertEqual( e.exception.args[0], "'auth_type' 'foo' must be one of %s" % ', '.join("'%s'" % k for k in sorted(AUTH_TYPE_MAP)) ) with self.assertRaises(ValueError) as e: Configuration(version='foo') self.assertEqual(e.exception.args[0], "'version' 'foo' must be a Version instance") with self.assertRaises(ValueError) as e: Configuration(retry_policy='foo') self.assertEqual(e.exception.args[0], "'retry_policy' 'foo' must be a RetryPolicy instance") def test_magic(self): config = Configuration( server='example.com', credentials=Credentials(get_random_string(8), get_random_string(8)), auth_type=NTLM, version=Version(build=Build(15, 1, 2, 3), api_version='foo'), ) # Just test that these work str(config) repr(config) @requests_mock.mock() # Just to make sure we don't make any requests def test_hardcode_all(self, m): # Test that we can hardcode everything without having a working server. This is useful if neither tasting or # guessing missing values works. Configuration( server='example.com', credentials=Credentials(get_random_string(8), get_random_string(8)), auth_type=NTLM, version=Version(build=Build(15, 1, 2, 3), api_version='foo'), ) def test_fail_fast_back_off(self): # Test that FailFast does not support back-off logic c = FailFast() self.assertIsNone(c.back_off_until) with self.assertRaises(AttributeError): c.back_off_until = 1 def test_service_account_back_off(self): # Test back-off logic in FaultTolerance sa = FaultTolerance() # Initially, the value is None self.assertIsNone(sa.back_off_until) # Test a non-expired back off value in_a_while = datetime.datetime.now() + datetime.timedelta(seconds=10) sa.back_off_until = in_a_while self.assertEqual(sa.back_off_until, in_a_while) # Test an expired back off value sa.back_off_until = datetime.datetime.now() time.sleep(0.001) self.assertIsNone(sa.back_off_until) # Test the back_off() helper sa.back_off(10) # This is not a precise test. Assuming fast computers, there should be less than 1 second between the two lines. self.assertEqual(int(math.ceil((sa.back_off_until - datetime.datetime.now()).total_seconds())), 10) # Test expiry sa.back_off(0) time.sleep(0.001) self.assertIsNone(sa.back_off_until) # Test default value sa.back_off(None) self.assertEqual(int(math.ceil((sa.back_off_until - datetime.datetime.now()).total_seconds())), 60) exchangelib-4.6.1/tests/test_credentials.py000066400000000000000000000041371414601472700210560ustar00rootroot00000000000000import pickle from exchangelib.account import Identity from exchangelib.credentials import Credentials, OAuth2Credentials, OAuth2AuthorizationCodeCredentials from .common import TimedTestCase class CredentialsTest(TimedTestCase): def test_hash(self): # Test that we can use credentials as a dict key self.assertEqual(hash(Credentials('a', 'b')), hash(Credentials('a', 'b'))) self.assertNotEqual(hash(Credentials('a', 'b')), hash(Credentials('a', 'a'))) self.assertNotEqual(hash(Credentials('a', 'b')), hash(Credentials('b', 'b'))) def test_equality(self): self.assertEqual(Credentials('a', 'b'), Credentials('a', 'b')) self.assertNotEqual(Credentials('a', 'b'), Credentials('a', 'a')) self.assertNotEqual(Credentials('a', 'b'), Credentials('b', 'b')) def test_type(self): self.assertEqual(Credentials('a', 'b').type, Credentials.UPN) self.assertEqual(Credentials('a@example.com', 'b').type, Credentials.EMAIL) self.assertEqual(Credentials('a\\n', 'b').type, Credentials.DOMAIN) def test_pickle(self): # Test that we can pickle, hash, repr, str and compare various credentials types for o in ( Credentials('XXX', 'YYY'), OAuth2Credentials('XXX', 'YYY', 'ZZZZ'), OAuth2Credentials('XXX', 'YYY', 'ZZZZ', identity=Identity('AAA')), OAuth2AuthorizationCodeCredentials(client_id='WWW', client_secret='XXX'), OAuth2AuthorizationCodeCredentials( client_id='WWW', client_secret='XXX', authorization_code='YYY', access_token={'access_token': 'ZZZ'}, tenant_id='ZZZ', identity=Identity('AAA') ), ): with self.subTest(o=o): pickled_o = pickle.dumps(o) unpickled_o = pickle.loads(pickled_o) self.assertIsInstance(unpickled_o, type(o)) self.assertEqual(o, unpickled_o) self.assertEqual(hash(o), hash(unpickled_o)) self.assertEqual(repr(o), repr(unpickled_o)) self.assertEqual(str(o), str(unpickled_o)) exchangelib-4.6.1/tests/test_ewsdatetime.py000066400000000000000000000267431414601472700211030ustar00rootroot00000000000000import datetime import dateutil.tz import pytz import requests_mock import warnings try: import zoneinfo except ImportError: from backports import zoneinfo from exchangelib.errors import UnknownTimeZone, NaiveDateTimeNotAllowed from exchangelib.ewsdatetime import EWSDateTime, EWSDate, EWSTimeZone, UTC from exchangelib.winzone import generate_map, CLDR_TO_MS_TIMEZONE_MAP, CLDR_WINZONE_URL, CLDR_WINZONE_TYPE_VERSION, \ CLDR_WINZONE_OTHER_VERSION from exchangelib.util import CONNECTION_ERRORS from .common import TimedTestCase class EWSDateTimeTest(TimedTestCase): def test_super_methods(self): tz = EWSTimeZone('Europe/Copenhagen') self.assertIsInstance(EWSDateTime.now(), EWSDateTime) self.assertIsInstance(EWSDateTime.now(tz=tz), EWSDateTime) self.assertIsInstance(EWSDateTime.utcnow(), EWSDateTime) self.assertIsInstance(EWSDateTime.fromtimestamp(123456789), EWSDateTime) self.assertIsInstance(EWSDateTime.fromtimestamp(123456789, tz=tz), EWSDateTime) self.assertIsInstance(EWSDateTime.utcfromtimestamp(123456789), EWSDateTime) def test_ewstimezone(self): # Test autogenerated translations tz = EWSTimeZone('Europe/Copenhagen') self.assertIsInstance(tz, EWSTimeZone) self.assertEqual(tz.key, 'Europe/Copenhagen') self.assertEqual(tz.ms_id, 'Romance Standard Time') # self.assertEqual(EWSTimeZone('Europe/Copenhagen').ms_name, '') # EWS works fine without the ms_name # Test localzone() tz = EWSTimeZone.localzone() self.assertIsInstance(tz, EWSTimeZone) # Test common helpers tz = EWSTimeZone('UTC') self.assertIsInstance(tz, EWSTimeZone) self.assertEqual(tz.key, 'UTC') self.assertEqual(tz.ms_id, 'UTC') tz = EWSTimeZone('GMT') self.assertIsInstance(tz, EWSTimeZone) self.assertEqual(tz.key, 'GMT') self.assertEqual(tz.ms_id, 'UTC') # Test mapper contents. Latest map from unicode.org has 394 entries self.assertGreater(len(EWSTimeZone.IANA_TO_MS_MAP), 300) for k, v in EWSTimeZone.IANA_TO_MS_MAP.items(): self.assertIsInstance(k, str) self.assertIsInstance(v, tuple) self.assertEqual(len(v), 2) self.assertIsInstance(v[0], str) # Test IANA exceptions sanitized = list(t for t in zoneinfo.available_timezones() if not t.startswith('SystemV/') and t != 'localtime') self.assertEqual(set(sanitized) - set(EWSTimeZone.IANA_TO_MS_MAP), set()) # Test timezone unknown by ZoneInfo with self.assertRaises(UnknownTimeZone) as e: EWSTimeZone('UNKNOWN') self.assertEqual(e.exception.args[0], 'No time zone found with key UNKNOWN') # Test timezone known by IANA but with no Winzone mapping with self.assertRaises(UnknownTimeZone) as e: del EWSTimeZone.IANA_TO_MS_MAP['Africa/Tripoli'] EWSTimeZone('Africa/Tripoli') self.assertEqual(e.exception.args[0], 'No Windows timezone name found for timezone "Africa/Tripoli"') # Test __eq__ with non-EWSTimeZone compare self.assertFalse(EWSTimeZone('GMT') == zoneinfo.ZoneInfo('UTC')) # Test from_ms_id() with non-standard MS ID self.assertEqual(EWSTimeZone('Europe/Copenhagen'), EWSTimeZone.from_ms_id('Europe/Copenhagen')) def test_from_timezone(self): self.assertEqual( EWSTimeZone('Europe/Copenhagen'), EWSTimeZone.from_timezone(EWSTimeZone('Europe/Copenhagen')) ) self.assertEqual( EWSTimeZone('Europe/Copenhagen'), EWSTimeZone.from_timezone(zoneinfo.ZoneInfo('Europe/Copenhagen')) ) self.assertEqual( EWSTimeZone('Europe/Copenhagen'), EWSTimeZone.from_timezone(dateutil.tz.gettz('Europe/Copenhagen')) ) self.assertEqual( EWSTimeZone('Europe/Copenhagen'), EWSTimeZone.from_timezone(pytz.timezone('Europe/Copenhagen')) ) self.assertEqual( EWSTimeZone('UTC'), EWSTimeZone.from_timezone(dateutil.tz.UTC) ) self.assertEqual( EWSTimeZone('UTC'), EWSTimeZone.from_timezone(datetime.timezone.utc) ) def test_localize(self): # Test some corner cases around DST tz = EWSTimeZone('Europe/Copenhagen') with warnings.catch_warnings(): # localize() is deprecated but we still want to test it. Silence the DeprecationWarning warnings.simplefilter("ignore") self.assertEqual( str(tz.localize(EWSDateTime(2023, 10, 29, 2, 36, 0), is_dst=False)), '2023-10-29 02:36:00+01:00' ) self.assertEqual( str(tz.localize(EWSDateTime(2023, 10, 29, 2, 36, 0), is_dst=None)), '2023-10-29 02:36:00+02:00' ) self.assertEqual( str(tz.localize(EWSDateTime(2023, 10, 29, 2, 36, 0), is_dst=True)), '2023-10-29 02:36:00+02:00' ) self.assertEqual( str(tz.localize(EWSDateTime(2023, 3, 26, 2, 36, 0), is_dst=False)), '2023-03-26 02:36:00+01:00' ) self.assertEqual( str(tz.localize(EWSDateTime(2023, 3, 26, 2, 36, 0), is_dst=None)), '2023-03-26 02:36:00+01:00' ) self.assertEqual( str(tz.localize(EWSDateTime(2023, 3, 26, 2, 36, 0), is_dst=True)), '2023-03-26 02:36:00+02:00' ) def test_ewsdatetime(self): # Test a static timezone tz = EWSTimeZone('Etc/GMT-5') dt = EWSDateTime(2000, 1, 2, 3, 4, 5, 678901, tzinfo=tz) self.assertIsInstance(dt, EWSDateTime) self.assertIsInstance(dt.tzinfo, EWSTimeZone) self.assertEqual(dt.tzinfo.ms_id, tz.ms_id) self.assertEqual(dt.tzinfo.ms_name, tz.ms_name) self.assertEqual(str(dt), '2000-01-02 03:04:05.678901+05:00') self.assertEqual( repr(dt), "EWSDateTime(2000, 1, 2, 3, 4, 5, 678901, tzinfo=EWSTimeZone(key='Etc/GMT-5'))" ) # Test a DST timezone tz = EWSTimeZone('Europe/Copenhagen') dt = EWSDateTime(2000, 1, 2, 3, 4, 5, 678901, tzinfo=tz) self.assertIsInstance(dt, EWSDateTime) self.assertIsInstance(dt.tzinfo, EWSTimeZone) self.assertEqual(dt.tzinfo.ms_id, tz.ms_id) self.assertEqual(dt.tzinfo.ms_name, tz.ms_name) self.assertEqual(str(dt), '2000-01-02 03:04:05.678901+01:00') self.assertEqual( repr(dt), "EWSDateTime(2000, 1, 2, 3, 4, 5, 678901, tzinfo=EWSTimeZone(key='Europe/Copenhagen'))" ) # Test from_string with self.assertRaises(NaiveDateTimeNotAllowed): EWSDateTime.from_string('2000-01-02T03:04:05') self.assertEqual( EWSDateTime.from_string('2000-01-02T03:04:05+01:00'), EWSDateTime(2000, 1, 2, 2, 4, 5, tzinfo=UTC) ) self.assertEqual( EWSDateTime.from_string('2000-01-02T03:04:05Z'), EWSDateTime(2000, 1, 2, 3, 4, 5, tzinfo=UTC) ) self.assertIsInstance(EWSDateTime.from_string('2000-01-02T03:04:05+01:00'), EWSDateTime) self.assertIsInstance(EWSDateTime.from_string('2000-01-02T03:04:05Z'), EWSDateTime) # Test addition, subtraction, summertime etc self.assertIsInstance(dt + datetime.timedelta(days=1), EWSDateTime) self.assertIsInstance(dt - datetime.timedelta(days=1), EWSDateTime) self.assertIsInstance(dt - EWSDateTime.now(tz=tz), datetime.timedelta) self.assertIsInstance(EWSDateTime.now(tz=tz), EWSDateTime) # Test various input for from_datetime() self.assertEqual(dt, EWSDateTime.from_datetime( datetime.datetime(2000, 1, 2, 3, 4, 5, 678901, tzinfo=EWSTimeZone('Europe/Copenhagen')) )) self.assertEqual(dt, EWSDateTime.from_datetime( datetime.datetime(2000, 1, 2, 3, 4, 5, 678901, tzinfo=zoneinfo.ZoneInfo('Europe/Copenhagen')) )) self.assertEqual(dt, EWSDateTime.from_datetime( datetime.datetime(2000, 1, 2, 3, 4, 5, 678901, tzinfo=dateutil.tz.gettz('Europe/Copenhagen')) )) self.assertEqual(dt, EWSDateTime.from_datetime( datetime.datetime(2000, 1, 2, 3, 4, 5, 678901, tzinfo=pytz.timezone('Europe/Copenhagen')) )) self.assertEqual(dt.ewsformat(), '2000-01-02T03:04:05.678901+01:00') utc_tz = EWSTimeZone('UTC') self.assertEqual(dt.astimezone(utc_tz).ewsformat(), '2000-01-02T02:04:05.678901Z') # Test summertime dt = EWSDateTime(2000, 8, 2, 3, 4, 5, 678901, tzinfo=tz) self.assertEqual(dt.astimezone(utc_tz).ewsformat(), '2000-08-02T01:04:05.678901Z') # Test in-place add and subtract dt = EWSDateTime(2000, 1, 2, 3, 4, 5, tzinfo=tz) dt += datetime.timedelta(days=1) self.assertIsInstance(dt, EWSDateTime) self.assertEqual(dt, EWSDateTime(2000, 1, 3, 3, 4, 5, tzinfo=tz)) dt = EWSDateTime(2000, 1, 2, 3, 4, 5, tzinfo=tz) dt -= datetime.timedelta(days=1) self.assertIsInstance(dt, EWSDateTime) self.assertEqual(dt, EWSDateTime(2000, 1, 1, 3, 4, 5, tzinfo=tz)) # Test ewsformat() failure dt = EWSDateTime(2000, 1, 2, 3, 4, 5) with self.assertRaises(ValueError): dt.ewsformat() # Test wrong tzinfo type with self.assertRaises(ValueError): EWSDateTime(2000, 1, 2, 3, 4, 5, tzinfo=pytz.timezone('Europe/Copenhagen')) with self.assertRaises(ValueError): EWSDateTime.from_datetime(EWSDateTime(2000, 1, 2, 3, 4, 5)) def test_generate(self): try: type_version, other_version, tz_map = generate_map() except CONNECTION_ERRORS: # generate_map() requires access to unicode.org, which may be unavailable. Don't fail test, since this is # out of our control. return self.assertEqual(type_version, CLDR_WINZONE_TYPE_VERSION) self.assertEqual(other_version, CLDR_WINZONE_OTHER_VERSION) self.assertDictEqual(tz_map, CLDR_TO_MS_TIMEZONE_MAP) @requests_mock.mock() def test_generate_failure(self, m): m.get(CLDR_WINZONE_URL, status_code=500) with self.assertRaises(ValueError): generate_map() def test_ewsdate(self): self.assertEqual(EWSDate(2000, 1, 1).ewsformat(), '2000-01-01') self.assertEqual(EWSDate.from_string('2000-01-01'), EWSDate(2000, 1, 1)) self.assertEqual(EWSDate.from_string('2000-01-01Z'), EWSDate(2000, 1, 1)) self.assertEqual(EWSDate.from_string('2000-01-01+01:00'), EWSDate(2000, 1, 1)) self.assertEqual(EWSDate.from_string('2000-01-01-01:00'), EWSDate(2000, 1, 1)) self.assertIsInstance(EWSDate(2000, 1, 2) - EWSDate(2000, 1, 1), datetime.timedelta) self.assertIsInstance(EWSDate(2000, 1, 2) + datetime.timedelta(days=1), EWSDate) self.assertIsInstance(EWSDate(2000, 1, 2) - datetime.timedelta(days=1), EWSDate) # Test in-place add and subtract dt = EWSDate(2000, 1, 2) dt += datetime.timedelta(days=1) self.assertIsInstance(dt, EWSDate) self.assertEqual(dt, EWSDate(2000, 1, 3)) dt = EWSDate(2000, 1, 2) dt -= datetime.timedelta(days=1) self.assertIsInstance(dt, EWSDate) self.assertEqual(dt, EWSDate(2000, 1, 1)) with self.assertRaises(ValueError): EWSDate.from_date(EWSDate(2000, 1, 2)) exchangelib-4.6.1/tests/test_extended_properties.py000066400000000000000000000300301414601472700226240ustar00rootroot00000000000000from exchangelib.extended_properties import ExtendedProperty, Flag from exchangelib.items import Message, CalendarItem from exchangelib.folders import Inbox from exchangelib.properties import Mailbox from .common import get_random_int, get_random_url from .test_items.test_basics import BaseItemTest class ExtendedPropertyTest(BaseItemTest): TEST_FOLDER = 'inbox' FOLDER_CLASS = Inbox ITEM_CLASS = Message def test_register(self): # Tests that we can register and de-register custom extended properties class TestProp(ExtendedProperty): property_set_id = 'deadbeaf-cafe-cafe-cafe-deadbeefcafe' property_name = 'Test Property' property_type = 'Integer' attr_name = 'dead_beef' # Before register self.assertNotIn(attr_name, {f.name for f in self.ITEM_CLASS.supported_fields(self.account.version)}) with self.assertRaises(ValueError): self.ITEM_CLASS.deregister(attr_name) # Not registered yet with self.assertRaises(ValueError): self.ITEM_CLASS.deregister('subject') # Not an extended property self.ITEM_CLASS.register(attr_name=attr_name, attr_cls=TestProp) try: # After register self.assertEqual(TestProp.python_type(), int) self.assertIn(attr_name, {f.name for f in self.ITEM_CLASS.supported_fields(self.account.version)}) # Test item creation, refresh, and update item = self.get_test_item(folder=self.test_folder) prop_val = item.dead_beef self.assertTrue(isinstance(prop_val, int)) item.save() item.refresh() self.assertEqual(prop_val, item.dead_beef) new_prop_val = get_random_int(0, 256) item.dead_beef = new_prop_val item.save() item.refresh() self.assertEqual(new_prop_val, item.dead_beef) # Test deregister with self.assertRaises(ValueError): self.ITEM_CLASS.register(attr_name=attr_name, attr_cls=TestProp) # Already registered with self.assertRaises(ValueError): self.ITEM_CLASS.register(attr_name='XXX', attr_cls=Mailbox) # Not an extended property finally: self.ITEM_CLASS.deregister(attr_name=attr_name) self.assertNotIn(attr_name, {f.name for f in self.ITEM_CLASS.supported_fields(self.account.version)}) def test_extended_property_arraytype(self): # Tests array type extended properties class TestArayProp(ExtendedProperty): property_set_id = 'deadcafe-beef-beef-beef-deadcafebeef' property_name = 'Test Array Property' property_type = 'IntegerArray' attr_name = 'dead_beef_array' self.ITEM_CLASS.register(attr_name=attr_name, attr_cls=TestArayProp) try: # Test item creation, refresh, and update item = self.get_test_item(folder=self.test_folder) prop_val = item.dead_beef_array self.assertTrue(isinstance(prop_val, list)) item.save() item.refresh() self.assertEqual(prop_val, item.dead_beef_array) new_prop_val = self.random_val(self.ITEM_CLASS.get_field_by_fieldname(attr_name)) item.dead_beef_array = new_prop_val item.save() item.refresh() self.assertEqual(new_prop_val, item.dead_beef_array) finally: self.ITEM_CLASS.deregister(attr_name=attr_name) def test_extended_property_with_tag(self): attr_name = 'my_flag' self.ITEM_CLASS.register(attr_name=attr_name, attr_cls=Flag) try: # Test item creation, refresh, and update item = self.get_test_item(folder=self.test_folder) prop_val = item.my_flag self.assertTrue(isinstance(prop_val, int)) item.save() item.refresh() self.assertEqual(prop_val, item.my_flag) new_prop_val = self.random_val(self.ITEM_CLASS.get_field_by_fieldname(attr_name)) item.my_flag = new_prop_val item.save() item.refresh() self.assertEqual(new_prop_val, item.my_flag) finally: self.ITEM_CLASS.deregister(attr_name=attr_name) def test_extended_property_with_invalid_tag(self): class InvalidProp(ExtendedProperty): property_tag = '0x8000' property_type = 'Integer' with self.assertRaises(ValueError): InvalidProp('Foo').clean() # property_tag is in protected range def test_extended_property_with_string_tag(self): attr_name = 'my_flag' self.ITEM_CLASS.register(attr_name=attr_name, attr_cls=Flag) try: # Test item creation, refresh, and update item = self.get_test_item(folder=self.test_folder) prop_val = item.my_flag self.assertTrue(isinstance(prop_val, int)) item.save() item.refresh() self.assertEqual(prop_val, item.my_flag) new_prop_val = self.random_val(self.ITEM_CLASS.get_field_by_fieldname(attr_name)) item.my_flag = new_prop_val item.save() item.refresh() self.assertEqual(new_prop_val, item.my_flag) finally: self.ITEM_CLASS.deregister(attr_name=attr_name) def test_extended_distinguished_property(self): if self.ITEM_CLASS == CalendarItem: # MyMeeting is an extended prop version of the 'CalendarItem.uid' field. They don't work together. self.skipTest("This extendedproperty doesn't work on CalendarItems") class MyMeeting(ExtendedProperty): distinguished_property_set_id = 'Meeting' property_type = 'Binary' property_id = 3 attr_name = 'my_meeting' self.ITEM_CLASS.register(attr_name=attr_name, attr_cls=MyMeeting) try: # Test item creation, refresh, and update item = self.get_test_item(folder=self.test_folder) prop_val = item.my_meeting self.assertTrue(isinstance(prop_val, bytes)) item.save() item = self.get_item_by_id((item.id, item.changekey)) self.assertEqual(prop_val, item.my_meeting, (prop_val, item.my_meeting)) new_prop_val = self.random_val(self.ITEM_CLASS.get_field_by_fieldname(attr_name)) item.my_meeting = new_prop_val item.save() item = self.get_item_by_id((item.id, item.changekey)) self.assertEqual(new_prop_val, item.my_meeting) finally: self.ITEM_CLASS.deregister(attr_name=attr_name) def test_extended_property_binary_array(self): class MyMeetingArray(ExtendedProperty): property_set_id = '00062004-0000-0000-C000-000000000046' property_type = 'BinaryArray' property_id = 32852 attr_name = 'my_meeting_array' self.ITEM_CLASS.register(attr_name=attr_name, attr_cls=MyMeetingArray) try: # Test item creation, refresh, and update item = self.get_test_item(folder=self.test_folder) prop_val = item.my_meeting_array self.assertTrue(isinstance(prop_val, list)) item.save() item = self.get_item_by_id((item.id, item.changekey)) self.assertEqual(prop_val, item.my_meeting_array) new_prop_val = self.random_val(self.ITEM_CLASS.get_field_by_fieldname(attr_name)) item.my_meeting_array = new_prop_val item.save() item = self.get_item_by_id((item.id, item.changekey)) self.assertEqual(new_prop_val, item.my_meeting_array) finally: self.ITEM_CLASS.deregister(attr_name=attr_name) def test_extended_property_validation(self): # Must not have property_set_id or property_tag class TestProp1(ExtendedProperty): distinguished_property_set_id = 'XXX' property_set_id = 'YYY' with self.assertRaises(ValueError): TestProp1.validate_cls() # Must have property_id or property_name class TestProp2(ExtendedProperty): distinguished_property_set_id = 'XXX' with self.assertRaises(ValueError): TestProp2.validate_cls() # distinguished_property_set_id must have a valid value class TestProp3(ExtendedProperty): distinguished_property_set_id = 'XXX' property_id = 'YYY' with self.assertRaises(ValueError): TestProp3.validate_cls() # Must not have distinguished_property_set_id or property_tag class TestProp4(ExtendedProperty): property_set_id = 'XXX' property_tag = 'YYY' with self.assertRaises(ValueError): TestProp4.validate_cls() # Must have property_id or property_name class TestProp5(ExtendedProperty): property_set_id = 'XXX' with self.assertRaises(ValueError): TestProp5.validate_cls() # property_tag is only compatible with property_type class TestProp6(ExtendedProperty): property_tag = 'XXX' property_set_id = 'YYY' with self.assertRaises(ValueError): TestProp6.validate_cls() # property_tag must be an integer or string that can be converted to int class TestProp7(ExtendedProperty): property_tag = 'XXX' with self.assertRaises(ValueError): TestProp7.validate_cls() # property_tag must not be in the reserved range class TestProp8(ExtendedProperty): property_tag = 0x8001 with self.assertRaises(ValueError): TestProp8.validate_cls() # Must not have property_id or property_tag class TestProp9(ExtendedProperty): property_name = 'XXX' property_id = 'YYY' with self.assertRaises(ValueError): TestProp9.validate_cls() # Must have distinguished_property_set_id or property_set_id class TestProp10(ExtendedProperty): property_name = 'XXX' with self.assertRaises(ValueError): TestProp10.validate_cls() # Must not have property_name or property_tag class TestProp11(ExtendedProperty): property_id = 'XXX' property_name = 'YYY' with self.assertRaises(ValueError): TestProp11.validate_cls() # This actually hits the check on property_name values # Must have distinguished_property_set_id or property_set_id class TestProp12(ExtendedProperty): property_id = 'XXX' with self.assertRaises(ValueError): TestProp12.validate_cls() # property_type must be a valid value class TestProp13(ExtendedProperty): property_id = 'XXX' property_set_id = 'YYY' property_type = 'ZZZ' with self.assertRaises(ValueError): TestProp13.validate_cls() def test_multiple_extended_properties(self): class ExternalSharingUrl(ExtendedProperty): property_set_id = "F52A8693-C34D-4980-9E20-9D4C1EABB6A7" property_name = "ExternalSharingUrl" property_type = "String" class ExternalSharingFolderId(ExtendedProperty): property_set_id = "F52A8693-C34D-4980-9E20-9D4C1EABB6A7" property_name = "ExternalSharingLocalFolderId" property_type = "Binary" try: self.ITEM_CLASS.register("sharing_url", ExternalSharingUrl) self.ITEM_CLASS.register("sharing_folder_id", ExternalSharingFolderId) url, folder_id = get_random_url(), self.test_folder.id.encode('utf-8') m = self.get_test_item() m.sharing_url, m.sharing_folder_id = url, folder_id m.save() m = self.test_folder.get(sharing_url=url) self.assertEqual(m.sharing_url, url) self.assertEqual(m.sharing_folder_id, folder_id) finally: self.ITEM_CLASS.deregister("sharing_url") self.ITEM_CLASS.deregister("sharing_folder_id") exchangelib-4.6.1/tests/test_field.py000066400000000000000000000270611414601472700176450ustar00rootroot00000000000000from collections import namedtuple import datetime from decimal import Decimal try: import zoneinfo except ImportError: from backports import zoneinfo from exchangelib.extended_properties import ExternId from exchangelib.fields import BooleanField, IntegerField, DecimalField, TextField, ChoiceField, DateTimeField, \ Base64Field, TimeZoneField, ExtendedPropertyField, CharListField, Choice, DateField, EnumField, EnumListField, \ CharField, InvalidFieldForVersion, InvalidChoiceForVersion from exchangelib.indexed_properties import SingleFieldIndexedElement from exchangelib.version import Version, EXCHANGE_2007, EXCHANGE_2010, EXCHANGE_2013 from exchangelib.util import to_xml, TNS from .common import TimedTestCase class FieldTest(TimedTestCase): def test_value_validation(self): field = TextField('foo', field_uri='bar', is_required=True, default=None) with self.assertRaises(ValueError) as e: field.clean(None) # Must have a default value on None input self.assertEqual(str(e.exception), "'foo' is a required field with no default") field = TextField('foo', field_uri='bar', is_required=True, default='XXX') self.assertEqual(field.clean(None), 'XXX') field = CharListField('foo', field_uri='bar') with self.assertRaises(ValueError) as e: field.clean('XXX') # Must be a list type self.assertEqual(str(e.exception), "Field 'foo' value 'XXX' must be a list") field = CharListField('foo', field_uri='bar') with self.assertRaises(TypeError) as e: field.clean([1, 2, 3]) # List items must be correct type self.assertEqual(str(e.exception), "Field 'foo' value 1 must be of type ") field = CharField('foo', field_uri='bar') with self.assertRaises(TypeError) as e: field.clean(1) # Value must be correct type self.assertEqual(str(e.exception), "Field 'foo' value 1 must be of type ") with self.assertRaises(ValueError) as e: field.clean('X' * 256) # Value length must be within max_length self.assertEqual( str(e.exception), "'foo' value 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' exceeds length 255" ) field = DateTimeField('foo', field_uri='bar') with self.assertRaises(ValueError) as e: field.clean(datetime.datetime(2017, 1, 1)) # Datetime values must be timezone aware self.assertEqual(str(e.exception), "Value '2017-01-01 00:00:00' on field 'foo' must be timezone aware") field = ChoiceField('foo', field_uri='bar', choices=[Choice('foo'), Choice('bar')]) with self.assertRaises(ValueError) as e: field.clean('XXX') # Value must be a valid choice self.assertEqual(str(e.exception), "Invalid choice 'XXX' for field 'foo'. Valid choices are: foo, bar") # A few tests on extended properties that override base methods field = ExtendedPropertyField('foo', value_cls=ExternId, is_required=True) with self.assertRaises(ValueError) as e: field.clean(None) # Value is required self.assertEqual(str(e.exception), "'foo' is a required field") with self.assertRaises(TypeError) as e: field.clean(123) # Correct type is required self.assertEqual(str(e.exception), "'ExternId' value 123 must be an instance of ") self.assertEqual(field.clean('XXX'), 'XXX') # We can clean a simple value and keep it as a simple value self.assertEqual(field.clean(ExternId('XXX')), ExternId('XXX')) # We can clean an ExternId instance as well class ExternIdArray(ExternId): property_type = 'StringArray' field = ExtendedPropertyField('foo', value_cls=ExternIdArray, is_required=True) with self.assertRaises(ValueError)as e: field.clean(None) # Value is required self.assertEqual(str(e.exception), "'foo' is a required field") with self.assertRaises(ValueError)as e: field.clean(123) # Must be an iterable self.assertEqual(str(e.exception), "'ExternIdArray' value 123 must be a list") with self.assertRaises(TypeError) as e: field.clean([123]) # Correct type is required self.assertEqual(str(e.exception), "'ExternIdArray' value element 123 must be an instance of ") # Test min/max on IntegerField field = IntegerField('foo', field_uri='bar', min=5, max=10) with self.assertRaises(ValueError) as e: field.clean(2) self.assertEqual(str(e.exception), "Value 2 on field 'foo' must be greater than 5") with self.assertRaises(ValueError)as e: field.clean(12) self.assertEqual(str(e.exception), "Value 12 on field 'foo' must be less than 10") # Test min/max on DecimalField field = DecimalField('foo', field_uri='bar', min=5, max=10) with self.assertRaises(ValueError) as e: field.clean(Decimal(2)) self.assertEqual(str(e.exception), "Value Decimal('2') on field 'foo' must be greater than 5") with self.assertRaises(ValueError)as e: field.clean(Decimal(12)) self.assertEqual(str(e.exception), "Value Decimal('12') on field 'foo' must be less than 10") # Test enum validation field = EnumField('foo', field_uri='bar', enum=['a', 'b', 'c']) with self.assertRaises(ValueError)as e: field.clean(0) # Enums start at 1 self.assertEqual(str(e.exception), "Value 0 on field 'foo' must be greater than 1") with self.assertRaises(ValueError) as e: field.clean(4) # Spills over list self.assertEqual(str(e.exception), "Value 4 on field 'foo' must be less than 3") with self.assertRaises(ValueError) as e: field.clean('d') # Value not in enum self.assertEqual(str(e.exception), "Value 'd' on field 'foo' must be one of ['a', 'b', 'c']") # Test enum list validation field = EnumListField('foo', field_uri='bar', enum=['a', 'b', 'c']) with self.assertRaises(ValueError)as e: field.clean([]) self.assertEqual(str(e.exception), "Value '[]' on field 'foo' must not be empty") with self.assertRaises(ValueError) as e: field.clean([0]) self.assertEqual(str(e.exception), "Value 0 on field 'foo' must be greater than 1") with self.assertRaises(ValueError) as e: field.clean([1, 1]) # Values must be unique self.assertEqual(str(e.exception), "List entries '[1, 1]' on field 'foo' must be unique") with self.assertRaises(ValueError) as e: field.clean(['d']) self.assertEqual(str(e.exception), "List value 'd' on field 'foo' must be one of ['a', 'b', 'c']") def test_garbage_input(self): # Test that we can survive garbage input for common field types tz = zoneinfo.ZoneInfo('Europe/Copenhagen') account = namedtuple('Account', ['default_timezone'])(default_timezone=tz) payload = b'''\ THIS_IS_GARBAGE ''' elem = to_xml(payload).find('{%s}Item' % TNS) for field_cls in (Base64Field, BooleanField, IntegerField, DateField, DateTimeField, DecimalField): field = field_cls('foo', field_uri='item:Foo', is_required=True, default='DUMMY') self.assertEqual(field.from_xml(elem=elem, account=account), None) # Test MS timezones payload = b'''\ ''' elem = to_xml(payload).find('{%s}Item' % TNS) field = TimeZoneField('foo', field_uri='item:Foo', default='DUMMY') self.assertEqual(field.from_xml(elem=elem, account=account), None) def test_versioned_field(self): field = TextField('foo', field_uri='bar', supported_from=EXCHANGE_2010) with self.assertRaises(InvalidFieldForVersion): field.clean('baz', version=Version(EXCHANGE_2007)) field.clean('baz', version=Version(EXCHANGE_2010)) field.clean('baz', version=Version(EXCHANGE_2013)) def test_versioned_choice(self): field = ChoiceField('foo', field_uri='bar', choices={ Choice('c1'), Choice('c2', supported_from=EXCHANGE_2010) }) with self.assertRaises(ValueError): field.clean('XXX') # Value must be a valid choice field.clean('c2', version=None) with self.assertRaises(InvalidChoiceForVersion): field.clean('c2', version=Version(EXCHANGE_2007)) field.clean('c2', version=Version(EXCHANGE_2010)) field.clean('c2', version=Version(EXCHANGE_2013)) def test_naive_datetime(self): # Test that we can survive naive datetimes on a datetime field tz = zoneinfo.ZoneInfo('Europe/Copenhagen') utc = zoneinfo.ZoneInfo('UTC') account = namedtuple('Account', ['default_timezone'])(default_timezone=tz) default_value = datetime.datetime(2017, 1, 2, 3, 4, tzinfo=tz) field = DateTimeField('foo', field_uri='item:DateTimeSent', default=default_value) # TZ-aware datetime string payload = b'''\ 2017-06-21T18:40:02Z ''' elem = to_xml(payload).find('{%s}Item' % TNS) self.assertEqual( field.from_xml(elem=elem, account=account), datetime.datetime(2017, 6, 21, 18, 40, 2, tzinfo=utc) ) # Naive datetime string is localized to tz of the account payload = b'''\ 2017-06-21T18:40:02 ''' elem = to_xml(payload).find('{%s}Item' % TNS) self.assertEqual( field.from_xml(elem=elem, account=account), datetime.datetime(2017, 6, 21, 18, 40, 2, tzinfo=tz) ) # Garbage string returns None payload = b'''\ THIS_IS_GARBAGE ''' elem = to_xml(payload).find('{%s}Item' % TNS) self.assertEqual(field.from_xml(elem=elem, account=account), None) # Element not found returns default value payload = b'''\ ''' elem = to_xml(payload).find('{%s}Item' % TNS) self.assertEqual(field.from_xml(elem=elem, account=account), default_value) def test_single_field_indexed_element(self): # A SingleFieldIndexedElement must have only one field defined class TestField(SingleFieldIndexedElement): a = CharField() b = CharField() with self.assertRaises(ValueError): TestField.value_field() exchangelib-4.6.1/tests/test_folder.py000066400000000000000000001016111414601472700200270ustar00rootroot00000000000000from exchangelib.errors import ErrorDeleteDistinguishedFolder, ErrorObjectTypeChanged, DoesNotExist, \ MultipleObjectsReturned, ErrorItemSave, ErrorItemNotFound from exchangelib.extended_properties import ExtendedProperty from exchangelib.items import Message from exchangelib.folders import Calendar, DeletedItems, Drafts, Inbox, Outbox, SentItems, JunkEmail, Messages, Tasks, \ Contacts, Folder, RecipientCache, GALContacts, System, AllContacts, MyContactsExtended, Reminders, Favorites, \ AllItems, ConversationSettings, Friends, RSSFeeds, Sharing, IMContactList, QuickContacts, Journal, Notes, \ SyncIssues, MyContacts, ToDoSearch, FolderCollection, DistinguishedFolderId, Files, \ DefaultFoldersChangeHistory, PassThroughSearchResults, SmsAndChatsSync, GraphAnalytics, Signal, \ PdpProfileV2Secured, VoiceMail, FolderQuerySet, SingleFolderQuerySet, SHALLOW, RootOfHierarchy, Companies, \ OrganizationalContacts, PeopleCentricConversationBuddies, PublicFoldersRoot from exchangelib.properties import Mailbox, InvalidField, EffectiveRights, PermissionSet, CalendarPermission, UserId from exchangelib.queryset import Q from exchangelib.services import GetFolder from .common import EWSTest, get_random_string, get_random_int, get_random_bool, get_random_datetime, get_random_bytes,\ get_random_byte def get_random_str_tuple(tuple_length, str_length): return tuple(get_random_string(str_length, spaces=False) for _ in range(tuple_length)) class FolderTest(EWSTest): def test_folders(self): for f in self.account.root.walk(): if isinstance(f, System): # No access to system folder, apparently continue f.test_access() # Test shortcuts for f, cls in ( (self.account.trash, DeletedItems), (self.account.drafts, Drafts), (self.account.inbox, Inbox), (self.account.outbox, Outbox), (self.account.sent, SentItems), (self.account.junk, JunkEmail), (self.account.contacts, Contacts), (self.account.tasks, Tasks), (self.account.calendar, Calendar), ): with self.subTest(f=f, cls=cls): self.assertIsInstance(f, cls) f.test_access() # Test item field lookup self.assertEqual(f.get_item_field_by_fieldname('subject').name, 'subject') with self.assertRaises(ValueError): f.get_item_field_by_fieldname('XXX') def test_folder_failure(self): # Folders must have an ID with self.assertRaises(ValueError): self.account.root.get_folder(Folder()) with self.assertRaises(ValueError): self.account.root.add_folder(Folder()) with self.assertRaises(ValueError): self.account.root.update_folder(Folder()) with self.assertRaises(ValueError): self.account.root.remove_folder(Folder()) # Removing a non-existent folder is allowed self.account.root.remove_folder(Folder(id='XXX')) # Must be called on a distinguished folder class with self.assertRaises(ValueError): RootOfHierarchy.get_distinguished(self.account) with self.assertRaises(ValueError): self.account.root.get_default_folder(Folder) def test_public_folders_root(self): # Test account does not have a public folders root. Make a dummy query just to hit .get_children() self.assertGreaterEqual( len(list(PublicFoldersRoot(account=self.account, is_distinguished=True).get_children(self.account.inbox))), 0, ) def test_find_folders(self): folders = list(FolderCollection(account=self.account, folders=[self.account.root]).find_folders()) self.assertGreater(len(folders), 40, sorted(f.name for f in folders)) def test_find_folders_with_restriction(self): # Exact match folders = list(FolderCollection(account=self.account, folders=[self.account.root]) .find_folders(q=Q(name='Top of Information Store'))) self.assertEqual(len(folders), 1, sorted(f.name for f in folders)) # Startswith folders = list(FolderCollection(account=self.account, folders=[self.account.root]) .find_folders(q=Q(name__startswith='Top of '))) self.assertEqual(len(folders), 1, sorted(f.name for f in folders)) # Wrong case folders = list(FolderCollection(account=self.account, folders=[self.account.root]) .find_folders(q=Q(name__startswith='top of '))) self.assertEqual(len(folders), 0, sorted(f.name for f in folders)) # Case insensitive folders = list(FolderCollection(account=self.account, folders=[self.account.root]) .find_folders(q=Q(name__istartswith='top of '))) self.assertEqual(len(folders), 1, sorted(f.name for f in folders)) def test_get_folders(self): folders = list(FolderCollection(account=self.account, folders=[self.account.root]).get_folders()) self.assertEqual(len(folders), 1, sorted(f.name for f in folders)) # Test that GetFolder can handle FolderId instances folders = list(FolderCollection(account=self.account, folders=[DistinguishedFolderId( id=Inbox.DISTINGUISHED_FOLDER_ID, mailbox=Mailbox(email_address=self.account.primary_smtp_address) )]).get_folders()) self.assertEqual(len(folders), 1, sorted(f.name for f in folders)) def test_get_folders_with_distinguished_id(self): # Test that we return an Inbox instance and not a generic Messages or Folder instance when we call GetFolder # with a DistinguishedFolderId instance with an ID of Inbox.DISTINGUISHED_FOLDER_ID. inbox_folder_id = DistinguishedFolderId( id=Inbox.DISTINGUISHED_FOLDER_ID, mailbox=Mailbox(email_address=self.account.primary_smtp_address) ) inbox = list(GetFolder(account=self.account).call( folders=[inbox_folder_id], shape='IdOnly', additional_fields=[], ))[0] self.assertIsInstance(inbox, Inbox) # Test via SingleFolderQuerySet inbox = SingleFolderQuerySet(account=self.account, folder=inbox_folder_id).resolve() self.assertIsInstance(inbox, Inbox) def test_folder_grouping(self): # If you get errors here, you probably need to fill out [folder class].LOCALIZED_NAMES for your locale. for f in self.account.root.walk(): with self.subTest(f=f): if isinstance(f, ( Messages, DeletedItems, AllContacts, MyContactsExtended, Sharing, Favorites, SyncIssues, MyContacts )): self.assertEqual(f.folder_class, 'IPF.Note') elif isinstance(f, Companies): self.assertEqual(f.folder_class, 'IPF.Contact.Company') elif isinstance(f, OrganizationalContacts): self.assertEqual(f.folder_class, 'IPF.Contact.OrganizationalContacts') elif isinstance(f, PeopleCentricConversationBuddies): self.assertEqual(f.folder_class, 'IPF.Contact.PeopleCentricConversationBuddies') elif isinstance(f, GALContacts): self.assertEqual(f.folder_class, 'IPF.Contact.GalContacts') elif isinstance(f, RecipientCache): self.assertEqual(f.folder_class, 'IPF.Contact.RecipientCache') elif isinstance(f, Contacts): self.assertEqual(f.folder_class, 'IPF.Contact') elif isinstance(f, Calendar): self.assertEqual(f.folder_class, 'IPF.Appointment') elif isinstance(f, (Tasks, ToDoSearch)): self.assertEqual(f.folder_class, 'IPF.Task') elif isinstance(f, Reminders): self.assertEqual(f.folder_class, 'Outlook.Reminder') elif isinstance(f, AllItems): self.assertEqual(f.folder_class, 'IPF') elif isinstance(f, ConversationSettings): self.assertEqual(f.folder_class, 'IPF.Configuration') elif isinstance(f, Files): self.assertEqual(f.folder_class, 'IPF.Files') elif isinstance(f, Friends): self.assertEqual(f.folder_class, 'IPF.Note') elif isinstance(f, RSSFeeds): self.assertEqual(f.folder_class, 'IPF.Note.OutlookHomepage') elif isinstance(f, IMContactList): self.assertEqual(f.folder_class, 'IPF.Contact.MOC.ImContactList') elif isinstance(f, QuickContacts): self.assertEqual(f.folder_class, 'IPF.Contact.MOC.QuickContacts') elif isinstance(f, Journal): self.assertEqual(f.folder_class, 'IPF.Journal') elif isinstance(f, Notes): self.assertEqual(f.folder_class, 'IPF.StickyNote') elif isinstance(f, DefaultFoldersChangeHistory): self.assertEqual(f.folder_class, 'IPM.DefaultFolderHistoryItem') elif isinstance(f, PassThroughSearchResults): self.assertEqual(f.folder_class, 'IPF.StoreItem.PassThroughSearchResults') elif isinstance(f, SmsAndChatsSync): self.assertEqual(f.folder_class, 'IPF.SmsAndChatsSync') elif isinstance(f, GraphAnalytics): self.assertEqual(f.folder_class, 'IPF.StoreItem.GraphAnalytics') elif isinstance(f, Signal): self.assertEqual(f.folder_class, 'IPF.StoreItem.Signal') elif isinstance(f, PdpProfileV2Secured): self.assertEqual(f.folder_class, 'IPF.StoreItem.PdpProfileSecured') elif isinstance(f, VoiceMail): self.assertEqual(f.folder_class, 'IPF.Note.Microsoft.Voicemail') else: self.assertIn(f.folder_class, (None, 'IPF'), (f.name, f.__class__.__name__, f.folder_class)) self.assertIsInstance(f, Folder) def test_counts(self): # Test count values on a folder f = Folder(parent=self.account.inbox, name=get_random_string(16)).save() f.refresh() self.assertEqual(f.total_count, 0) self.assertEqual(f.unread_count, 0) self.assertEqual(f.child_folder_count, 0) # Create some items items = [] for i in range(3): subject = 'Test Subject %s' % i item = Message(account=self.account, folder=f, is_read=False, subject=subject, categories=self.categories) item.save() items.append(item) # Refresh values and see that total_count and unread_count changes f.refresh() self.assertEqual(f.total_count, 3) self.assertEqual(f.unread_count, 3) self.assertEqual(f.child_folder_count, 0) for i in items: i.is_read = True i.save() # Refresh values and see that unread_count changes f.refresh() self.assertEqual(f.total_count, 3) self.assertEqual(f.unread_count, 0) self.assertEqual(f.child_folder_count, 0) self.bulk_delete(items) # Refresh values and see that total_count changes f.refresh() self.assertEqual(f.total_count, 0) self.assertEqual(f.unread_count, 0) self.assertEqual(f.child_folder_count, 0) # Create some subfolders subfolders = [] for i in range(3): subfolders.append(Folder(parent=f, name=get_random_string(16)).save()) # Refresh values and see that child_folder_count changes f.refresh() self.assertEqual(f.total_count, 0) self.assertEqual(f.unread_count, 0) self.assertEqual(f.child_folder_count, 3) for sub_f in subfolders: sub_f.delete() # Refresh values and see that child_folder_count changes f.refresh() self.assertEqual(f.total_count, 0) self.assertEqual(f.unread_count, 0) self.assertEqual(f.child_folder_count, 0) f.delete() def test_refresh(self): # Test that we can refresh folders for f in self.account.root.walk(): with self.subTest(f=f): if isinstance(f, System): # Can't refresh the 'System' folder for some reason continue old_values = {} for field in f.FIELDS: old_values[field.name] = getattr(f, field.name) if field.name in ('account', 'id', 'changekey', 'parent_folder_id'): # These are needed for a successful refresh() continue if field.is_read_only: continue setattr(f, field.name, self.random_val(field)) f.refresh() for field in f.FIELDS: if field.name == 'changekey': # folders may change while we're testing continue if field.is_read_only: # count values may change during the test continue self.assertEqual(getattr(f, field.name), old_values[field.name], (f, field.name)) # Test refresh of root orig_name = self.account.root.name self.account.root.name = 'xxx' self.account.root.refresh() self.assertEqual(self.account.root.name, orig_name) folder = Folder() with self.assertRaises(ValueError): folder.refresh() # Must have root folder folder.root = self.account.root with self.assertRaises(ValueError): folder.refresh() # Must have an id def test_parent(self): self.assertEqual( self.account.calendar.parent.name, 'Top of Information Store' ) self.assertEqual( self.account.calendar.parent.parent.name, 'root' ) def test_children(self): self.assertIn( 'Top of Information Store', [c.name for c in self.account.root.children] ) def test_parts(self): self.assertEqual( [p.name for p in self.account.calendar.parts], ['root', 'Top of Information Store', self.account.calendar.name] ) def test_absolute(self): self.assertEqual( self.account.calendar.absolute, '/root/Top of Information Store/' + self.account.calendar.name ) def test_walk(self): self.assertGreaterEqual(len(list(self.account.root.walk())), 20) self.assertGreaterEqual(len(list(self.account.contacts.walk())), 2) def test_tree(self): self.assertTrue(self.account.root.tree().startswith('root')) def test_glob(self): self.assertGreaterEqual(len(list(self.account.root.glob('*'))), 5) self.assertEqual(len(list(self.account.contacts.glob('GAL*'))), 1) self.assertEqual(len(list(self.account.contacts.glob('gal*'))), 1) # Test case-insensitivity self.assertGreaterEqual(len(list(self.account.contacts.glob('/'))), 5) self.assertGreaterEqual(len(list(self.account.contacts.glob('../*'))), 5) self.assertEqual(len(list(self.account.root.glob('**/%s' % self.account.contacts.name))), 1) self.assertEqual(len(list(self.account.root.glob('Top of*/%s' % self.account.contacts.name))), 1) def test_collection_filtering(self): self.assertGreaterEqual(self.account.root.tois.children.all().count(), 0) self.assertGreaterEqual(self.account.root.tois.walk().all().count(), 0) self.assertGreaterEqual(self.account.root.tois.glob('*').all().count(), 0) def test_empty_collections(self): self.assertEqual(self.account.trash.children.all().count(), 0) self.assertEqual(self.account.trash.walk().all().count(), 0) self.assertEqual(self.account.trash.glob('XXX').all().count(), 0) self.assertEqual(list(self.account.trash.glob('XXX').get_folders()), []) self.assertEqual(list(self.account.trash.glob('XXX').find_folders()), []) def test_div_navigation(self): self.assertEqual( (self.account.root / 'Top of Information Store' / self.account.calendar.name).id, self.account.calendar.id ) self.assertEqual( (self.account.root / 'Top of Information Store' / '..').id, self.account.root.id ) self.assertEqual( (self.account.root / '.').id, self.account.root.id ) def test_double_div_navigation(self): self.account.root.clear_cache() # Clear the cache # Test normal navigation self.assertEqual( (self.account.root // 'Top of Information Store' // self.account.calendar.name).id, self.account.calendar.id ) self.assertIsNone(self.account.root._subfolders) # Test parent ('..') syntax. Should not work with self.assertRaises(ValueError) as e: _ = self.account.root // 'Top of Information Store' // '..' self.assertEqual(e.exception.args[0], 'Cannot get parent without a folder cache') self.assertIsNone(self.account.root._subfolders) # Test self ('.') syntax self.assertEqual( (self.account.root // '.').id, self.account.root.id ) self.assertIsNone(self.account.root._subfolders) def test_extended_properties(self): # Test extended properties on folders and folder roots. This extended prop gets the size (in bytes) of a folder class FolderSize(ExtendedProperty): property_tag = 0x0e08 property_type = 'Integer' try: Folder.register('size', FolderSize) self.account.inbox.refresh() self.assertGreater(self.account.inbox.size, 0) finally: Folder.deregister('size') try: RootOfHierarchy.register('size', FolderSize) self.account.root.refresh() self.assertGreater(self.account.root.size, 0) finally: RootOfHierarchy.deregister('size') # Register and deregister is only allowed on Folder and RootOfHierarchy classes with self.assertRaises(TypeError): self.account.calendar.register(FolderSize) with self.assertRaises(TypeError): self.account.calendar.deregister(FolderSize) with self.assertRaises(TypeError): self.account.root.register(FolderSize) with self.assertRaises(TypeError): self.account.root.deregister(FolderSize) def test_create_update_empty_delete(self): f = Messages(parent=self.account.inbox, name=get_random_string(16)) f.save() self.assertIsNotNone(f.id) self.assertIsNotNone(f.changekey) new_name = get_random_string(16) f.name = new_name f.save() f.refresh() self.assertEqual(f.name, new_name) with self.assertRaises(ErrorObjectTypeChanged): # FolderClass may not be changed f.folder_class = get_random_string(16) f.save(update_fields=['folder_class']) # Create a subfolder Messages(parent=f, name=get_random_string(16)).save() self.assertEqual(len(list(f.children)), 1) f.empty() self.assertEqual(len(list(f.children)), 1) f.empty(delete_sub_folders=True) self.assertEqual(len(list(f.children)), 0) # Create a subfolder again, and delete it by wiping Messages(parent=f, name=get_random_string(16)).save() self.assertEqual(len(list(f.children)), 1) f.wipe() self.assertEqual(len(list(f.children)), 0) f.delete() with self.assertRaises(ValueError): # No longer has an ID f.refresh() # Delete all subfolders of inbox for c in self.account.inbox.children: c.delete() with self.assertRaises(ErrorDeleteDistinguishedFolder): self.account.inbox.delete() def test_move(self): f1 = Folder(parent=self.account.inbox, name=get_random_string(16)).save() f2 = Folder(parent=self.account.inbox, name=get_random_string(16)).save() f1_id, f1_changekey, f1_parent = f1.id, f1.changekey, f1.parent f1.move(f2) self.assertEqual(f1.id, f1_id) self.assertNotEqual(f1.changekey, f1_changekey) self.assertEqual(f1.parent, f2) self.assertNotEqual(f1.changekey, f1_parent) f1_id, f1_changekey, f1_parent = f1.id, f1.changekey, f1.parent f1.refresh() self.assertEqual(f1.id, f1_id) self.assertEqual(f1.parent, f2) self.assertNotEqual(f1.changekey, f1_parent) f1.delete() f2.delete() def test_generic_folder(self): f = Folder(parent=self.account.inbox, name=get_random_string(16)) f.save() f.name = get_random_string(16) f.save() f.delete() def test_folder_query_set(self): # Create a folder hierarchy and test a folder queryset # # -f0 # - f1 # - f2 # - f21 # - f22 f0 = Folder(parent=self.account.inbox, name=get_random_string(16)).save() f1 = Folder(parent=f0, name=get_random_string(16)).save() f2 = Folder(parent=f0, name=get_random_string(16)).save() f21 = Folder(parent=f2, name=get_random_string(16)).save() f22 = Folder(parent=f2, name=get_random_string(16)).save() folder_qs = SingleFolderQuerySet(account=self.account, folder=f0) try: # Test all() self.assertSetEqual( {f.name for f in folder_qs.all()}, {f.name for f in (f1, f2, f21, f22)} ) # Test only() self.assertSetEqual( {f.name for f in folder_qs.only('name').all()}, {f.name for f in (f1, f2, f21, f22)} ) self.assertSetEqual( {f.child_folder_count for f in folder_qs.only('name').all()}, {None} ) # Test depth() self.assertSetEqual( {f.name for f in folder_qs.depth(SHALLOW).all()}, {f.name for f in (f1, f2)} ) # Test filter() self.assertSetEqual( {f.name for f in folder_qs.filter(name=f1.name)}, {f.name for f in (f1,)} ) self.assertSetEqual( {f.name for f in folder_qs.filter(name__in=[f1.name, f2.name])}, {f.name for f in (f1, f2)} ) # Test get() self.assertEqual(folder_qs.get(id=f2.id).name, f2.name) self.assertEqual(folder_qs.get(id=f2.id, changekey=f2.changekey).name, f2.name) self.assertEqual( folder_qs.get(name=f2.name).child_folder_count, 2 ) self.assertEqual( folder_qs.filter(name=f2.name).get().child_folder_count, 2 ) self.assertEqual( folder_qs.only('name').get(name=f2.name).name, f2.name ) self.assertEqual( folder_qs.only('name').get(name=f2.name).child_folder_count, None ) with self.assertRaises(DoesNotExist): folder_qs.get(name=get_random_string(16)) with self.assertRaises(MultipleObjectsReturned): folder_qs.get() finally: f0.wipe() f0.delete() def test_folder_query_set_failures(self): with self.assertRaises(ValueError): FolderQuerySet('XXX') fld_qs = SingleFolderQuerySet(account=self.account, folder=self.account.inbox) with self.assertRaises(InvalidField): fld_qs.only('XXX') with self.assertRaises(InvalidField): list(fld_qs.filter(XXX='XXX')) def test_user_configuration(self): """Test that we can do CRUD operations on user configuration data.""" # Create a test folder that we delete afterwards f = Messages(parent=self.account.inbox, name=get_random_string(16)).save() # The name must be fewer than 237 characters, can contain only the characters "A-Z", "a-z", "0-9", and ".", # and must not start with "IPM.Configuration" name = get_random_string(16, spaces=False, special=False) # Should not exist yet with self.assertRaises(ErrorItemNotFound): f.get_user_configuration(name=name) # Create a config dictionary = { get_random_bool(): get_random_str_tuple(10, 2), get_random_int(): get_random_bool(), get_random_byte(): get_random_int(), get_random_bytes(16): get_random_byte(), get_random_string(8): get_random_bytes(16), get_random_datetime(tz=self.account.default_timezone): get_random_string(8), get_random_str_tuple(4, 4): get_random_datetime(tz=self.account.default_timezone), } xml_data = b'%s' % get_random_string(16).encode('utf-8') binary_data = get_random_bytes(100) f.create_user_configuration(name=name, dictionary=dictionary, xml_data=xml_data, binary_data=binary_data) # Fetch and compare values config = f.get_user_configuration(name=name) self.assertEqual(config.dictionary, dictionary) self.assertEqual(config.xml_data, xml_data) self.assertEqual(config.binary_data, binary_data) # Cannot create one more with the same name with self.assertRaises(ErrorItemSave): f.create_user_configuration(name=name) # Does not exist on a different folder with self.assertRaises(ErrorItemNotFound): self.account.inbox.get_user_configuration(name=name) # Update the config f.update_user_configuration( name=name, dictionary={'bar': 'foo', 456: 'a', 'b': True}, xml_data=b'baz', binary_data=b'YYY' ) # Fetch again and compare values config = f.get_user_configuration(name=name) self.assertEqual(config.dictionary, {'bar': 'foo', 456: 'a', 'b': True}) self.assertEqual(config.xml_data, b'baz') self.assertEqual(config.binary_data, b'YYY') # Delete the config f.delete_user_configuration(name=name) # We already deleted this config with self.assertRaises(ErrorItemNotFound): f.get_user_configuration(name=name) f.delete() def test_permissionset_effectiverights_parsing(self): # Test static XML since server may not have any permission sets or effective rights xml = b'''\ NoError IPF.Appointment My Calendar 1 2 true true true true true true false SID1 user1@example.com User 1 false false false true false None None FullDetails Reviewer SID2 user2@example.com User 2 true false false true false All All FullDetails Editor ''' ws = GetFolder(account=self.account) ws.folders = [self.account.calendar] res = list(ws.parse(xml)) self.assertEqual(len(res), 1) fld = res[0] self.assertEqual( fld.effective_rights, EffectiveRights( create_associated=True, create_contents=True, create_hierarchy=True, delete=True, modify=True, read=True, view_private_items=False ) ) self.assertEqual( fld.permission_set, PermissionSet( permissions=None, calendar_permissions=[ CalendarPermission( can_create_items=False, can_create_subfolders=False, is_folder_owner=False, is_folder_visible=True, is_folder_contact=False, edit_items='None', delete_items='None', read_items='FullDetails', user_id=UserId( sid='SID1', primary_smtp_address='user1@example.com', display_name='User 1', distinguished_user=None, external_user_identity=None ), calendar_permission_level='Reviewer' ), CalendarPermission( can_create_items=True, can_create_subfolders=False, is_folder_owner=False, is_folder_visible=True, is_folder_contact=False, edit_items='All', delete_items='All', read_items='FullDetails', user_id=UserId( sid='SID2', primary_smtp_address='user2@example.com', display_name='User 2', distinguished_user=None, external_user_identity=None ), calendar_permission_level='Editor' ) ], unknown_entries=None ), ) exchangelib-4.6.1/tests/test_items/000077500000000000000000000000001414601472700173235ustar00rootroot00000000000000exchangelib-4.6.1/tests/test_items/__init__.py000066400000000000000000000000001414601472700214220ustar00rootroot00000000000000exchangelib-4.6.1/tests/test_items/test_basics.py000066400000000000000000001056441414601472700222120ustar00rootroot00000000000000import abc import datetime from decimal import Decimal from keyword import kwlist import time import unittest import unittest.util from dateutil.relativedelta import relativedelta from exchangelib.errors import ErrorItemNotFound, ErrorUnsupportedPathForQuery, ErrorInvalidValueForProperty, \ ErrorPropertyUpdate, ErrorInvalidPropertySet from exchangelib.extended_properties import ExternId from exchangelib.fields import TextField, BodyField, FieldPath, CultureField, IdField, ChoiceField, AttachmentField,\ BooleanField from exchangelib.indexed_properties import SingleFieldIndexedElement, MultiFieldIndexedElement from exchangelib.items import CalendarItem, Contact, Task, DistributionList, BaseItem from exchangelib.properties import Mailbox, Attendee from exchangelib.queryset import Q from exchangelib.util import value_to_xml_text from ..common import EWSTest, get_random_string, get_random_datetime_range, get_random_date, \ get_random_decimal, get_random_choice, get_random_int, get_random_datetime class BaseItemTest(EWSTest, metaclass=abc.ABCMeta): TEST_FOLDER = None FOLDER_CLASS = None ITEM_CLASS = None @classmethod def setUpClass(cls): if cls is BaseItemTest: raise unittest.SkipTest("Skip BaseItemTest, it's only for inheritance") super().setUpClass() def setUp(self): super().setUp() self.test_folder = getattr(self.account, self.TEST_FOLDER) self.assertEqual(type(self.test_folder), self.FOLDER_CLASS) self.assertEqual(self.test_folder.DISTINGUISHED_FOLDER_ID, self.TEST_FOLDER) def tearDown(self): # Delete all test items and delivery receipts self.test_folder.filter( Q(categories__contains=self.categories) | Q(subject__startswith='Delivered: Subject: ') ).delete() super().tearDown() def get_random_insert_kwargs(self): insert_kwargs = {} for f in self.ITEM_CLASS.FIELDS: if not f.supports_version(self.account.version): # Cannot be used with this EWS version continue if self.ITEM_CLASS == CalendarItem and f in CalendarItem.timezone_fields(): # Timezone fields will (and must) be populated automatically from the timestamp continue if f.is_read_only: # These cannot be created continue if f.name == 'mime_content': # This needs special formatting. See separate test_mime_content() test continue if f.name == 'attachments': # Testing attachments is heavy. Leave this to specific tests insert_kwargs[f.name] = [] continue if f.name == 'resources': # The test server doesn't have any resources insert_kwargs[f.name] = [] continue if f.name == 'optional_attendees': # 'optional_attendees' and 'required_attendees' are mutually exclusive insert_kwargs[f.name] = None continue if f.name == 'start': start = get_random_date() insert_kwargs[f.name], insert_kwargs['end'] = \ get_random_datetime_range(start_date=start, end_date=start, tz=self.account.default_timezone) insert_kwargs['recurrence'] = self.random_val(self.ITEM_CLASS.get_field_by_fieldname('recurrence')) insert_kwargs['recurrence'].boundary.start = insert_kwargs[f.name].date() continue if f.name == 'start_date': insert_kwargs[f.name] = get_random_datetime().date() insert_kwargs['due_date'] = insert_kwargs[f.name] # Don't set 'recurrence' here. It's difficult to test updates so we'll test task recurrence separately insert_kwargs['recurrence'] = None continue if f.name == 'end': continue if f.name == 'is_all_day': # For CalendarItem instances, the 'is_all_day' attribute affects the 'start' and 'end' values. Changing # from 'false' to 'true' removes the time part of these datetimes. insert_kwargs['is_all_day'] = False continue if f.name == 'recurrence': continue if f.name == 'due_date': continue if f.name == 'start_date': continue if f.name == 'status': # Start with an incomplete task status = get_random_choice(set(f.supported_choices(version=self.account.version)) - {Task.COMPLETED}) insert_kwargs[f.name] = status if status == Task.NOT_STARTED: insert_kwargs['percent_complete'] = Decimal(0) else: insert_kwargs['percent_complete'] = get_random_decimal(1, 99) continue if f.name == 'percent_complete': continue insert_kwargs[f.name] = self.random_val(f) return insert_kwargs def get_item_fields(self): return [self.ITEM_CLASS.get_field_by_fieldname('id'), self.ITEM_CLASS.get_field_by_fieldname('changekey')] \ + [f for f in self.ITEM_CLASS.FIELDS if f.name != '_id'] def get_random_update_kwargs(self, item, insert_kwargs): update_kwargs = {} for f in self.ITEM_CLASS.FIELDS: if not f.supports_version(self.account.version): # Cannot be used with this EWS version continue if self.ITEM_CLASS == CalendarItem and f in CalendarItem.timezone_fields(): # Timezone fields will (and must) be populated automatically from the timestamp continue if f.is_read_only: # These cannot be changed continue if not item.is_draft and f.is_read_only_after_send: # These cannot be changed when the item is no longer a draft continue if f.name == 'message_id' and f.is_read_only_after_send: # Cannot be updated, regardless of draft status continue if f.name == 'attachments': # Testing attachments is heavy. Leave this to specific tests update_kwargs[f.name] = [] continue if f.name == 'resources': # The test server doesn't have any resources update_kwargs[f.name] = [] continue if isinstance(f, AttachmentField): # Attachments are handled separately continue if f.name == 'start': start = get_random_date(start_date=insert_kwargs['end'].date()) update_kwargs[f.name], update_kwargs['end'] = \ get_random_datetime_range(start_date=start, end_date=start, tz=self.account.default_timezone) update_kwargs['recurrence'] = self.random_val(self.ITEM_CLASS.get_field_by_fieldname('recurrence')) update_kwargs['recurrence'].boundary.start = update_kwargs[f.name].date() continue if f.name == 'start_date': update_kwargs[f.name] = get_random_datetime().date() update_kwargs['due_date'] = update_kwargs[f.name] # Don't set 'recurrence' here. It's difficult to test updates so we'll test task recurrence separately update_kwargs['recurrence'] = None continue if f.name == 'end': continue if f.name == 'recurrence': continue if f.name == 'due_date': continue if f.name == 'start_date': continue if f.name == 'status': # Update task to a completed state update_kwargs[f.name] = Task.COMPLETED update_kwargs['percent_complete'] = Decimal(100) continue if f.name == 'percent_complete': continue if f.name == 'reminder_is_set': if self.ITEM_CLASS == Task: # Task type doesn't allow updating 'reminder_is_set' to True update_kwargs[f.name] = False else: update_kwargs[f.name] = not insert_kwargs[f.name] continue if isinstance(f, BooleanField): update_kwargs[f.name] = not insert_kwargs[f.name] continue if f.value_cls in (Mailbox, Attendee): if insert_kwargs[f.name] is None: update_kwargs[f.name] = self.random_val(f) else: update_kwargs[f.name] = None continue update_kwargs[f.name] = self.random_val(f) if self.ITEM_CLASS == CalendarItem: # EWS always sets due date to 'start' update_kwargs['reminder_due_by'] = update_kwargs['start'] if update_kwargs.get('is_all_day', False): # For is_all_day items, EWS will remove the time part of start and end values update_kwargs['start'] = update_kwargs['start'].date() update_kwargs['end'] = (update_kwargs['end'] + datetime.timedelta(days=1)).date() return update_kwargs def get_test_item(self, folder=None, categories=None): item_kwargs = self.get_random_insert_kwargs() item_kwargs['categories'] = categories or self.categories return self.ITEM_CLASS(folder=folder or self.test_folder, **item_kwargs) def get_test_folder(self, folder=None): return self.FOLDER_CLASS(parent=folder or self.test_folder, name=get_random_string(8)) def get_item_by_id(self, item): _id, changekey = item if isinstance(item, tuple) else (item.id, item.changekey) return self.account.root.get(id=_id, changekey=changekey) class CommonItemTest(BaseItemTest): @classmethod def setUpClass(cls): if cls is CommonItemTest: raise unittest.SkipTest("Skip CommonItemTest, it's only for inheritance") super().setUpClass() def test_field_names(self): # Test that fieldnames don't clash with Python keywords for f in self.ITEM_CLASS.FIELDS: self.assertNotIn(f.name, kwlist) def test_magic(self): item = self.get_test_item() self.assertIn('subject=', str(item)) self.assertIn(item.__class__.__name__, repr(item)) def test_queryset_nonsearchable_fields(self): for f in self.get_item_fields(): with self.subTest(f=f): if f.is_searchable or isinstance(f, IdField) or not f.supports_version(self.account.version): continue if f.name in ('percent_complete', 'allow_new_time_proposal'): # These fields don't raise an error when used in a filter, but also don't match anything in a filter continue try: filter_val = f.clean(self.random_val(f)) filter_kwargs = {'%s__in' % f.name: filter_val} if f.is_list else {f.name: filter_val} # We raise ValueError when searching on an is_searchable=False field with self.assertRaises(ValueError): list(self.test_folder.filter(**filter_kwargs)) # Make sure the is_searchable=False setting is correct by searching anyway and testing that this # fails server-side. This only works for values that we are actually able to convert to a search # string. try: value_to_xml_text(filter_val) except TypeError: continue f.is_searchable = True if f.name in ('reminder_due_by', 'conversation_index'): # Filtering is accepted but doesn't work self.assertEqual( self.test_folder.filter(**filter_kwargs).count(), 0 ) else: with self.assertRaises((ErrorUnsupportedPathForQuery, ErrorInvalidValueForProperty)): list(self.test_folder.filter(**filter_kwargs)) finally: f.is_searchable = False def _reduce_fields_for_filter(self, item, fields): for f in fields: if not f.supports_version(self.account.version): # Cannot be used with this EWS version continue if not f.is_searchable: # Cannot be used in a QuerySet continue if getattr(item, f.name) is None: # We cannot filter on None values continue if self.ITEM_CLASS == Contact and f.name in ('body', 'display_name'): # filtering 'body' or 'display_name' on Contact items doesn't work at all. Error in EWS? continue yield f def _run_filter_tests(self, qs, f, filter_kwargs, val): for kw in filter_kwargs: with self.subTest(f=f, kw=kw): matches = qs.filter(**kw).count() if isinstance(f, TextField) and f.is_complex: # Complex text fields sometimes fail a search using generated data. In production, # they almost always work anyway. Give it one more try after 10 seconds; it seems EWS does # some sort of indexing that needs to catch up. if not matches and isinstance(f, BodyField): # The body field is particularly nasty in this area. Give up continue for _ in range(5): if matches: break time.sleep(2) matches = qs.filter(**kw).count() if f.is_list and not val and list(kw)[0].endswith('__%s' % Q.LOOKUP_IN): # __in with an empty list returns an empty result self.assertEqual(matches, 0, (f.name, val, kw)) else: self.assertEqual(matches, 1, (f.name, val, kw)) def test_filter_on_simple_fields(self): # Test that we can filter on all simple fields item = self.get_test_item() fields = [] for f in self._reduce_fields_for_filter(item, self.get_item_fields()): if f.is_list: continue fields.append(f) if not fields: self.skipTest('No matching simple fields on this model') item.save() common_qs = self.test_folder.filter(categories__contains=self.categories) for f in fields: val = getattr(item, f.name) # Filter with =, __in and __contains. We could have more filters here, but these should always match. filter_kwargs = [{f.name: val}, {'%s__in' % f.name: [val]}] if isinstance(f, TextField) and not isinstance(f, ChoiceField): # Choice fields cannot be filtered using __contains. Sort of makes sense. random_start = get_random_int(min_val=0, max_val=len(val)//2) random_end = get_random_int(min_val=len(val)//2+1, max_val=len(val)) filter_kwargs.append({'%s__contains' % f.name: val[random_start:random_end]}) self._run_filter_tests(common_qs, f, filter_kwargs, val) def test_filter_on_list_fields(self): # Test that we can filter on all list fields item = self.get_test_item() fields = [] for f in self._reduce_fields_for_filter(item, self.get_item_fields()): if not f.is_list: continue if issubclass(f.value_cls, MultiFieldIndexedElement): continue if issubclass(f.value_cls, SingleFieldIndexedElement): continue fields.append(f) if not fields: self.skipTest('No matching list fields on this model') item.save() common_qs = self.test_folder.filter(categories__contains=self.categories) for f in fields: val = getattr(item, f.name) # Filter multi-value fields with =, __in and __contains filter_kwargs = [{'%s__in' % f.name: val}, {'%s__contains' % f.name: val}] self._run_filter_tests(common_qs, f, filter_kwargs, val) def test_filter_on_single_field_index_fields(self): # Test that we can filter on all index fields item = self.get_test_item() fields = [] for f in self._reduce_fields_for_filter(item, self.get_item_fields()): if not issubclass(f.value_cls, SingleFieldIndexedElement): continue fields.append(f) if not fields: self.skipTest('No matching single index fields on this model') item.save() common_qs = self.test_folder.filter(categories__contains=self.categories) for f in fields: val = getattr(item, f.name) # For these, we may filter by item or subfield value filter_kwargs = [] for v in val: for subfield in f.value_cls.supported_fields(version=self.account.version): field_path = FieldPath(field=f, label=v.label, subfield=subfield) path, subval = field_path.path, field_path.get_value(item) if subval is None: continue filter_kwargs.extend([ {f.name: v}, {path: subval}, {'%s__in' % path: [subval]}, {'%s__contains' % path: [subval]} ]) self._run_filter_tests(common_qs, f, filter_kwargs, val) def test_filter_on_multi_field_index_fields(self): # Test that we can filter on all index fields # TODO: Test filtering on subfields of IndexedField item = self.get_test_item() fields = [] for f in self._reduce_fields_for_filter(item, self.get_item_fields()): if not issubclass(f.value_cls, MultiFieldIndexedElement): continue fields.append(f) if not fields: self.skipTest('No matching multi index fields on this model') item.save() common_qs = self.test_folder.filter(categories__contains=self.categories) for f in fields: val = getattr(item, f.name) # Filter multi-value fields with =, __in and __contains # For these, we need to filter on the subfield filter_kwargs = [] for v in val: for subfield in f.value_cls.supported_fields(version=self.account.version): field_path = FieldPath(field=f, label=v.label, subfield=subfield) path, subval = field_path.path, field_path.get_value(item) if subval is None: continue filter_kwargs.extend([ {path: subval}, {'%s__in' % path: [subval]}, {'%s__contains' % path: [subval]} ]) self._run_filter_tests(common_qs, f, filter_kwargs, val) def test_text_field_settings(self): # Test that the max_length and is_complex field settings are correctly set for text fields item = self.get_test_item().save() for f in self.ITEM_CLASS.FIELDS: with self.subTest(f=f): if not f.supports_version(self.account.version): # Cannot be used with this EWS version continue if not isinstance(f, TextField): continue if isinstance(f, ChoiceField): # This one can't contain random values continue if isinstance(f, CultureField): # This one can't contain random values continue if f.is_read_only: continue if f.name == 'categories': # We're filtering on this one, so leave it alone continue old_max_length = getattr(f, 'max_length', None) old_is_complex = f.is_complex try: # Set a string long enough to not be handled by FindItems f.max_length = 4000 if f.is_list: setattr( item, f.name, [get_random_string(f.max_length) for _ in range(len(getattr(item, f.name)))] ) else: setattr(item, f.name, get_random_string(f.max_length)) try: item.save(update_fields=[f.name]) except ErrorPropertyUpdate: # Some fields throw this error when updated to a huge value self.assertIn(f.name, ['given_name', 'middle_name', 'surname']) continue except ErrorInvalidPropertySet: # Some fields can not be updated after save self.assertTrue(f.is_read_only_after_send) continue # is_complex=True forces the query to use GetItems which will always get the full value f.is_complex = True new_full_item = self.test_folder.all().only(f.name).get(id=item.id) new_full = getattr(new_full_item, f.name) if old_max_length: if f.is_list: for s in new_full: self.assertEqual(len(s), old_max_length, (f.name, len(s), old_max_length)) else: self.assertEqual(len(new_full), old_max_length, (f.name, len(new_full), old_max_length)) # is_complex=False forces the query to use FindItems which will only get the short value f.is_complex = False new_short_item = self.test_folder.all().only(f.name).get(categories__contains=self.categories) new_short = getattr(new_short_item, f.name) if not old_is_complex: self.assertEqual(new_short, new_full, (f.name, new_short, new_full)) finally: if old_max_length: f.max_length = old_max_length else: delattr(f, 'max_length') f.is_complex = old_is_complex def test_save_and_delete(self): # Test that we can create, update and delete single items using methods directly on the item. insert_kwargs = self.get_random_insert_kwargs() insert_kwargs['categories'] = self.categories item = self.ITEM_CLASS(folder=self.test_folder, **insert_kwargs) self.assertIsNone(item.id) self.assertIsNone(item.changekey) # Create item.save() self.assertIsNotNone(item.id) self.assertIsNotNone(item.changekey) for k, v in insert_kwargs.items(): self.assertEqual(getattr(item, k), v, (k, getattr(item, k), v)) # Test that whatever we have locally also matches whatever is in the DB fresh_item = self.get_item_by_id(item) for f in self.ITEM_CLASS.FIELDS: with self.subTest(f=f): old, new = getattr(item, f.name), getattr(fresh_item, f.name) if f.is_read_only and old is None: # Some fields are automatically set server-side continue if f.name == 'reminder_due_by': # EWS sets a default value if it is not set on insert. Ignore continue if f.name == 'mime_content': # This will change depending on other contents fields continue if f.is_list: old, new = set(old or ()), set(new or ()) self.assertEqual(old, new, (f.name, old, new)) # Update update_kwargs = self.get_random_update_kwargs(item=item, insert_kwargs=insert_kwargs) for k, v in update_kwargs.items(): setattr(item, k, v) item.save() for k, v in update_kwargs.items(): self.assertEqual(getattr(item, k), v, (k, getattr(item, k), v)) # Test that whatever we have locally also matches whatever is in the DB fresh_item = self.get_item_by_id(item) for f in self.ITEM_CLASS.FIELDS: with self.subTest(f=f): old, new = getattr(item, f.name), getattr(fresh_item, f.name) if f.is_read_only and old is None: # Some fields are automatically updated server-side continue if f.name == 'mime_content': # This will change depending on other contents fields continue if f.name == 'reminder_due_by': if new is None: # EWS does not always return a value if reminder_is_set is False. continue if old is not None: # EWS sometimes randomly sets the new reminder due date to one month before or after we # wanted it, and sometimes 30 days before or after. But only sometimes... old_date = old.astimezone(self.account.default_timezone).date() new_date = new.astimezone(self.account.default_timezone).date() if getattr(item, 'is_all_day', False) and old_date == new_date: # There is some weirdness with the time part of the reminder_due_by value for all-day events item.reminder_due_by = new continue if relativedelta(month=1) + new_date == old_date: item.reminder_due_by = new continue if relativedelta(month=1) + old_date == new_date: item.reminder_due_by = new continue if abs(old_date - new_date) == datetime.timedelta(days=30): item.reminder_due_by = new continue if f.is_list: old, new = set(old or ()), set(new or ()) self.assertEqual(old, new, (f.name, old, new)) # Hard delete item_id = (item.id, item.changekey) item.delete() for e in self.account.fetch(ids=[item_id]): # It's gone from the account self.assertIsInstance(e, ErrorItemNotFound) # Really gone, not just changed ItemId items = self.test_folder.filter(categories__contains=item.categories) self.assertEqual(items.count(), 0) def test_item(self): # Test insert insert_kwargs = self.get_random_insert_kwargs() insert_kwargs['categories'] = self.categories item = self.ITEM_CLASS(folder=self.test_folder, **insert_kwargs) # Test with generator as argument insert_ids = self.test_folder.bulk_create(items=(i for i in [item])) self.assertEqual(len(insert_ids), 1) self.assertIsInstance(insert_ids[0], BaseItem) find_ids = list(self.test_folder.filter(categories__contains=item.categories).values_list('id', 'changekey')) self.assertEqual(len(find_ids), 1) self.assertEqual(len(find_ids[0]), 2, find_ids[0]) self.assertEqual(insert_ids, find_ids) # Test with generator as argument item = list(self.account.fetch(ids=(i for i in find_ids)))[0] for f in self.ITEM_CLASS.FIELDS: with self.subTest(f=f): if not f.supports_version(self.account.version): # Cannot be used with this EWS version continue if self.ITEM_CLASS == CalendarItem and f in CalendarItem.timezone_fields(): # Timezone fields will (and must) be populated automatically from the timestamp continue if f.is_read_only: continue if f.name == 'reminder_due_by': # EWS sets a default value if it is not set on insert. Ignore continue if f.name == 'mime_content': # This will change depending on other contents fields continue old, new = getattr(item, f.name), insert_kwargs[f.name] if f.is_list: old, new = set(old or ()), set(new or ()) self.assertEqual(old, new, (f.name, old, new)) # Test update update_kwargs = self.get_random_update_kwargs(item=item, insert_kwargs=insert_kwargs) if self.ITEM_CLASS in (Contact, DistributionList): # Contact and DistributionList don't support mime_type updates at all update_kwargs.pop('mime_content', None) update_fieldnames = [f for f in update_kwargs if f != 'attachments'] for k, v in update_kwargs.items(): setattr(item, k, v) # Test with generator as argument update_ids = self.account.bulk_update(items=(i for i in [(item, update_fieldnames)])) self.assertEqual(len(update_ids), 1) self.assertEqual(len(update_ids[0]), 2, update_ids) self.assertEqual(insert_ids[0].id, update_ids[0][0]) # ID should be the same self.assertNotEqual(insert_ids[0].changekey, update_ids[0][1]) # Changekey should change when item is updated item = self.get_item_by_id(update_ids[0]) for f in self.ITEM_CLASS.FIELDS: with self.subTest(f=f): if not f.supports_version(self.account.version): # Cannot be used with this EWS version continue if self.ITEM_CLASS == CalendarItem and f in CalendarItem.timezone_fields(): # Timezone fields will (and must) be populated automatically from the timestamp continue if f.is_read_only or f.is_read_only_after_send: # These cannot be changed continue if f.name == 'mime_content': # This will change depending on other contents fields continue old, new = getattr(item, f.name), update_kwargs[f.name] if f.name == 'reminder_due_by': if old is None: # EWS does not always return a value if reminder_is_set is False. Set one now item.reminder_due_by = new continue if new is not None: # EWS sometimes randomly sets the new reminder due date to one month before or after we # wanted it, and sometimes 30 days before or after. But only sometimes... old_date = old.astimezone(self.account.default_timezone).date() new_date = new.astimezone(self.account.default_timezone).date() if getattr(item, 'is_all_day', False) and old_date == new_date: # There is some weirdness with the time part of the reminder_due_by value for all-day events item.reminder_due_by = new continue if relativedelta(month=1) + new_date == old_date: item.reminder_due_by = new continue if relativedelta(month=1) + old_date == new_date: item.reminder_due_by = new continue if abs(old_date - new_date) == datetime.timedelta(days=30): item.reminder_due_by = new continue if f.is_list: old, new = set(old or ()), set(new or ()) self.assertEqual(old, new, (f.name, old, new)) # Test wiping or removing fields wipe_kwargs = {} for f in self.ITEM_CLASS.FIELDS: if not f.supports_version(self.account.version): # Cannot be used with this EWS version continue if self.ITEM_CLASS == CalendarItem and f in CalendarItem.timezone_fields(): # Timezone fields will (and must) be populated automatically from the timestamp continue if f.is_required or f.is_required_after_save: # These cannot be deleted continue if f.is_read_only or f.is_read_only_after_send: # These cannot be changed continue wipe_kwargs[f.name] = None for k, v in wipe_kwargs.items(): setattr(item, k, v) wipe_ids = self.account.bulk_update([(item, update_fieldnames)]) self.assertEqual(len(wipe_ids), 1) self.assertEqual(len(wipe_ids[0]), 2, wipe_ids) self.assertEqual(insert_ids[0].id, wipe_ids[0][0]) # ID should be the same self.assertNotEqual(insert_ids[0].changekey, wipe_ids[0][1]) # Changekey should not be the same when item is updated item = self.get_item_by_id(wipe_ids[0]) for f in self.ITEM_CLASS.FIELDS: with self.subTest(f=f): if not f.supports_version(self.account.version): # Cannot be used with this EWS version continue if self.ITEM_CLASS == CalendarItem and f in CalendarItem.timezone_fields(): # Timezone fields will (and must) be populated automatically from the timestamp continue if f.is_required or f.is_required_after_save: continue if f.is_read_only or f.is_read_only_after_send: continue old, new = getattr(item, f.name), wipe_kwargs[f.name] if f.is_list: old, new = set(old or ()), set(new or ()) self.assertEqual(old, new, (f.name, old, new)) try: self.ITEM_CLASS.register('extern_id', ExternId) # Test extern_id = None, which deletes the extended property entirely extern_id = None item.extern_id = extern_id wipe2_ids = self.account.bulk_update([(item, ['extern_id'])]) self.assertEqual(len(wipe2_ids), 1) self.assertEqual(len(wipe2_ids[0]), 2, wipe2_ids) self.assertEqual(insert_ids[0].id, wipe2_ids[0][0]) # ID must be the same self.assertNotEqual(insert_ids[0].changekey, wipe2_ids[0][1]) # Changekey must change when item is updated item = self.get_item_by_id(wipe2_ids[0]) self.assertEqual(item.extern_id, extern_id) finally: self.ITEM_CLASS.deregister('extern_id') exchangelib-4.6.1/tests/test_items/test_bulk.py000066400000000000000000000220241414601472700216710ustar00rootroot00000000000000import datetime from exchangelib.errors import ErrorItemNotFound, ErrorInvalidChangeKey, ErrorInvalidIdMalformed from exchangelib.fields import FieldPath from exchangelib.folders import Inbox, Folder, Calendar from exchangelib.items import Item, Message, SAVE_ONLY, SEND_ONLY, SEND_AND_SAVE_COPY, CalendarItem, BulkCreateResult from exchangelib.services import CreateItem from .test_basics import BaseItemTest class BulkMethodTest(BaseItemTest): TEST_FOLDER = 'inbox' FOLDER_CLASS = Inbox ITEM_CLASS = Message def test_fetch(self): item = self.get_test_item() self.test_folder.bulk_create(items=[item, item]) ids = self.test_folder.filter(categories__contains=item.categories) items = list(self.account.fetch(ids=ids)) for item in items: self.assertIsInstance(item, self.ITEM_CLASS) self.assertEqual(len(items), 2) items = list(self.account.fetch(ids=ids, only_fields=['subject'])) self.assertEqual(len(items), 2) items = list(self.account.fetch(ids=ids, only_fields=[FieldPath.from_string('subject', self.test_folder)])) self.assertEqual(len(items), 2) items = list(self.account.fetch(ids=ids, only_fields=['id', 'changekey'])) self.assertEqual(len(items), 2) def test_bulk_create(self): item = self.get_test_item() res = self.test_folder.bulk_create(items=[item, item]) self.assertEqual(len(res), 2) for r in res: self.assertIsInstance(r, BulkCreateResult) def test_no_account(self): # Test bulk operations on items with no self.account item = self.get_test_item() item.account = None res = self.test_folder.bulk_create(items=[item])[0] item.id, item.changekey = res.id, res.changekey item.account = None self.assertEqual(list(self.account.fetch(ids=[item]))[0].id, item.id) item.account = None res = self.account.bulk_update(items=[(item, ('subject',))])[0] item.id, item.changekey = res item.account = None res = self.account.bulk_copy(ids=[item], to_folder=self.account.trash)[0] item.id, item.changekey = res item.account = None res = self.account.bulk_move(ids=[item], to_folder=self.test_folder)[0] item.id, item.changekey = res item.account = None self.assertEqual(self.account.bulk_delete(ids=[item]), [True]) item = self.get_test_item().save() item.account = None self.assertEqual(self.account.bulk_send(ids=[item]), [True]) def test_empty_args(self): # We allow empty sequences for these methods self.assertEqual(self.test_folder.bulk_create(items=[]), []) self.assertEqual(list(self.account.fetch(ids=[])), []) self.assertEqual(self.account.bulk_create(folder=self.test_folder, items=[]), []) self.assertEqual(self.account.bulk_update(items=[]), []) self.assertEqual(self.account.bulk_delete(ids=[]), []) self.assertEqual(self.account.bulk_send(ids=[]), []) self.assertEqual(self.account.bulk_copy(ids=[], to_folder=self.account.trash), []) self.assertEqual(self.account.bulk_move(ids=[], to_folder=self.account.trash), []) self.assertEqual(self.account.upload(data=[]), []) self.assertEqual(self.account.export(items=[]), []) def test_qs_args(self): # We allow querysets for these methods qs = self.test_folder.none() self.assertEqual(list(self.account.fetch(ids=qs)), []) with self.assertRaises(ValueError): # bulk_create() does not allow queryset input self.account.bulk_create(folder=self.test_folder, items=qs) with self.assertRaises(ValueError): # bulk_update() does not allow queryset input self.account.bulk_update(items=qs) self.assertEqual(self.account.bulk_delete(ids=qs), []) self.assertEqual(self.account.bulk_send(ids=qs), []) self.assertEqual(self.account.bulk_copy(ids=qs, to_folder=self.account.trash), []) self.assertEqual(self.account.bulk_move(ids=qs, to_folder=self.account.trash), []) self.assertEqual(self.account.upload(data=qs), []) self.assertEqual(self.account.export(items=qs), []) def test_no_kwargs(self): self.assertEqual(self.test_folder.bulk_create([]), []) self.assertEqual(list(self.account.fetch([])), []) self.assertEqual(self.account.bulk_create(self.test_folder, []), []) self.assertEqual(self.account.bulk_update([]), []) self.assertEqual(self.account.bulk_delete([]), []) self.assertEqual(self.account.bulk_send([]), []) self.assertEqual(self.account.bulk_copy([], to_folder=self.account.trash), []) self.assertEqual(self.account.bulk_move([], to_folder=self.account.trash), []) self.assertEqual(self.account.upload([]), []) self.assertEqual(self.account.export([]), []) def test_invalid_bulk_args(self): # Test bulk_create with self.assertRaises(ValueError): # Folder must belong to account self.account.bulk_create(folder=Folder(root=None), items=[1]) with self.assertRaises(AttributeError): # Must have folder on save self.account.bulk_create(folder=None, items=[1], message_disposition=SAVE_ONLY) # Test that we can send_and_save with a default folder self.account.bulk_create(folder=None, items=[], message_disposition=SEND_AND_SAVE_COPY) with self.assertRaises(AttributeError): # Must not have folder on send-only self.account.bulk_create(folder=self.test_folder, items=[1], message_disposition=SEND_ONLY) # Test bulk_update with self.assertRaises(ValueError): # Cannot update in send-only mode self.account.bulk_update(items=[1], message_disposition=SEND_ONLY) def test_bulk_failure(self): # Test that bulk_* can handle EWS errors and return the errors in order without losing non-failure results items1 = [self.get_test_item().save() for _ in range(3)] items1[1].changekey = 'XXX' for i, res in enumerate(self.account.bulk_delete(items1)): if i == 1: self.assertIsInstance(res, ErrorInvalidChangeKey) else: self.assertEqual(res, True) items2 = [self.get_test_item().save() for _ in range(3)] items2[1].id = 'AAAA==' for i, res in enumerate(self.account.bulk_delete(items2)): if i == 1: self.assertIsInstance(res, ErrorInvalidIdMalformed) else: self.assertEqual(res, True) items3 = [self.get_test_item().save() for _ in range(3)] items3[1].id = items1[0].id for i, res in enumerate(self.account.fetch(items3)): if i == 1: self.assertIsInstance(res, ErrorItemNotFound) else: self.assertIsInstance(res, Item) def test_bulk_create_with_no_result(self): # Some CreateItem responses do not contain the ID of the created items. See issue#984 xml = b'''\ NoError NoError ''' ws = CreateItem(account=self.account) self.assertListEqual( list(ws.parse(xml)), [True, True] ) class CalendarBulkMethodTest(BaseItemTest): TEST_FOLDER = 'calendar' FOLDER_CLASS = Calendar ITEM_CLASS = CalendarItem def test_no_account(self): # Test corner cases with bulk operations on items with no self.account item = self.get_test_item() item.recurrence = None item.is_all_day = True item.start, item.end = datetime.date(2020, 1, 1), datetime.date(2020, 1, 2) item.account = None res = self.test_folder.bulk_create(items=[item])[0] item.id, item.changekey = res.id, res.changekey item.account = None self.account.bulk_update(items=[(item, ('start',))]) exchangelib-4.6.1/tests/test_items/test_calendaritems.py000066400000000000000000000545741414601472700235660ustar00rootroot00000000000000import datetime from exchangelib.errors import ErrorInvalidOperation, ErrorItemNotFound from exchangelib.ewsdatetime import UTC from exchangelib.folders import Calendar from exchangelib.items import CalendarItem, BulkCreateResult from exchangelib.items.calendar_item import SINGLE, OCCURRENCE, EXCEPTION, RECURRING_MASTER from exchangelib.recurrence import Recurrence, DailyPattern, Occurrence, FirstOccurrence, LastOccurrence, \ DeletedOccurrence from ..common import get_random_string, get_random_datetime_range, get_random_date from .test_basics import CommonItemTest class CalendarTest(CommonItemTest): TEST_FOLDER = 'calendar' FOLDER_CLASS = Calendar ITEM_CLASS = CalendarItem def match_cat(self, i): if isinstance(i, Exception): return False return set(i.categories or []) == set(self.categories) def test_cancel(self): item = self.get_test_item().save() res = item.cancel() # Returns (id, changekey) of cancelled item self.assertIsInstance(res, BulkCreateResult) with self.assertRaises(ErrorItemNotFound): # Item is already cancelled item.cancel() def test_updating_timestamps(self): # Test that we can update an item without changing anything, and maintain the hidden timezone fields as local # timezones, and that returned timestamps are in UTC. item = self.get_test_item() item.reminder_is_set = True item.is_all_day = False item.recurrence = None item.save() item.refresh() self.assertEqual(item.type, SINGLE) for i in self.account.calendar.filter(categories__contains=self.categories).only('start', 'end', 'categories'): self.assertEqual(i.start, item.start) self.assertEqual(i.start.tzinfo, UTC) self.assertEqual(i.end, item.end) self.assertEqual(i.end.tzinfo, UTC) self.assertEqual(i._start_timezone, self.account.default_timezone) self.assertEqual(i._end_timezone, self.account.default_timezone) i.save(update_fields=['start', 'end']) self.assertEqual(i.start, item.start) self.assertEqual(i.start.tzinfo, UTC) self.assertEqual(i.end, item.end) self.assertEqual(i.end.tzinfo, UTC) self.assertEqual(i._start_timezone, self.account.default_timezone) self.assertEqual(i._end_timezone, self.account.default_timezone) for i in self.account.calendar.filter(categories__contains=self.categories).only('start', 'end', 'categories'): self.assertEqual(i.start, item.start) self.assertEqual(i.start.tzinfo, UTC) self.assertEqual(i.end, item.end) self.assertEqual(i.end.tzinfo, UTC) self.assertEqual(i._start_timezone, self.account.default_timezone) self.assertEqual(i._end_timezone, self.account.default_timezone) i.delete() def test_update_to_non_utc_datetime(self): # Test updating with non-UTC datetime values. This is a separate code path in UpdateItem code item = self.get_test_item() item.reminder_is_set = True item.is_all_day = False item.save() # Update start, end and recurrence with timezoned datetimes. For some reason, EWS throws # 'ErrorOccurrenceTimeSpanTooBig' is we go back in time. start = get_random_date(start_date=item.start.date() + datetime.timedelta(days=1)) dt_start, dt_end = [ dt.astimezone(self.account.default_timezone) for dt in get_random_datetime_range(start_date=start, end_date=start, tz=self.account.default_timezone) ] item.start, item.end = dt_start, dt_end item.recurrence.boundary.start = dt_start.date() item.save() item.refresh() self.assertEqual(item.start, dt_start) self.assertEqual(item.end, dt_end) def test_all_day_datetimes(self): # Test that we can use plain dates for start and end values for all-day items start = get_random_date() start_dt, end_dt = get_random_datetime_range( start_date=start, end_date=start + datetime.timedelta(days=365), tz=self.account.default_timezone ) # Assign datetimes for start and end item = self.ITEM_CLASS(folder=self.test_folder, start=start_dt, end=end_dt, is_all_day=True, categories=self.categories).save() # Returned item start and end values should be EWSDate instances item = self.test_folder.all().only('is_all_day', 'start', 'end').get(id=item.id, changekey=item.changekey) self.assertEqual(item.is_all_day, True) self.assertEqual(item.start, start_dt.date()) self.assertEqual(item.end, end_dt.date()) item.save() # Make sure we can update item.delete() # We are also allowed to assign plain dates as values for all-day items item = self.ITEM_CLASS(folder=self.test_folder, start=start_dt.date(), end=end_dt.date(), is_all_day=True, categories=self.categories).save() # Returned item start and end values should be EWSDate instances item = self.test_folder.all().only('is_all_day', 'start', 'end').get(id=item.id, changekey=item.changekey) self.assertEqual(item.is_all_day, True) self.assertEqual(item.start, start_dt.date()) self.assertEqual(item.end, end_dt.date()) item.save() # Make sure we can update def test_view(self): item1 = self.ITEM_CLASS( account=self.account, folder=self.test_folder, subject=get_random_string(16), start=datetime.datetime(2016, 1, 1, 8, tzinfo=self.account.default_timezone), end=datetime.datetime(2016, 1, 1, 10, tzinfo=self.account.default_timezone), categories=self.categories, ) item2 = self.ITEM_CLASS( account=self.account, folder=self.test_folder, subject=get_random_string(16), start=datetime.datetime(2016, 2, 1, 8, tzinfo=self.account.default_timezone), end=datetime.datetime(2016, 2, 1, 10, tzinfo=self.account.default_timezone), categories=self.categories, ) self.test_folder.bulk_create(items=[item1, item2]) qs = self.test_folder.view(start=item1.start, end=item2.end) # Test missing args with self.assertRaises(TypeError): self.test_folder.view() # Test bad args with self.assertRaises(ValueError): list(self.test_folder.view(start=item1.end, end=item1.start)) with self.assertRaises(TypeError): list(self.test_folder.view(start='xxx', end=item1.end)) with self.assertRaises(ValueError): list(self.test_folder.view(start=item1.start, end=item1.end, max_items=0)) # Test dates self.assertEqual( len([i for i in self.test_folder.view(start=item1.start, end=item1.end) if self.match_cat(i)]), 1 ) self.assertEqual( len([i for i in self.test_folder.view(start=item1.start, end=item2.end) if self.match_cat(i)]), 2 ) # Edge cases. Get view from end of item1 to start of item2. Should logically return 0 items, but Exchange wants # it differently and returns item1 even though there is no overlap. self.assertEqual( len([i for i in self.test_folder.view(start=item1.end, end=item2.start) if self.match_cat(i)]), 1 ) self.assertEqual( len([i for i in self.test_folder.view(start=item1.start, end=item2.start) if self.match_cat(i)]), 1 ) # Test max_items self.assertEqual( len([ i for i in self.test_folder.view(start=item1.start, end=item2.end, max_items=9999) if self.match_cat(i) ]), 2 ) self.assertEqual( self.test_folder.view(start=item1.start, end=item2.end, max_items=1).count(), 1 ) # Test client-side ordering self.assertListEqual( [i.subject for i in qs.order_by('subject') if self.match_cat(i)], sorted([item1.subject, item2.subject]) ) # Test client-side ordering on a field with no default value and no default value_cls value self.assertListEqual( [i.start for i in qs.order_by('-start') if self.match_cat(i)], [item2.start, item1.start] ) # Test client-side ordering on multiple fields. Intentionally sort first on a field where values are equal, # to see that we can sort on the 2nd field. self.assertListEqual( [i.start for i in qs.order_by('categories', '-start') if self.match_cat(i)], [item2.start, item1.start] ) # Test chaining self.assertTrue(qs.count() >= 2) with self.assertRaises(ErrorInvalidOperation): qs.filter(subject=item1.subject).count() # EWS does not allow restrictions self.assertListEqual( [i for i in qs.order_by('subject').values('subject') if i['subject'] in (item1.subject, item2.subject)], [{'subject': s} for s in sorted([item1.subject, item2.subject])] ) def test_client_side_ordering_on_mixed_all_day_and_normal(self): # Test that client-side ordering on start and end fields works for items that are a mix of normal an all-day # items. This requires us to compare datetime.datetime -> EWSDate values which is not allowed by default # (EWSDate -> datetime.datetime *is* allowed). start = datetime.datetime(2016, 1, 1, 8, tzinfo=self.account.default_timezone) end = datetime.datetime(2016, 1, 1, 10, tzinfo=self.account.default_timezone) all_day_date = (start - datetime.timedelta(days=1)).date() item1 = self.ITEM_CLASS( account=self.account, folder=self.test_folder, subject=get_random_string(16), start=all_day_date, end=all_day_date, is_all_day=True, categories=self.categories, ) item2 = self.ITEM_CLASS( account=self.account, folder=self.test_folder, subject=get_random_string(16), start=start, end=end, categories=self.categories, ) self.test_folder.bulk_create(items=[item1, item2]) list(self.test_folder.view(start=start - datetime.timedelta(days=1), end=end).order_by('start')) list(self.test_folder.view(start=start - datetime.timedelta(days=1), end=end).order_by('-start')) def test_recurring_item(self): # Create a recurring calendar item. Test that occurrence fields are correct on the master item # Create a master item with 4 daily occurrences from 8:00 to 10:00. 'start' and 'end' are values for the first # occurrence. start = datetime.datetime(2016, 1, 1, 8, tzinfo=self.account.default_timezone) end = datetime.datetime(2016, 1, 1, 10, tzinfo=self.account.default_timezone) master_item = self.ITEM_CLASS( folder=self.test_folder, start=start, end=end, recurrence=Recurrence(pattern=DailyPattern(interval=1), start=start.date(), number=4), categories=self.categories, ).save() master_item.refresh() self.assertEqual(master_item.is_recurring, False) self.assertEqual(master_item.type, RECURRING_MASTER) self.assertIsInstance(master_item.first_occurrence, FirstOccurrence) self.assertEqual(master_item.first_occurrence.start, start) self.assertEqual(master_item.first_occurrence.end, end) self.assertIsInstance(master_item.last_occurrence, LastOccurrence) self.assertEqual(master_item.last_occurrence.start, start + datetime.timedelta(days=3)) self.assertEqual(master_item.last_occurrence.end, end + datetime.timedelta(days=3)) self.assertEqual(master_item.modified_occurrences, None) self.assertEqual(master_item.deleted_occurrences, None) # Test occurrences as full calendar items, unfolded from the master range_start, range_end = start, end + datetime.timedelta(days=3) unfolded = [i for i in self.test_folder.view(start=range_start, end=range_end) if self.match_cat(i)] self.assertEqual(len(unfolded), 4) for item in unfolded: self.assertEqual(item.type, OCCURRENCE) self.assertEqual(item.is_recurring, True) first_occurrence = unfolded[0] self.assertEqual(first_occurrence.id, master_item.first_occurrence.id) self.assertEqual(first_occurrence.start, master_item.first_occurrence.start) self.assertEqual(first_occurrence.end, master_item.first_occurrence.end) second_occurrence = unfolded[1] self.assertEqual(second_occurrence.start, master_item.start + datetime.timedelta(days=1)) self.assertEqual(second_occurrence.end, master_item.end + datetime.timedelta(days=1)) third_occurrence = unfolded[2] self.assertEqual(third_occurrence.start, master_item.start + datetime.timedelta(days=2)) self.assertEqual(third_occurrence.end, master_item.end + datetime.timedelta(days=2)) last_occurrence = unfolded[3] self.assertEqual(last_occurrence.id, master_item.last_occurrence.id) self.assertEqual(last_occurrence.start, master_item.last_occurrence.start) self.assertEqual(last_occurrence.end, master_item.last_occurrence.end) def test_change_occurrence(self): # Test that we can make changes to individual occurrences and see the effect on the master item. start = datetime.datetime(2016, 1, 1, 8, tzinfo=self.account.default_timezone) end = datetime.datetime(2016, 1, 1, 10, tzinfo=self.account.default_timezone) master_item = self.ITEM_CLASS( folder=self.test_folder, start=start, end=end, recurrence=Recurrence(pattern=DailyPattern(interval=1), start=start.date(), number=4), categories=self.categories, ).save() master_item.refresh() # Test occurrences as full calendar items, unfolded from the master range_start, range_end = start, end + datetime.timedelta(days=3) unfolded = [i for i in self.test_folder.view(start=range_start, end=range_end) if self.match_cat(i)] # Change the start and end of the second occurrence second_occurrence = unfolded[1] second_occurrence.start += datetime.timedelta(hours=1) second_occurrence.end += datetime.timedelta(hours=1) second_occurrence.save() # Test change on the master item master_item.refresh() self.assertEqual(len(master_item.modified_occurrences), 1) modified_occurrence = master_item.modified_occurrences[0] self.assertIsInstance(modified_occurrence, Occurrence) self.assertEqual(modified_occurrence.id, second_occurrence.id) self.assertEqual(modified_occurrence.start, second_occurrence.start) self.assertEqual(modified_occurrence.end, second_occurrence.end) self.assertEqual(modified_occurrence.original_start, second_occurrence.start - datetime.timedelta(hours=1)) self.assertEqual(master_item.deleted_occurrences, None) # Test change on the unfolded item unfolded = [i for i in self.test_folder.view(start=range_start, end=range_end) if self.match_cat(i)] self.assertEqual(len(unfolded), 4) self.assertEqual(unfolded[1].type, EXCEPTION) self.assertEqual(unfolded[1].start, second_occurrence.start) self.assertEqual(unfolded[1].end, second_occurrence.end) self.assertEqual(unfolded[1].original_start, second_occurrence.start - datetime.timedelta(hours=1)) def test_delete_occurrence(self): # Test that we can delete an occurrence and see the cange on the master item start = datetime.datetime(2016, 1, 1, 8, tzinfo=self.account.default_timezone) end = datetime.datetime(2016, 1, 1, 10, tzinfo=self.account.default_timezone) master_item = self.ITEM_CLASS( folder=self.test_folder, start=start, end=end, recurrence=Recurrence(pattern=DailyPattern(interval=1), start=start.date(), number=4), categories=self.categories, ).save() master_item.refresh() # Test occurrences as full calendar items, unfolded from the master range_start, range_end = start, end + datetime.timedelta(days=3) unfolded = [i for i in self.test_folder.view(start=range_start, end=range_end) if self.match_cat(i)] # Delete the third occurrence third_occurrence = unfolded[2] third_occurrence.delete() # Test change on the master item master_item.refresh() self.assertEqual(master_item.modified_occurrences, None) self.assertEqual(len(master_item.deleted_occurrences), 1) deleted_occurrence = master_item.deleted_occurrences[0] self.assertIsInstance(deleted_occurrence, DeletedOccurrence) self.assertEqual(deleted_occurrence.start, third_occurrence.start) # Test change on the unfolded items unfolded = [i for i in self.test_folder.view(start=range_start, end=range_end) if self.match_cat(i)] self.assertEqual(len(unfolded), 3) def test_change_occurrence_via_index(self): # Test updating occurrences via occurrence index without knowing the ID of the occurrence. start = datetime.datetime(2016, 1, 1, 8, tzinfo=self.account.default_timezone) end = datetime.datetime(2016, 1, 1, 10, tzinfo=self.account.default_timezone) master_item = self.ITEM_CLASS( folder=self.test_folder, start=start, end=end, subject=get_random_string(16), recurrence=Recurrence(pattern=DailyPattern(interval=1), start=start.date(), number=4), categories=self.categories, ).save() # Change the start and end of the second occurrence second_occurrence = master_item.occurrence(index=2) second_occurrence.start = start + datetime.timedelta(days=1, hours=1) second_occurrence.end = end + datetime.timedelta(days=1, hours=1) second_occurrence.save(update_fields=['start', 'end']) # Test that UpdateItem works with only a few fields second_occurrence = master_item.occurrence(index=2) second_occurrence.refresh() self.assertEqual(second_occurrence.subject, master_item.subject) second_occurrence.start += datetime.timedelta(hours=1) second_occurrence.end += datetime.timedelta(hours=1) second_occurrence.save(update_fields=['start', 'end']) # Test that UpdateItem works after refresh # Test change on the master item master_item.refresh() self.assertEqual(len(master_item.modified_occurrences), 1) modified_occurrence = master_item.modified_occurrences[0] self.assertIsInstance(modified_occurrence, Occurrence) self.assertEqual(modified_occurrence.id, second_occurrence.id) self.assertEqual(modified_occurrence.start, second_occurrence.start) self.assertEqual(modified_occurrence.end, second_occurrence.end) self.assertEqual(modified_occurrence.original_start, second_occurrence.start - datetime.timedelta(hours=2)) self.assertEqual(master_item.deleted_occurrences, None) def test_delete_occurrence_via_index(self): # Test deleting occurrences via occurrence index without knowing the ID of the occurrence. start = datetime.datetime(2016, 1, 1, 8, tzinfo=self.account.default_timezone) end = datetime.datetime(2016, 1, 1, 10, tzinfo=self.account.default_timezone) master_item = self.ITEM_CLASS( folder=self.test_folder, start=start, end=end, subject=get_random_string(16), recurrence=Recurrence(pattern=DailyPattern(interval=1), start=start.date(), number=4), categories=self.categories, ).save() # Delete the third occurrence third_occurrence = master_item.occurrence(index=3) third_occurrence.refresh() # Test that GetItem works third_occurrence = master_item.occurrence(index=3) third_occurrence.delete() # Test that DeleteItem works # Test change on the master item master_item.refresh() self.assertEqual(master_item.modified_occurrences, None) self.assertEqual(len(master_item.deleted_occurrences), 1) deleted_occurrence = master_item.deleted_occurrences[0] self.assertIsInstance(deleted_occurrence, DeletedOccurrence) self.assertEqual(deleted_occurrence.start, start + datetime.timedelta(days=2)) def test_get_master_recurrence(self): # Test getting the master recurrence via an occurrence start = datetime.datetime(2016, 1, 1, 8, tzinfo=self.account.default_timezone) end = datetime.datetime(2016, 1, 1, 10, tzinfo=self.account.default_timezone) recurrence = Recurrence(pattern=DailyPattern(interval=1), start=start.date(), number=4) master_item = self.ITEM_CLASS( folder=self.test_folder, start=start, end=end, subject=get_random_string(16), recurrence=recurrence, categories=self.categories, ).save() # Get the master from an occurrence range_start, range_end = start, end + datetime.timedelta(days=3) unfolded = [i for i in self.test_folder.view(start=range_start, end=range_end) if self.match_cat(i)] third_occurrence = unfolded[2] self.assertEqual(third_occurrence.recurrence, None) master_from_occurrence = third_occurrence.recurring_master() master_from_occurrence.refresh() # Test that GetItem works self.assertEqual(master_from_occurrence.recurrence, recurrence) self.assertEqual(master_from_occurrence.subject, master_item.subject) master_from_occurrence = third_occurrence.recurring_master() master_from_occurrence.subject = get_random_string(16) master_from_occurrence.save(update_fields=['subject']) # Test that UpdateItem works master_from_occurrence.delete() # Test that DeleteItem works with self.assertRaises(ErrorItemNotFound): master_item.delete() # Item is gone from the server, so this should fail with self.assertRaises(ErrorItemNotFound): third_occurrence.delete() # Item is gone from the server, so this should fail exchangelib-4.6.1/tests/test_items/test_contacts.py000066400000000000000000000235501414601472700225570ustar00rootroot00000000000000import datetime try: import zoneinfo except ImportError: from backports import zoneinfo from exchangelib.errors import ErrorInvalidIdMalformed from exchangelib.folders import Contacts from exchangelib.indexed_properties import EmailAddress, PhysicalAddress, PhoneNumber from exchangelib.items import Contact, DistributionList, Persona from exchangelib.properties import Mailbox, Member, Attribution, SourceId, FolderId, StringAttributedValue, \ PhoneNumberAttributedValue, PersonaPhoneNumberTypeValue from exchangelib.services import GetPersona from ..common import get_random_string, get_random_email from .test_basics import CommonItemTest class ContactsTest(CommonItemTest): TEST_FOLDER = 'contacts' FOLDER_CLASS = Contacts ITEM_CLASS = Contact def test_order_by_on_indexed_field(self): # Test order_by() on IndexedField (simple and multi-subfield). Only Contact items have these test_items = [] label = self.random_val(EmailAddress.get_field_by_fieldname('label')) for i in range(4): item = self.get_test_item() item.email_addresses = [EmailAddress(email='%s@foo.com' % i, label=label)] test_items.append(item) self.test_folder.bulk_create(items=test_items) qs = self.test_folder.filter(categories__contains=self.categories) self.assertEqual( [i[0].email for i in qs.order_by('email_addresses__%s' % label) .values_list('email_addresses', flat=True)], ['0@foo.com', '1@foo.com', '2@foo.com', '3@foo.com'] ) self.assertEqual( [i[0].email for i in qs.order_by('-email_addresses__%s' % label) .values_list('email_addresses', flat=True)], ['3@foo.com', '2@foo.com', '1@foo.com', '0@foo.com'] ) self.bulk_delete(qs) test_items = [] label = self.random_val(PhysicalAddress.get_field_by_fieldname('label')) for i in range(4): item = self.get_test_item() item.physical_addresses = [PhysicalAddress(street='Elm St %s' % i, label=label)] test_items.append(item) self.test_folder.bulk_create(items=test_items) qs = self.test_folder.filter(categories__contains=self.categories) self.assertEqual( [i[0].street for i in qs.order_by('physical_addresses__%s__street' % label) .values_list('physical_addresses', flat=True)], ['Elm St 0', 'Elm St 1', 'Elm St 2', 'Elm St 3'] ) self.assertEqual( [i[0].street for i in qs.order_by('-physical_addresses__%s__street' % label) .values_list('physical_addresses', flat=True)], ['Elm St 3', 'Elm St 2', 'Elm St 1', 'Elm St 0'] ) self.bulk_delete(qs) def test_order_by_failure(self): # Test error handling on indexed properties with labels and subfields qs = self.test_folder.filter(categories__contains=self.categories) with self.assertRaises(ValueError): qs.order_by('email_addresses') # Must have label with self.assertRaises(ValueError): qs.order_by('email_addresses__FOO') # Must have a valid label with self.assertRaises(ValueError): qs.order_by('email_addresses__EmailAddress1__FOO') # Must not have a subfield with self.assertRaises(ValueError): qs.order_by('physical_addresses__Business') # Must have a subfield with self.assertRaises(ValueError): qs.order_by('physical_addresses__Business__FOO') # Must have a valid subfield def test_update_on_single_field_indexed_field(self): home = PhoneNumber(label='HomePhone', phone_number='123') business = PhoneNumber(label='BusinessPhone', phone_number='456') item = self.get_test_item() item.phone_numbers = [home] item.save() item.phone_numbers = [business] item.save(update_fields=['phone_numbers']) item.refresh() self.assertListEqual(item.phone_numbers, [business]) def test_update_on_multi_field_indexed_field(self): home = PhysicalAddress(label='Home', street='ABC') business = PhysicalAddress(label='Business', street='DEF', city='GHI') item = self.get_test_item() item.physical_addresses = [home] item.save() item.physical_addresses = [business] item.save(update_fields=['physical_addresses']) item.refresh() self.assertListEqual(item.physical_addresses, [business]) def test_distribution_lists(self): dl = DistributionList(folder=self.test_folder, display_name=get_random_string(255), categories=self.categories) dl.save() new_dl = self.test_folder.get(categories__contains=dl.categories) self.assertEqual(new_dl.display_name, dl.display_name) self.assertEqual(new_dl.members, None) dl.refresh() # We set mailbox_type to OneOff because otherwise the email address must be an actual account dl.members = { Member(mailbox=Mailbox(email_address=get_random_email(), mailbox_type='OneOff')) for _ in range(4) } dl.save() new_dl = self.test_folder.get(categories__contains=dl.categories) self.assertEqual({m.mailbox.email_address for m in new_dl.members}, dl.members) dl.delete() def test_find_people(self): # The test server may not have any contacts. Just test that the FindPeople and GetPersona services work. self.assertGreaterEqual(len(list(self.test_folder.people())), 0) self.assertGreaterEqual( len(list( self.test_folder.people().only('display_name').filter(display_name='john').order_by('display_name') )), 0 ) def test_get_persona(self): xml = b'''\ NoError Person 2012-06-01T17:00:34Z Brian Johnson 4255550110 0 Outlook true false false Brian Johnson 2 3 (425)555-0110 Mobile 0 (425)555-0111 Mobile 1 ''' ws = GetPersona(account=self.account) persona = ws.parse(xml) self.assertEqual(persona.id, 'AAQkADEzAQAKtOtR=') self.assertEqual(persona.persona_type, 'Person') self.assertEqual( persona.creation_time, datetime.datetime(2012, 6, 1, 17, 0, 34, tzinfo=zoneinfo.ZoneInfo('UTC')) ) self.assertEqual(persona.display_name, 'Brian Johnson') self.assertEqual(persona.relevance_score, '4255550110') self.assertEqual(persona.attributions[0], Attribution( ID=None, _id=SourceId(id='AAMkA =', changekey='EQAAABY+'), display_name='Outlook', is_writable=True, is_quick_contact=False, is_hidden=False, folder_id=FolderId(id='AAMkA=', changekey='AQAAAA==') )) self.assertEqual(persona.display_names, [ StringAttributedValue(value='Brian Johnson', attributions=['2', '3']), ]) self.assertEqual(persona.mobile_phones, [ PhoneNumberAttributedValue( value=PersonaPhoneNumberTypeValue(number='(425)555-0110', type='Mobile'), attributions=['0'], ), PhoneNumberAttributedValue( value=PersonaPhoneNumberTypeValue(number='(425)555-0111', type='Mobile'), attributions=['1'], ) ]) def test_get_persona_failure(self): # The test server may not have any personas. Just test that the service response with something we can parse persona = Persona(id='AAA=', changekey='xxx') try: GetPersona(account=self.account).call(persona=persona) except ErrorInvalidIdMalformed: pass exchangelib-4.6.1/tests/test_items/test_generic.py000066400000000000000000001212661414601472700223600ustar00rootroot00000000000000import datetime try: import zoneinfo except ImportError: from backports import zoneinfo from exchangelib.attachments import ItemAttachment from exchangelib.errors import ErrorItemNotFound, ErrorInternalServerError from exchangelib.extended_properties import ExtendedProperty, ExternId from exchangelib.fields import ExtendedPropertyField, CharField from exchangelib.folders import Inbox, FolderCollection from exchangelib.items import CalendarItem, Message from exchangelib.queryset import QuerySet from exchangelib.restriction import Restriction, Q from exchangelib.version import Build, EXCHANGE_2007, EXCHANGE_2013 from ..common import get_random_string, mock_version from .test_basics import CommonItemTest class GenericItemTest(CommonItemTest): """Tests that don't need to be run for every single folder type""" TEST_FOLDER = 'inbox' FOLDER_CLASS = Inbox ITEM_CLASS = Message def test_validation(self): item = self.get_test_item() item.clean() for f in self.ITEM_CLASS.FIELDS: with self.subTest(f=f): # Test field max_length if isinstance(f, CharField) and f.max_length: with self.assertRaises(ValueError): setattr(item, f.name, 'a' * (f.max_length + 1)) item.clean() setattr(item, f.name, 'a') def test_invalid_direct_args(self): item = self.get_test_item() item.account = None with self.assertRaises(ValueError): item.save() # Must have account on save item = self.get_test_item() item.id = 'XXX' # Fake a saved item item.account = None with self.assertRaises(ValueError): item.save() # Must have account on update item = self.get_test_item() with self.assertRaises(ValueError): item.save(update_fields=['foo', 'bar']) # update_fields is only valid on update item = self.get_test_item() item.account = None with self.assertRaises(ValueError): item.refresh() # Must have account on refresh item = self.get_test_item() with self.assertRaises(ValueError): item.refresh() # Refresh an item that has not been saved item = self.get_test_item() item.save() item_id, changekey = item.id, item.changekey item.delete() item.id, item.changekey = item_id, changekey with self.assertRaises(ErrorItemNotFound): item.refresh() # Refresh an item that doesn't exist item = self.get_test_item() item.account = None with self.assertRaises(ValueError): item.copy(to_folder=self.test_folder) # Must have an account on copy item = self.get_test_item() with self.assertRaises(ValueError): item.copy(to_folder=self.test_folder) # Must be an existing item item = self.get_test_item() item.save() item_id, changekey = item.id, item.changekey item.delete() item.id, item.changekey = item_id, changekey with self.assertRaises(ErrorItemNotFound): item.copy(to_folder=self.test_folder) # Item disappeared item = self.get_test_item() item.account = None with self.assertRaises(ValueError): item.move(to_folder=self.test_folder) # Must have an account on move item = self.get_test_item() with self.assertRaises(ValueError): item.move(to_folder=self.test_folder) # Must be an existing item item = self.get_test_item() item.save() item_id, changekey = item.id, item.changekey item.delete() item.id, item.changekey = item_id, changekey with self.assertRaises(ErrorItemNotFound): item.move(to_folder=self.test_folder) # Item disappeared item = self.get_test_item() item.account = None with self.assertRaises(ValueError): item.delete() # Must have an account item = self.get_test_item() with self.assertRaises(ValueError): item.delete() # Must be an existing item item = self.get_test_item() item.save() item_id, changekey = item.id, item.changekey item.delete() item.id, item.changekey = item_id, changekey with self.assertRaises(ErrorItemNotFound): item.delete() # Item disappeared item = self.get_test_item() with self.assertRaises(ValueError): item._delete(delete_type='FOO', send_meeting_cancellations=None, affected_task_occurrences=None, suppress_read_receipts=None) with self.assertRaises(ValueError): item.delete(send_meeting_cancellations='XXX') with self.assertRaises(ValueError): item.delete(affected_task_occurrences='XXX') with self.assertRaises(ValueError): item.delete(suppress_read_receipts='XXX') def test_invalid_kwargs_on_send(self): # Only Message class has the send() method item = self.get_test_item() item.account = None with self.assertRaises(ValueError): item.send() # Must have account on send item = self.get_test_item() item.save() item_id, changekey = item.id, item.changekey item.delete() item.id, item.changekey = item_id, changekey with self.assertRaises(ErrorItemNotFound): item.send() # Item disappeared item = self.get_test_item() with self.assertRaises(AttributeError): item.send(copy_to_folder=self.account.trash, save_copy=False) # Inconsistent args def test_unsupported_fields(self): # Create a field that is not supported by any current versions. Test that we fail when using this field class UnsupportedProp(ExtendedProperty): property_set_id = 'deadcafe-beef-beef-beef-deadcafebeef' property_name = 'Unsupported Property' property_type = 'String' attr_name = 'unsupported_property' self.ITEM_CLASS.register(attr_name=attr_name, attr_cls=UnsupportedProp) try: for f in self.ITEM_CLASS.FIELDS: if f.name == attr_name: f.supported_from = Build(99, 99, 99, 99) with self.assertRaises(ValueError): self.test_folder.get(**{attr_name: 'XXX'}) with self.assertRaises(ValueError): list(self.test_folder.filter(**{attr_name: 'XXX'})) with self.assertRaises(ValueError): list(self.test_folder.all().only(attr_name)) with self.assertRaises(ValueError): list(self.test_folder.all().values(attr_name)) with self.assertRaises(ValueError): list(self.test_folder.all().values_list(attr_name)) finally: self.ITEM_CLASS.deregister(attr_name=attr_name) def test_order_by(self): # Test order_by() on normal field test_items = [] for i in range(4): item = self.get_test_item() item.subject = 'Subj %s' % i test_items.append(item) self.test_folder.bulk_create(items=test_items) qs = QuerySet( folder_collection=FolderCollection(account=self.account, folders=[self.test_folder]) ).filter(categories__contains=self.categories) self.assertEqual( list(qs.order_by('subject').values_list('subject', flat=True)), ['Subj 0', 'Subj 1', 'Subj 2', 'Subj 3'] ) self.assertEqual( list(qs.order_by('-subject').values_list('subject', flat=True)), ['Subj 3', 'Subj 2', 'Subj 1', 'Subj 0'] ) self.bulk_delete(qs) try: self.ITEM_CLASS.register('extern_id', ExternId) # Test order_by() on ExtendedProperty test_items = [] for i in range(4): item = self.get_test_item() item.extern_id = 'ID %s' % i test_items.append(item) self.test_folder.bulk_create(items=test_items) qs = QuerySet( folder_collection=FolderCollection(account=self.account, folders=[self.test_folder]) ).filter(categories__contains=self.categories) self.assertEqual( list(qs.order_by('extern_id').values_list('extern_id', flat=True)), ['ID 0', 'ID 1', 'ID 2', 'ID 3'] ) self.assertEqual( list(qs.order_by('-extern_id').values_list('extern_id', flat=True)), ['ID 3', 'ID 2', 'ID 1', 'ID 0'] ) finally: self.ITEM_CLASS.deregister('extern_id') self.bulk_delete(qs) # Test sorting on multiple fields try: self.ITEM_CLASS.register('extern_id', ExternId) test_items = [] for i in range(2): for j in range(2): item = self.get_test_item() item.subject = 'Subj %s' % i item.extern_id = 'ID %s' % j test_items.append(item) self.test_folder.bulk_create(items=test_items) qs = QuerySet( folder_collection=FolderCollection(account=self.account, folders=[self.test_folder]) ).filter(categories__contains=self.categories) self.assertEqual( list(qs.order_by('subject', 'extern_id').values('subject', 'extern_id')), [{'subject': 'Subj 0', 'extern_id': 'ID 0'}, {'subject': 'Subj 0', 'extern_id': 'ID 1'}, {'subject': 'Subj 1', 'extern_id': 'ID 0'}, {'subject': 'Subj 1', 'extern_id': 'ID 1'}] ) self.assertEqual( list(qs.order_by('-subject', 'extern_id').values('subject', 'extern_id')), [{'subject': 'Subj 1', 'extern_id': 'ID 0'}, {'subject': 'Subj 1', 'extern_id': 'ID 1'}, {'subject': 'Subj 0', 'extern_id': 'ID 0'}, {'subject': 'Subj 0', 'extern_id': 'ID 1'}] ) self.assertEqual( list(qs.order_by('subject', '-extern_id').values('subject', 'extern_id')), [{'subject': 'Subj 0', 'extern_id': 'ID 1'}, {'subject': 'Subj 0', 'extern_id': 'ID 0'}, {'subject': 'Subj 1', 'extern_id': 'ID 1'}, {'subject': 'Subj 1', 'extern_id': 'ID 0'}] ) self.assertEqual( list(qs.order_by('-subject', '-extern_id').values('subject', 'extern_id')), [{'subject': 'Subj 1', 'extern_id': 'ID 1'}, {'subject': 'Subj 1', 'extern_id': 'ID 0'}, {'subject': 'Subj 0', 'extern_id': 'ID 1'}, {'subject': 'Subj 0', 'extern_id': 'ID 0'}] ) finally: self.ITEM_CLASS.deregister('extern_id') def test_order_by_with_empty_values(self): # Test order_by() when some values are empty test_items = [] for i in range(4): item = self.get_test_item() if i % 2 == 0: item.subject = 'Subj %s' % i else: item.subject = None test_items.append(item) self.test_folder.bulk_create(items=test_items) qs = QuerySet( folder_collection=FolderCollection(account=self.account, folders=[self.test_folder]) ).filter(categories__contains=self.categories) self.assertEqual( list(qs.order_by('subject').values_list('subject', flat=True)), [None, None, 'Subj 0', 'Subj 2'] ) self.assertEqual( list(qs.order_by('-subject').values_list('subject', flat=True)), ['Subj 2', 'Subj 0', None, None] ) def test_order_by_on_list_field(self): # Test order_by() on list fields where some values are empty test_items = [] for i in range(4): item = self.get_test_item() item.subject = self.categories[0] # Make sure we have something unique to filter on if i % 2 == 0: item.categories = ['Cat %s' % i] else: item.categories = [] test_items.append(item) self.test_folder.bulk_create(items=test_items) qs = QuerySet( folder_collection=FolderCollection(account=self.account, folders=[self.test_folder]) ).filter(subject=self.categories[0]) self.assertEqual( list(qs.order_by('categories').values_list('categories', flat=True)), [None, None, ['Cat 0'], ['Cat 2']] ) self.assertEqual( list(qs.order_by('-categories').values_list('categories', flat=True)), [['Cat 2'], ['Cat 0'], None, None] ) def test_finditems(self): now = datetime.datetime.now(tz=zoneinfo.ZoneInfo('UTC')) # Test argument types item = self.get_test_item() ids = self.test_folder.bulk_create(items=[item]) # No arguments. There may be leftover items in the folder, so just make sure there's at least one. self.assertGreaterEqual( self.test_folder.filter().count(), 1 ) # Q object self.assertEqual( self.test_folder.filter(Q(subject=item.subject)).count(), 1 ) # Multiple Q objects self.assertEqual( self.test_folder.filter(Q(subject=item.subject), ~Q(subject=item.subject[:-3] + 'XXX')).count(), 1 ) # Multiple Q object and kwargs self.assertEqual( self.test_folder.filter(Q(subject=item.subject), categories__contains=item.categories).count(), 1 ) self.bulk_delete(ids) # Test categories which are handled specially - only '__contains' and '__in' lookups are supported item = self.get_test_item(categories=['TestA', 'TestB']) ids = self.test_folder.bulk_create(items=[item]) common_qs = self.test_folder.filter(subject=item.subject) # Guard against other simultaneous runs self.assertEqual( common_qs.filter(categories__contains='ci6xahH1').count(), # Plain string 0 ) self.assertEqual( common_qs.filter(categories__contains=['ci6xahH1']).count(), # Same, but as list 0 ) self.assertEqual( common_qs.filter(categories__contains=['TestA', 'TestC']).count(), # One wrong category 0 ) self.assertEqual( common_qs.filter(categories__contains=['TESTA']).count(), # Test case insensitivity 1 ) self.assertEqual( common_qs.filter(categories__contains=['testa']).count(), # Test case insensitivity 1 ) self.assertEqual( common_qs.filter(categories__contains=['TestA']).count(), # Partial 1 ) self.assertEqual( common_qs.filter(categories__contains=item.categories).count(), # Exact match 1 ) with self.assertRaises(ValueError): common_qs.filter(categories__in='ci6xahH1').count() # Plain string is not supported self.assertEqual( common_qs.filter(categories__in=['ci6xahH1']).count(), # Same, but as list 0 ) self.assertEqual( common_qs.filter(categories__in=['TestA', 'TestC']).count(), # One wrong category 1 ) self.assertEqual( common_qs.filter(categories__in=['TestA']).count(), # Partial 1 ) self.assertEqual( common_qs.filter(categories__in=item.categories).count(), # Exact match 1 ) self.bulk_delete(ids) common_qs = self.test_folder.filter(categories__contains=self.categories) one_hour = datetime.timedelta(hours=1) two_hours = datetime.timedelta(hours=2) # Test 'exists' ids = self.test_folder.bulk_create(items=[self.get_test_item()]) self.assertEqual( common_qs.filter(datetime_created__exists=True).count(), 1 ) self.assertEqual( common_qs.filter(datetime_created__exists=False).count(), 0 ) self.bulk_delete(ids) # Test 'range' ids = self.test_folder.bulk_create(items=[self.get_test_item()]) self.assertEqual( common_qs.filter(datetime_created__range=(now + one_hour, now + two_hours)).count(), 0 ) self.assertEqual( common_qs.filter(datetime_created__range=(now - one_hour, now + one_hour)).count(), 1 ) self.bulk_delete(ids) # Test '>' ids = self.test_folder.bulk_create(items=[self.get_test_item()]) self.assertEqual( common_qs.filter(datetime_created__gt=now + one_hour).count(), 0 ) self.assertEqual( common_qs.filter(datetime_created__gt=now - one_hour).count(), 1 ) self.bulk_delete(ids) # Test '>=' ids = self.test_folder.bulk_create(items=[self.get_test_item()]) self.assertEqual( common_qs.filter(datetime_created__gte=now + one_hour).count(), 0 ) self.assertEqual( common_qs.filter(datetime_created__gte=now - one_hour).count(), 1 ) self.bulk_delete(ids) # Test '<' ids = self.test_folder.bulk_create(items=[self.get_test_item()]) self.assertEqual( common_qs.filter(datetime_created__lt=now - one_hour).count(), 0 ) self.assertEqual( common_qs.filter(datetime_created__lt=now + one_hour).count(), 1 ) self.bulk_delete(ids) # Test '<=' ids = self.test_folder.bulk_create(items=[self.get_test_item()]) self.assertEqual( common_qs.filter(datetime_created__lte=now - one_hour).count(), 0 ) self.assertEqual( common_qs.filter(datetime_created__lte=now + one_hour).count(), 1 ) self.bulk_delete(ids) # Test '=' item = self.get_test_item() ids = self.test_folder.bulk_create(items=[item]) self.assertEqual( common_qs.filter(subject=item.subject[:-3] + 'XXX').count(), 0 ) self.assertEqual( common_qs.filter(subject=item.subject).count(), 1 ) self.bulk_delete(ids) # Test '!=' item = self.get_test_item() ids = self.test_folder.bulk_create(items=[item]) self.assertEqual( common_qs.filter(subject__not=item.subject).count(), 0 ) self.assertEqual( common_qs.filter(subject__not=item.subject[:-3] + 'XXX').count(), 1 ) self.bulk_delete(ids) # Test 'exact' item = self.get_test_item() item.subject = 'aA' + item.subject[2:] ids = self.test_folder.bulk_create(items=[item]) self.assertEqual( common_qs.filter(subject__exact=item.subject[:-3] + 'XXX').count(), 0 ) self.assertEqual( common_qs.filter(subject__exact=item.subject.lower()).count(), 0 ) self.assertEqual( common_qs.filter(subject__exact=item.subject.upper()).count(), 0 ) self.assertEqual( common_qs.filter(subject__exact=item.subject).count(), 1 ) self.bulk_delete(ids) # Test 'iexact' item = self.get_test_item() item.subject = 'aA' + item.subject[2:] ids = self.test_folder.bulk_create(items=[item]) self.assertEqual( common_qs.filter(subject__iexact=item.subject[:-3] + 'XXX').count(), 0 ) self.assertIn( common_qs.filter(subject__iexact=item.subject.lower()).count(), (0, 1) # iexact search is broken on some EWS versions ) self.assertIn( common_qs.filter(subject__iexact=item.subject.upper()).count(), (0, 1) # iexact search is broken on some EWS versions ) self.assertEqual( common_qs.filter(subject__iexact=item.subject).count(), 1 ) self.bulk_delete(ids) # Test 'contains' item = self.get_test_item() item.subject = item.subject[2:8] + 'aA' + item.subject[8:] ids = self.test_folder.bulk_create(items=[item]) self.assertEqual( common_qs.filter(subject__contains=item.subject[2:14] + 'XXX').count(), 0 ) self.assertEqual( common_qs.filter(subject__contains=item.subject[2:14].lower()).count(), 0 ) self.assertEqual( common_qs.filter(subject__contains=item.subject[2:14].upper()).count(), 0 ) self.assertEqual( common_qs.filter(subject__contains=item.subject[2:14]).count(), 1 ) self.bulk_delete(ids) # Test 'icontains' item = self.get_test_item() item.subject = item.subject[2:8] + 'aA' + item.subject[8:] ids = self.test_folder.bulk_create(items=[item]) self.assertEqual( common_qs.filter(subject__icontains=item.subject[2:14] + 'XXX').count(), 0 ) self.assertIn( common_qs.filter(subject__icontains=item.subject[2:14].lower()).count(), (0, 1) # icontains search is broken on some EWS versions ) self.assertIn( common_qs.filter(subject__icontains=item.subject[2:14].upper()).count(), (0, 1) # icontains search is broken on some EWS versions ) self.assertEqual( common_qs.filter(subject__icontains=item.subject[2:14]).count(), 1 ) self.bulk_delete(ids) # Test 'startswith' item = self.get_test_item() item.subject = 'aA' + item.subject[2:] ids = self.test_folder.bulk_create(items=[item]) self.assertEqual( common_qs.filter(subject__startswith='XXX' + item.subject[:12]).count(), 0 ) self.assertEqual( common_qs.filter(subject__startswith=item.subject[:12].lower()).count(), 0 ) self.assertEqual( common_qs.filter(subject__startswith=item.subject[:12].upper()).count(), 0 ) self.assertEqual( common_qs.filter(subject__startswith=item.subject[:12]).count(), 1 ) self.bulk_delete(ids) # Test 'istartswith' item = self.get_test_item() item.subject = 'aA' + item.subject[2:] ids = self.test_folder.bulk_create(items=[item]) self.assertEqual( common_qs.filter(subject__istartswith='XXX' + item.subject[:12]).count(), 0 ) self.assertIn( common_qs.filter(subject__istartswith=item.subject[:12].lower()).count(), (0, 1) # istartswith search is broken on some EWS versions ) self.assertIn( common_qs.filter(subject__istartswith=item.subject[:12].upper()).count(), (0, 1) # istartswith search is broken on some EWS versions ) self.assertEqual( common_qs.filter(subject__istartswith=item.subject[:12]).count(), 1 ) self.bulk_delete(ids) def test_filter_with_querystring(self): # QueryString is only supported from Exchange 2010 with self.assertRaises(NotImplementedError): Q('Subject:XXX').to_xml(self.test_folder, version=mock_version(build=EXCHANGE_2007), applies_to=Restriction.ITEMS) # We don't allow QueryString in combination with other restrictions with self.assertRaises(ValueError): self.test_folder.filter('Subject:XXX', foo='bar') with self.assertRaises(ValueError): self.test_folder.filter('Subject:XXX').filter(foo='bar') with self.assertRaises(ValueError): self.test_folder.filter(foo='bar').filter('Subject:XXX') item = self.get_test_item() item.subject = get_random_string(length=8, spaces=False, special=False) item.save() # For some reason, the querystring search doesn't work instantly. We may have to wait for up to 60 seconds. # I'm too impatient for that, so also allow empty results. This makes the test almost worthless but I blame EWS. # Also, some servers are misconfigured and don't support querystrings at all. Don't fail on that. try: self.assertIn( self.test_folder.filter('Subject:%s' % item.subject).count(), (0, 1) ) except ErrorInternalServerError as e: self.assertIn('AQS parser has been removed from Windows 2016 Server Core', e.args[0]) def test_complex_fields(self): # Test that complex fields can be fetched using only(). This is a test for #141. item = self.get_test_item().save() for f in self.ITEM_CLASS.FIELDS: with self.subTest(f=f): if not f.supports_version(self.account.version): # Cannot be used with this EWS version continue if f.name in ('optional_attendees', 'required_attendees', 'resources'): continue if f.is_read_only: continue if f.name == 'reminder_due_by': # EWS sets a default value if it is not set on insert. Ignore continue if f.name == 'mime_content': # This will change depending on other contents fields continue old = getattr(item, f.name) # Test field as single element in only() fresh_item = self.test_folder.all().only(f.name).get(categories__contains=item.categories) new = getattr(fresh_item, f.name) if f.is_list: old, new = set(old or ()), set(new or ()) self.assertEqual(old, new, (f.name, old, new)) # Test field as one of the elements in only() fresh_item = self.test_folder.all().only('subject', f.name).get(categories__contains=item.categories) new = getattr(fresh_item, f.name) if f.is_list: old, new = set(old or ()), set(new or ()) self.assertEqual(old, new, (f.name, old, new)) def test_text_body(self): if self.account.version.build < EXCHANGE_2013: self.skipTest('Exchange version too old') item = self.get_test_item() item.body = 'X' * 500 # Make body longer than the normal 256 char text field limit item.save() fresh_item = self.test_folder.filter(categories__contains=item.categories).only('text_body')[0] self.assertEqual(fresh_item.text_body, item.body) def test_only_fields(self): item = self.get_test_item().save() item = self.test_folder.get(categories__contains=item.categories) self.assertIsInstance(item, self.ITEM_CLASS) for f in self.ITEM_CLASS.FIELDS: with self.subTest(f=f): self.assertTrue(hasattr(item, f.name)) if not f.supports_version(self.account.version): # Cannot be used with this EWS version continue if f.name in ('optional_attendees', 'required_attendees', 'resources'): continue if f.name == 'reminder_due_by' and not item.reminder_is_set: # We delete the due date if reminder is not set continue if f.is_read_only: continue self.assertIsNotNone(getattr(item, f.name), (f, getattr(item, f.name))) only_fields = ('subject', 'body', 'categories') item = self.test_folder.all().only(*only_fields).get(categories__contains=item.categories) self.assertIsInstance(item, self.ITEM_CLASS) for f in self.ITEM_CLASS.FIELDS: with self.subTest(f=f): self.assertTrue(hasattr(item, f.name)) if not f.supports_version(self.account.version): # Cannot be used with this EWS version continue if f.name in only_fields: self.assertIsNotNone(getattr(item, f.name), (f.name, getattr(item, f.name))) elif f.is_required: v = getattr(item, f.name) if f.name == 'attachments': self.assertEqual(v, [], (f.name, v)) elif f.default is None: self.assertIsNone(v, (f.name, v)) else: self.assertEqual(v, f.default, (f.name, v)) def test_export_and_upload(self): # 15 new items which we will attempt to export and re-upload items = [self.get_test_item().save() for _ in range(15)] ids = [(i.id, i.changekey) for i in items] # re-fetch items because there will be some extra fields added by the server items = list(self.account.fetch(items)) # Try exporting and making sure we get the right response export_results = self.account.export(items) self.assertEqual(len(items), len(export_results)) for result in export_results: self.assertIsInstance(result, str) # Try re-uploading our results (insert) insert_results = self.account.upload([(self.test_folder, data) for data in export_results]) self.assertEqual(len(items), len(insert_results), (items, insert_results)) for result in insert_results: # Must be a completely new ItemId self.assertIsInstance(result, tuple) self.assertNotIn(result, ids) # Check the items uploaded are the same as the original items def to_dict(item): dict_item = {} # fieldnames is everything except the ID so we'll use it to compare for f in self.ITEM_CLASS.FIELDS: # datetime_created and last_modified_time aren't copied, but instead are added to the new item after # uploading. This means mime_content and size can also change. Items also get new IDs on upload. And # meeting_count values are dependent on contents of current calendar. Form query strings contain the # item ID and will also change. if f.name in {'_id', 'first_occurrence', 'last_occurrence', 'datetime_created', 'last_modified_time', 'mime_content', 'size', 'conversation_id', 'adjacent_meeting_count', 'conflicting_meeting_count', 'web_client_read_form_query_string', 'web_client_edit_form_query_string'}: continue dict_item[f.name] = getattr(item, f.name) if f.name == 'attachments': # Attachments get new IDs on upload. Wipe them here so we can compare the other fields for a in dict_item[f.name]: a.attachment_id = None return dict_item inserted_items = list(self.account.fetch(insert_results)) uploaded_data = sorted([to_dict(item) for item in inserted_items], key=lambda i: i['subject']) original_data = sorted([to_dict(item) for item in items], key=lambda i: i['subject']) self.assertListEqual(original_data, uploaded_data) # Test update instead of insert update_results = self.account.upload([ (self.test_folder, ((i.id, i.changekey), i.is_associated, data)) for i, data in zip(inserted_items, export_results) ]) self.assertEqual(len(export_results), len(update_results), (export_results, update_results)) for i, result in zip(inserted_items, update_results): # Must be a completely new ItemId self.assertIsInstance(result, tuple) item_id, changekey = result self.assertEqual(i.id, item_id) self.assertNotEqual(i.changekey, changekey) def test_export_with_error(self): # 15 new items which we will attempt to export and re-upload items = [self.get_test_item().save() for _ in range(15)] # Use id tuples for export here because deleting an item clears it's # id. ids = [(item.id, item.changekey) for item in items] # Delete one of the items, this will cause an error items[3].delete() export_results = self.account.export(ids) self.assertEqual(len(items), len(export_results)) for idx, result in enumerate(export_results): if idx == 3: # If it is the one returning the error self.assertIsInstance(result, ErrorItemNotFound) else: self.assertIsInstance(result, str) # Clean up after yourself del ids[3] # Sending the deleted one through will cause an error def test_archive(self): item = self.get_test_item(folder=self.test_folder).save() item_id, changekey = item.archive(to_folder=self.account.trash) self.account.root.get(id=item_id, changekey=changekey) def test_item_attachments(self): item = self.get_test_item(folder=self.test_folder) item.attachments = [] attached_item1 = self.get_test_item(folder=self.test_folder) attached_item1.attachments = [] attached_item1.save() attachment1 = ItemAttachment(name='attachment1', item=attached_item1) item.attach(attachment1) self.assertEqual(len(item.attachments), 1) item.save() fresh_item = self.get_item_by_id(item) self.assertEqual(len(fresh_item.attachments), 1) fresh_attachments = sorted(fresh_item.attachments, key=lambda a: a.name) self.assertEqual(fresh_attachments[0].name, 'attachment1') self.assertIsInstance(fresh_attachments[0].item, self.ITEM_CLASS) for f in self.ITEM_CLASS.FIELDS: with self.subTest(f=f): # Normalize some values we don't control if f.is_read_only: continue if self.ITEM_CLASS == CalendarItem and f in CalendarItem.timezone_fields(): # Timezone fields will (and must) be populated automatically from the timestamp continue if isinstance(f, ExtendedPropertyField): # Attachments don't have these values. It may be possible to request it if we can find the FieldURI continue if f.name == 'is_read': # This is always true for item attachments? continue if f.name == 'reminder_due_by': # EWS sets a default value if it is not set on insert. Ignore continue if f.name == 'mime_content': # This will change depending on other contents fields continue old_val = getattr(attached_item1, f.name) new_val = getattr(fresh_attachments[0].item, f.name) if f.is_list: old_val, new_val = set(old_val or ()), set(new_val or ()) self.assertEqual(old_val, new_val, (f.name, old_val, new_val)) # Test attach on saved object attached_item2 = self.get_test_item(folder=self.test_folder) attached_item2.attachments = [] attached_item2.save() attachment2 = ItemAttachment(name='attachment2', item=attached_item2) item.attach(attachment2) self.assertEqual(len(item.attachments), 2) fresh_item = self.get_item_by_id(item) self.assertEqual(len(fresh_item.attachments), 2) fresh_attachments = sorted(fresh_item.attachments, key=lambda a: a.name) self.assertEqual(fresh_attachments[0].name, 'attachment1') self.assertIsInstance(fresh_attachments[0].item, self.ITEM_CLASS) for f in self.ITEM_CLASS.FIELDS: with self.subTest(f=f): # Normalize some values we don't control if f.is_read_only: continue if self.ITEM_CLASS == CalendarItem and f in CalendarItem.timezone_fields(): # Timezone fields will (and must) be populated automatically from the timestamp continue if isinstance(f, ExtendedPropertyField): # Attachments don't have these values. It may be possible to request it if we can find the FieldURI continue if f.name == 'reminder_due_by': # EWS sets a default value if it is not set on insert. Ignore continue if f.name == 'is_read': # This is always true for item attachments? continue if f.name == 'mime_content': # This will change depending on other contents fields continue old_val = getattr(attached_item1, f.name) new_val = getattr(fresh_attachments[0].item, f.name) if f.is_list: old_val, new_val = set(old_val or ()), set(new_val or ()) self.assertEqual(old_val, new_val, (f.name, old_val, new_val)) self.assertEqual(fresh_attachments[1].name, 'attachment2') self.assertIsInstance(fresh_attachments[1].item, self.ITEM_CLASS) for f in self.ITEM_CLASS.FIELDS: # Normalize some values we don't control if f.is_read_only: continue if self.ITEM_CLASS == CalendarItem and f in CalendarItem.timezone_fields(): # Timezone fields will (and must) be populated automatically from the timestamp continue if isinstance(f, ExtendedPropertyField): # Attachments don't have these values. It may be possible to request it if we can find the FieldURI continue if f.name == 'reminder_due_by': # EWS sets a default value if it is not set on insert. Ignore continue if f.name == 'is_read': # This is always true for item attachments? continue if f.name == 'mime_content': # This will change depending on other contents fields continue old_val = getattr(attached_item2, f.name) new_val = getattr(fresh_attachments[1].item, f.name) if f.is_list: old_val, new_val = set(old_val or ()), set(new_val or ()) self.assertEqual(old_val, new_val, (f.name, old_val, new_val)) # Test detach item.detach(attachment2) self.assertTrue(attachment2.attachment_id is None) self.assertTrue(attachment2.parent_item is None) fresh_item = self.get_item_by_id(item) self.assertEqual(len(fresh_item.attachments), 1) fresh_attachments = sorted(fresh_item.attachments, key=lambda a: a.name) for f in self.ITEM_CLASS.FIELDS: with self.subTest(f=f): # Normalize some values we don't control if f.is_read_only: continue if self.ITEM_CLASS == CalendarItem and f in CalendarItem.timezone_fields(): # Timezone fields will (and must) be populated automatically from the timestamp continue if isinstance(f, ExtendedPropertyField): # Attachments don't have these values. It may be possible to request it if we can find the FieldURI continue if f.name == 'reminder_due_by': # EWS sets a default value if it is not set on insert. Ignore continue if f.name == 'is_read': # This is always true for item attachments? continue if f.name == 'mime_content': # This will change depending on other contents fields continue old_val = getattr(attached_item1, f.name) new_val = getattr(fresh_attachments[0].item, f.name) if f.is_list: old_val, new_val = set(old_val or ()), set(new_val or ()) self.assertEqual(old_val, new_val, (f.name, old_val, new_val)) # Test attach with non-saved item attached_item3 = self.get_test_item(folder=self.test_folder) attached_item3.attachments = [] attachment3 = ItemAttachment(name='attachment2', item=attached_item3) item.attach(attachment3) item.detach(attachment3) exchangelib-4.6.1/tests/test_items/test_helpers.py000066400000000000000000000124371414601472700224050ustar00rootroot00000000000000from exchangelib.errors import ErrorItemNotFound from exchangelib.folders import Inbox from exchangelib.items import Message from .test_basics import BaseItemTest class ItemHelperTest(BaseItemTest): TEST_FOLDER = 'inbox' FOLDER_CLASS = Inbox ITEM_CLASS = Message def test_save_with_update_fields(self): item = self.get_test_item() with self.assertRaises(ValueError): item.save(update_fields=['subject']) # update_fields does not work on item creation item.save() item.subject = 'XXX' item.body = 'YYY' item.save(update_fields=['subject']) item.refresh() self.assertEqual(item.subject, 'XXX') self.assertNotEqual(item.body, 'YYY') # Test invalid 'update_fields' input with self.assertRaises(ValueError) as e: item.save(update_fields=['xxx']) self.assertEqual( e.exception.args[0], "Field name(s) 'xxx' are not valid for a '%s' item" % self.ITEM_CLASS.__name__ ) with self.assertRaises(ValueError) as e: item.save(update_fields='subject') self.assertEqual( e.exception.args[0], "Field name(s) 's', 'u', 'b', 'j', 'e', 'c', 't' are not valid for a '%s' item" % self.ITEM_CLASS.__name__ ) def test_soft_delete(self): # First, empty trash bin self.account.trash.filter(categories__contains=self.categories).delete() self.account.recoverable_items_deletions.filter(categories__contains=self.categories).delete() item = self.get_test_item().save() item_id = (item.id, item.changekey) # Soft delete item.soft_delete() for e in self.account.fetch(ids=[item_id]): # It's gone from the test folder self.assertIsInstance(e, ErrorItemNotFound) # Really gone, not just changed ItemId self.assertEqual(self.test_folder.filter(categories__contains=item.categories).count(), 0) self.assertEqual(self.account.trash.filter(categories__contains=item.categories).count(), 0) # But we can find it in the recoverable items folder self.assertEqual( self.account.recoverable_items_deletions.filter(categories__contains=item.categories).count(), 1 ) def test_move_to_trash(self): # First, empty trash bin self.account.trash.filter(categories__contains=self.categories).delete() item = self.get_test_item().save() item_id = (item.id, item.changekey) # Move to trash item.move_to_trash() for e in self.account.fetch(ids=[item_id]): # Not in the test folder anymore self.assertIsInstance(e, ErrorItemNotFound) # Really gone, not just changed ItemId self.assertEqual(self.test_folder.filter(categories__contains=item.categories).count(), 0) # Test that the item moved to trash item = self.account.trash.get(categories__contains=item.categories) moved_item = self.get_item_by_id(item) # The item was copied, so the ItemId has changed. Let's compare the subject instead self.assertEqual(item.subject, moved_item.subject) def test_copy(self): # First, empty trash bin self.account.trash.filter(categories__contains=self.categories).delete() item = self.get_test_item().save() # Copy to trash. We use trash because it can contain all item types. copy_item_id, copy_changekey = item.copy(to_folder=self.account.trash) # Test that the item still exists in the folder self.assertEqual(self.test_folder.filter(categories__contains=item.categories).count(), 1) # Test that the copied item exists in trash copied_item = self.account.trash.get(categories__contains=item.categories) self.assertNotEqual(item.id, copied_item.id) self.assertNotEqual(item.changekey, copied_item.changekey) self.assertEqual(copy_item_id, copied_item.id) self.assertEqual(copy_changekey, copied_item.changekey) def test_move(self): # First, empty trash bin self.account.trash.filter(categories__contains=self.categories).delete() item = self.get_test_item().save() item_id = (item.id, item.changekey) # Move to trash. We use trash because it can contain all item types. This changes the ItemId item.move(to_folder=self.account.trash) for e in self.account.fetch(ids=[item_id]): # original item ID no longer exists self.assertIsInstance(e, ErrorItemNotFound) # Test that the item moved to trash self.assertEqual(self.test_folder.filter(categories__contains=item.categories).count(), 0) moved_item = self.account.trash.get(categories__contains=item.categories) self.assertEqual(item.id, moved_item.id) self.assertEqual(item.changekey, moved_item.changekey) def test_refresh(self): # Test that we can refresh items, and that refresh fails if the item no longer exists on the server item = self.get_test_item().save() orig_subject = item.subject item.subject = 'XXX' item.refresh() self.assertEqual(item.subject, orig_subject) item.delete() with self.assertRaises(ValueError): # Item no longer has an ID item.refresh() exchangelib-4.6.1/tests/test_items/test_messages.py000066400000000000000000000170711414601472700225510ustar00rootroot00000000000000from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText import time from exchangelib.folders import Inbox from exchangelib.items import Message from exchangelib.queryset import DoesNotExist from ..common import get_random_string from .test_basics import CommonItemTest class MessagesTest(CommonItemTest): # Just test one of the Message-type folders TEST_FOLDER = 'inbox' FOLDER_CLASS = Inbox ITEM_CLASS = Message INCOMING_MESSAGE_TIMEOUT = 60 def get_incoming_message(self, subject): t1 = time.monotonic() while True: t2 = time.monotonic() if t2 - t1 > self.INCOMING_MESSAGE_TIMEOUT: self.skipTest('Too bad. Gave up in %s waiting for the incoming message to show up' % self.id()) try: return self.account.inbox.get(subject=subject) except DoesNotExist: time.sleep(5) def test_send(self): # Test that we can send (only) Message items item = self.get_test_item() item.folder = None item.send() self.assertIsNone(item.id) self.assertIsNone(item.changekey) self.assertEqual(self.test_folder.filter(categories__contains=item.categories).count(), 0) def test_send_and_save(self): # Test that we can send_and_save Message items item = self.get_test_item() item.send_and_save() self.assertIsNone(item.id) self.assertIsNone(item.changekey) time.sleep(5) # Requests are supposed to be transactional, but apparently not... # Also, the sent item may be followed by an automatic message with the same category self.assertGreaterEqual(self.test_folder.filter(categories__contains=item.categories).count(), 1) # Test update, although it makes little sense item = self.get_test_item() item.save() item.send_and_save() time.sleep(5) # Requests are supposed to be transactional, but apparently not... # Also, the sent item may be followed by an automatic message with the same category self.assertGreaterEqual(self.test_folder.filter(categories__contains=item.categories).count(), 1) def test_send_draft(self): item = self.get_test_item() item.folder = self.account.drafts item.is_draft = True item.save() # Save a draft item.send() # Send the draft self.assertIsNone(item.id) self.assertIsNone(item.changekey) self.assertEqual(item.folder, self.account.sent) self.assertEqual(self.test_folder.filter(categories__contains=item.categories).count(), 0) def test_send_and_copy_to_folder(self): item = self.get_test_item() item.send(save_copy=True, copy_to_folder=self.account.sent) # Send the draft and save to the sent folder self.assertIsNone(item.id) self.assertIsNone(item.changekey) self.assertEqual(item.folder, self.account.sent) time.sleep(5) # Requests are supposed to be transactional, but apparently not... self.assertEqual(self.account.sent.filter(categories__contains=item.categories).count(), 1) def test_bulk_send(self): with self.assertRaises(AttributeError): self.account.bulk_send(ids=[], save_copy=False, copy_to_folder=self.account.trash) item = self.get_test_item() item.save() for res in self.account.bulk_send(ids=[item]): self.assertEqual(res, True) time.sleep(10) # Requests are supposed to be transactional, but apparently not... # By default, sent items are placed in the sent folder self.assertEqual(self.account.sent.filter(categories__contains=item.categories).count(), 1) def test_reply(self): # Test that we can reply to a Message item. EWS only allows items that have been sent to receive a reply item = self.get_test_item() item.folder = None item.send() # get_test_item() sets the to_recipients to the test account sent_item = self.get_incoming_message(item.subject) new_subject = ('Re: %s' % sent_item.subject)[:255] sent_item.reply(subject=new_subject, body='Hello reply', to_recipients=[item.author]) self.assertEqual(self.account.sent.filter(subject=new_subject).count(), 1) def test_reply_all(self): # Test that we can reply-all a Message item. EWS only allows items that have been sent to receive a reply item = self.get_test_item(folder=None) item.folder = None item.send() sent_item = self.get_incoming_message(item.subject) new_subject = ('Re: %s' % sent_item.subject)[:255] sent_item.reply_all(subject=new_subject, body='Hello reply') self.assertEqual(self.account.sent.filter(subject=new_subject).count(), 1) def test_forward(self): # Test that we can forward a Message item. EWS only allows items that have been sent to receive a reply item = self.get_test_item(folder=None) item.folder = None item.send() sent_item = self.get_incoming_message(item.subject) new_subject = ('Re: %s' % sent_item.subject)[:255] sent_item.forward(subject=new_subject, body='Hello reply', to_recipients=[item.author]) self.assertEqual(self.account.sent.filter(subject=new_subject).count(), 1) def test_create_forward(self): # Test that we can forward a Message item. EWS only allows items that have been sent to receive a reply item = self.get_test_item(folder=None) item.folder = None item.send() sent_item = self.get_incoming_message(item.subject) new_subject = ('Re: %s' % sent_item.subject)[:255] sent_item.create_forward(subject=new_subject, body='Hello reply', to_recipients=[item.author]).send() self.assertEqual(self.account.sent.filter(subject=new_subject).count(), 1) def test_mark_as_junk(self): # Test that we can mark a Message item as junk and non-junk, and that the message goes to the junk forlder and # back to the the inbox. item = self.get_test_item().save() item.mark_as_junk(is_junk=False, move_item=False) self.assertEqual(item.folder, self.test_folder) self.assertEqual(self.test_folder.get(categories__contains=self.categories).id, item.id) item.mark_as_junk(is_junk=True, move_item=False) self.assertEqual(item.folder, self.test_folder) self.assertEqual(self.test_folder.get(categories__contains=self.categories).id, item.id) item.mark_as_junk(is_junk=True, move_item=True) self.assertEqual(item.folder, self.account.junk) self.assertEqual(self.account.junk.get(categories__contains=self.categories).id, item.id) item.mark_as_junk(is_junk=False, move_item=True) self.assertEqual(item.folder, self.account.inbox) self.assertEqual(self.account.inbox.get(categories__contains=self.categories).id, item.id) def test_mime_content(self): # Tests the 'mime_content' field subject = get_random_string(16) msg = MIMEMultipart() msg['From'] = self.account.primary_smtp_address msg['To'] = self.account.primary_smtp_address msg['Subject'] = subject body = 'MIME test mail' msg.attach(MIMEText(body, 'plain', _charset='utf-8')) mime_content = msg.as_bytes() self.ITEM_CLASS( folder=self.test_folder, to_recipients=[self.account.primary_smtp_address], mime_content=mime_content, categories=self.categories, ).save() self.assertEqual(self.test_folder.get(subject=subject).body, body) exchangelib-4.6.1/tests/test_items/test_queryset.py000066400000000000000000000437431414601472700226300ustar00rootroot00000000000000import time import warnings from exchangelib.folders import Inbox, FolderCollection from exchangelib.items import Message, SHALLOW, ASSOCIATED from exchangelib.queryset import QuerySet, DoesNotExist, MultipleObjectsReturned from .test_basics import BaseItemTest class ItemQuerySetTest(BaseItemTest): TEST_FOLDER = 'inbox' FOLDER_CLASS = Inbox ITEM_CLASS = Message def test_querysets(self): test_items = [] for i in range(4): item = self.get_test_item() item.subject = 'Item %s' % i item.save() test_items.append(item) qs = QuerySet( folder_collection=FolderCollection(account=self.account, folders=[self.test_folder]) ).filter(categories__contains=self.categories) test_cat = self.categories[0] self.assertEqual( {(i.subject, i.categories[0]) for i in qs}, {('Item 0', test_cat), ('Item 1', test_cat), ('Item 2', test_cat), ('Item 3', test_cat)} ) self.assertEqual( [(i.subject, i.categories[0]) for i in qs.none()], [] ) self.assertEqual( [(i.subject, i.categories[0]) for i in qs.filter(subject__startswith='Item 2')], [('Item 2', test_cat)] ) self.assertEqual( {(i.subject, i.categories[0]) for i in qs.exclude(subject__startswith='Item 2')}, {('Item 0', test_cat), ('Item 1', test_cat), ('Item 3', test_cat)} ) self.assertEqual( {(i.subject, i.categories) for i in qs.only('subject')}, {('Item 0', None), ('Item 1', None), ('Item 2', None), ('Item 3', None)} ) self.assertEqual( [(i.subject, i.categories[0]) for i in qs.order_by('subject')], [('Item 0', test_cat), ('Item 1', test_cat), ('Item 2', test_cat), ('Item 3', test_cat)] ) self.assertEqual( # Test '-some_field' syntax for reverse sorting [(i.subject, i.categories[0]) for i in qs.order_by('-subject')], [('Item 3', test_cat), ('Item 2', test_cat), ('Item 1', test_cat), ('Item 0', test_cat)] ) self.assertEqual( # Test ordering on a field that we don't need to fetch [(i.subject, i.categories[0]) for i in qs.order_by('-subject').only('categories')], [(None, test_cat), (None, test_cat), (None, test_cat), (None, test_cat)] ) self.assertEqual( [(i.subject, i.categories[0]) for i in qs.order_by('subject').reverse()], [('Item 3', test_cat), ('Item 2', test_cat), ('Item 1', test_cat), ('Item 0', test_cat)] ) with self.assertRaises(ValueError): list(qs.values([])) self.assertEqual( list(qs.order_by('subject').values('subject')), [{'subject': 'Item 0'}, {'subject': 'Item 1'}, {'subject': 'Item 2'}, {'subject': 'Item 3'}] ) # Test .values() in combinations of 'id' and 'changekey', which are handled specially self.assertEqual( list(qs.order_by('subject').values('id')), [{'id': i.id} for i in test_items] ) self.assertEqual( list(qs.order_by('subject').values('changekey')), [{'changekey': i.changekey} for i in test_items] ) self.assertEqual( list(qs.order_by('subject').values('id', 'changekey')), [{k: getattr(i, k) for k in ('id', 'changekey')} for i in test_items] ) self.assertEqual( set(qs.values_list('subject')), {('Item 0',), ('Item 1',), ('Item 2',), ('Item 3',)} ) # Test .values_list() in combinations of 'id' and 'changekey', which are handled specially self.assertEqual( list(qs.order_by('subject').values_list('id')), [(i.id,) for i in test_items] ) self.assertEqual( list(qs.order_by('subject').values_list('changekey')), [(i.changekey,) for i in test_items] ) self.assertEqual( list(qs.order_by('subject').values_list('id', 'changekey')), [(i.id, i.changekey) for i in test_items] ) self.assertEqual( {i.subject for i in qs.only('subject')}, {'Item 0', 'Item 1', 'Item 2', 'Item 3'} ) # Test .only() in combinations of 'id' and 'changekey', which are handled specially self.assertEqual( [(i.id,) for i in qs.order_by('subject').only('id')], [(i.id,) for i in test_items] ) self.assertEqual( [(i.changekey,) for i in qs.order_by('subject').only('changekey')], [(i.changekey,) for i in test_items] ) self.assertEqual( [(i.id, i.changekey) for i in qs.order_by('subject').only('id', 'changekey')], [(i.id, i.changekey) for i in test_items] ) with self.assertRaises(ValueError): list(qs.values_list('id', 'changekey', flat=True)) with self.assertRaises(AttributeError): list(qs.values_list('id', xxx=True)) self.assertEqual( list(qs.order_by('subject').values_list('id', flat=True)), [i.id for i in test_items] ) self.assertEqual( list(qs.order_by('subject').values_list('changekey', flat=True)), [i.changekey for i in test_items] ) self.assertEqual( set(qs.values_list('subject', flat=True)), {'Item 0', 'Item 1', 'Item 2', 'Item 3'} ) self.assertEqual( qs.values_list('subject', flat=True).get(subject='Item 2'), 'Item 2' ) self.assertEqual( {(i.subject, i.categories[0]) for i in qs.exclude(subject__startswith='Item 2')}, {('Item 0', test_cat), ('Item 1', test_cat), ('Item 3', test_cat)} ) # Test that we can sort on a field that we don't want self.assertEqual( [i.categories[0] for i in qs.only('categories').order_by('subject')], [test_cat, test_cat, test_cat, test_cat] ) # Test iterator with warnings.catch_warnings(): # iterator() is deprecated but we still want to test it. Silence the DeprecationWarning warnings.simplefilter("ignore") self.assertEqual( {(i.subject, i.categories[0]) for i in qs.iterator()}, {('Item 0', test_cat), ('Item 1', test_cat), ('Item 2', test_cat), ('Item 3', test_cat)} ) # Test that iterator() preserves the result format self.assertEqual( {(i[0], i[1][0]) for i in qs.values_list('subject', 'categories').iterator()}, {('Item 0', test_cat), ('Item 1', test_cat), ('Item 2', test_cat), ('Item 3', test_cat)} ) self.assertEqual(qs.get(subject='Item 3').subject, 'Item 3') with self.assertRaises(DoesNotExist): qs.get(subject='Item XXX') with self.assertRaises(MultipleObjectsReturned): qs.get(subject__startswith='Item') # len() and count() self.assertEqual(qs.count(), 4) # Indexing and slicing self.assertTrue(isinstance(qs[0], self.ITEM_CLASS)) self.assertEqual(len(list(qs[1:3])), 2) self.assertEqual(qs.count(), 4) with self.assertRaises(IndexError): print(qs[99999]) # Exists self.assertEqual(qs.exists(), True) self.assertEqual(qs.filter(subject='Test XXX').exists(), False) self.assertEqual( qs.filter(subject__startswith='Item').delete(), [True, True, True, True] ) def test_queryset_failure(self): qs = QuerySet( folder_collection=FolderCollection(account=self.account, folders=[self.test_folder]) ).filter(categories__contains=self.categories) with self.assertRaises(ValueError): qs.order_by('XXX') with self.assertRaises(ValueError): qs.values('XXX') with self.assertRaises(ValueError): qs.values_list('XXX') with self.assertRaises(ValueError): qs.only('XXX') with self.assertRaises(ValueError): qs.reverse() # We can't reverse when we haven't defined an order yet def test_cached_queryset_corner_cases(self): test_items = [] for i in range(4): item = self.get_test_item() item.subject = 'Item %s' % i item.save() test_items.append(item) qs = QuerySet( folder_collection=FolderCollection(account=self.account, folders=[self.test_folder]) ).filter(categories__contains=self.categories).order_by('subject') with self.assertRaises(MultipleObjectsReturned): qs.get() # Get with a full cache self.assertEqual(qs[2].subject, 'Item 2') # Index with a full cache self.assertEqual(qs[-2].subject, 'Item 2') # Negative index with a full cache qs.delete() # Delete with a full cache self.assertEqual(qs.count(), 0) # QuerySet is empty after delete self.assertEqual(list(qs.none()), []) def test_queryset_get_by_id(self): item = self.get_test_item().save() with self.assertRaises(ValueError): list(self.test_folder.filter(id__in=[item.id])) with self.assertRaises(ValueError): list(self.test_folder.get(id=item.id, changekey=item.changekey, subject='XXX')) with self.assertRaises(ValueError): list(self.test_folder.get(id=None, changekey=item.changekey)) # Test a simple get() get_item = self.test_folder.get(id=item.id, changekey=item.changekey) self.assertEqual(item.id, get_item.id) self.assertEqual(item.changekey, get_item.changekey) self.assertEqual(item.subject, get_item.subject) self.assertEqual(item.body, get_item.body) # Test get() with ID only get_item = self.test_folder.get(id=item.id) self.assertEqual(item.id, get_item.id) self.assertEqual(item.changekey, get_item.changekey) self.assertEqual(item.subject, get_item.subject) self.assertEqual(item.body, get_item.body) get_item = self.test_folder.get(id=item.id, changekey=None) self.assertEqual(item.id, get_item.id) self.assertEqual(item.changekey, get_item.changekey) self.assertEqual(item.subject, get_item.subject) self.assertEqual(item.body, get_item.body) # Test a get() from queryset get_item = self.test_folder.all().get(id=item.id, changekey=item.changekey) self.assertEqual(item.id, get_item.id) self.assertEqual(item.changekey, get_item.changekey) self.assertEqual(item.subject, get_item.subject) self.assertEqual(item.body, get_item.body) # Test a get() with only() get_item = self.test_folder.all().only('subject').get(id=item.id, changekey=item.changekey) self.assertEqual(item.id, get_item.id) self.assertEqual(item.changekey, get_item.changekey) self.assertEqual(item.subject, get_item.subject) self.assertIsNone(get_item.body) def test_paging(self): # Test that paging services work correctly. Default EWS paging size is 1000 items. Our default is 100 items. items = [] for _ in range(11): i = self.get_test_item() del i.attachments[:] items.append(i) self.test_folder.bulk_create(items=items) ids = self.test_folder.filter(categories__contains=self.categories).values_list('id', 'changekey') ids.page_size = 10 self.bulk_delete(ids) def test_slicing(self): # Test that slicing works correctly items = [] for i in range(4): item = self.get_test_item() item.subject = 'Subj %s' % i del item.attachments[:] items.append(item) self.test_folder.bulk_create(items=items) qs = self.test_folder.filter(categories__contains=self.categories).only('subject').order_by('subject') # Test positive index self.assertEqual( qs._copy_self()[0].subject, 'Subj 0' ) # Test positive index self.assertEqual( qs._copy_self()[3].subject, 'Subj 3' ) # Test negative index self.assertEqual( qs._copy_self()[-2].subject, 'Subj 2' ) # Test positive slice self.assertEqual( [i.subject for i in qs._copy_self()[0:2]], ['Subj 0', 'Subj 1'] ) # Test positive slice self.assertEqual( [i.subject for i in qs._copy_self()[2:4]], ['Subj 2', 'Subj 3'] ) # Test positive open slice self.assertEqual( [i.subject for i in qs._copy_self()[:2]], ['Subj 0', 'Subj 1'] ) # Test positive open slice self.assertEqual( [i.subject for i in qs._copy_self()[2:]], ['Subj 2', 'Subj 3'] ) # Test negative slice self.assertEqual( [i.subject for i in qs._copy_self()[-3:-1]], ['Subj 1', 'Subj 2'] ) # Test negative slice self.assertEqual( [i.subject for i in qs._copy_self()[1:-1]], ['Subj 1', 'Subj 2'] ) # Test negative open slice self.assertEqual( [i.subject for i in qs._copy_self()[:-2]], ['Subj 0', 'Subj 1'] ) # Test negative open slice self.assertEqual( [i.subject for i in qs._copy_self()[-2:]], ['Subj 2', 'Subj 3'] ) # Test positive slice with step self.assertEqual( [i.subject for i in qs._copy_self()[0:4:2]], ['Subj 0', 'Subj 2'] ) # Test negative slice with step self.assertEqual( [i.subject for i in qs._copy_self()[4:0:-2]], ['Subj 3', 'Subj 1'] ) def test_delete_via_queryset(self): self.get_test_item().save() qs = self.test_folder.filter(categories__contains=self.categories) self.assertEqual(qs.count(), 1) qs.delete() self.assertEqual(qs.count(), 0) def test_send_via_queryset(self): self.get_test_item().save() qs = self.test_folder.filter(categories__contains=self.categories) to_folder = self.account.sent to_folder_qs = to_folder.filter(categories__contains=self.categories) self.assertEqual(qs.count(), 1) self.assertEqual(to_folder_qs.count(), 0) qs.send(copy_to_folder=to_folder) time.sleep(5) # Requests are supposed to be transactional, but apparently not... self.assertEqual(qs.count(), 0) self.assertEqual(to_folder_qs.count(), 1) def test_send_with_no_copy_via_queryset(self): self.get_test_item().save() qs = self.test_folder.filter(categories__contains=self.categories) to_folder = self.account.sent to_folder_qs = to_folder.filter(categories__contains=self.categories) self.assertEqual(qs.count(), 1) self.assertEqual(to_folder_qs.count(), 0) qs.send(save_copy=False) time.sleep(5) # Requests are supposed to be transactional, but apparently not... self.assertEqual(qs.count(), 0) self.assertEqual(to_folder_qs.count(), 0) def test_copy_via_queryset(self): self.get_test_item().save() qs = self.test_folder.filter(categories__contains=self.categories) to_folder = self.account.trash to_folder_qs = to_folder.filter(categories__contains=self.categories) self.assertEqual(qs.count(), 1) self.assertEqual(to_folder_qs.count(), 0) qs.copy(to_folder=to_folder) self.assertEqual(qs.count(), 1) self.assertEqual(to_folder_qs.count(), 1) def test_move_via_queryset(self): self.get_test_item().save() qs = self.test_folder.filter(categories__contains=self.categories) to_folder = self.account.trash to_folder_qs = to_folder.filter(categories__contains=self.categories) self.assertEqual(qs.count(), 1) self.assertEqual(to_folder_qs.count(), 0) qs.move(to_folder=to_folder) self.assertEqual(qs.count(), 0) self.assertEqual(to_folder_qs.count(), 1) def test_mark_as_junk_via_queryset(self): self.get_test_item().save() qs = self.test_folder.filter(categories__contains=self.categories) qs.mark_as_junk(is_junk=False, move_item=False) self.assertEqual(self.test_folder.filter(categories__contains=self.categories).count(), 1) qs.mark_as_junk(is_junk=True, move_item=False) self.assertEqual(self.test_folder.filter(categories__contains=self.categories).count(), 1) qs.mark_as_junk(is_junk=True, move_item=True) self.assertEqual(self.account.junk.filter(categories__contains=self.categories).count(), 1) self.account.junk.filter(categories__contains=self.categories).mark_as_junk(is_junk=False, move_item=True) self.assertEqual(self.account.inbox.filter(categories__contains=self.categories).count(), 1) def test_archive_via_queryset(self): self.get_test_item().save() qs = self.test_folder.filter(categories__contains=self.categories) to_folder = self.account.trash qs.archive(to_folder=to_folder) self.assertEqual(qs.count(), 0) self.assertEqual(to_folder.filter(categories__contains=self.categories).count(), 1) def test_depth(self): self.assertGreaterEqual(self.test_folder.all().depth(ASSOCIATED).count(), 0) self.assertGreaterEqual(self.test_folder.all().depth(SHALLOW).count(), 0) def test_empty_in_sequence(self): # 'Q(foo_in=[])' should always match nothing. We interpret it to mean "is foo contained in the empty set?" # which is always false. item = self.get_test_item().save() qs = self.test_folder.filter(categories__contains=self.categories) self.assertEqual(qs.filter(subject__in=[item.subject]).count(), 1) self.assertEqual(qs.filter(subject__in=[]).count(), 0) exchangelib-4.6.1/tests/test_items/test_sync.py000066400000000000000000000270151414601472700217150ustar00rootroot00000000000000import time from exchangelib.errors import ErrorInvalidSubscription, ErrorSubscriptionNotFound from exchangelib.folders import Inbox from exchangelib.items import Message from exchangelib.properties import StatusEvent, CreatedEvent, ModifiedEvent, DeletedEvent, Notification from exchangelib.services import SendNotification from .test_basics import BaseItemTest from ..common import get_random_string class SyncTest(BaseItemTest): TEST_FOLDER = 'inbox' FOLDER_CLASS = Inbox ITEM_CLASS = Message def test_pull_subscribe(self): with self.account.inbox.pull_subscription() as (subscription_id, watermark): self.assertIsNotNone(subscription_id) self.assertIsNotNone(watermark) # Context manager already unsubscribed us with self.assertRaises(ErrorSubscriptionNotFound): self.account.inbox.unsubscribe(subscription_id) def test_push_subscribe(self): with self.account.inbox.push_subscription( callback_url='https://example.com/foo' ) as (subscription_id, watermark): self.assertIsNotNone(subscription_id) self.assertIsNotNone(watermark) with self.assertRaises(ErrorInvalidSubscription): self.account.inbox.unsubscribe(subscription_id) def test_streaming_subscribe(self): with self.account.inbox.streaming_subscription() as subscription_id: self.assertIsNotNone(subscription_id) # Context manager already unsubscribed us with self.assertRaises(ErrorSubscriptionNotFound): self.account.inbox.unsubscribe(subscription_id) def test_sync_folder_hierarchy(self): test_folder = self.get_test_folder().save() # Test that folder_sync_state is set after calling sync_hierarchy self.assertIsNone(test_folder.folder_sync_state) list(test_folder.sync_hierarchy()) self.assertIsNotNone(test_folder.folder_sync_state) # Test that we see a create event f1 = self.FOLDER_CLASS(parent=test_folder, name=get_random_string(8)).save() changes = list(test_folder.sync_hierarchy()) self.assertEqual(len(changes), 1) change_type, f = changes[0] self.assertEqual(change_type, 'create') self.assertEqual(f.id, f1.id) # Test that we see an update event f1.name = get_random_string(8) f1.save(update_fields=['name']) changes = list(test_folder.sync_hierarchy()) self.assertEqual(len(changes), 1) change_type, f = changes[0] self.assertEqual(change_type, 'update') self.assertEqual(f.id, f1.id) # Test that we see a delete event f1_id = f1.id f1.delete() changes = list(test_folder.sync_hierarchy()) self.assertEqual(len(changes), 1) change_type, f = changes[0] self.assertEqual(change_type, 'delete') self.assertEqual(f.id, f1_id) def test_sync_folder_items(self): test_folder = self.get_test_folder().save() # Test that item_sync_state is set after calling sync_hierarchy self.assertIsNone(test_folder.item_sync_state) list(test_folder.sync_items()) self.assertIsNotNone(test_folder.item_sync_state) # Test that we see a create event i1 = self.get_test_item(folder=test_folder).save() changes = list(test_folder.sync_items()) self.assertEqual(len(changes), 1) change_type, i = changes[0] self.assertEqual(change_type, 'create') self.assertEqual(i.id, i1.id) # Test that we see an update event i1.subject = get_random_string(8) i1.save(update_fields=['subject']) changes = list(test_folder.sync_items()) self.assertEqual(len(changes), 1) change_type, i = changes[0] self.assertEqual(change_type, 'update') self.assertEqual(i.id, i1.id) # Test that we see a read_flag_change event i1.is_read = not i1.is_read i1.save(update_fields=['is_read']) changes = list(test_folder.sync_items()) self.assertEqual(len(changes), 1) change_type, (i, read_state) = changes[0] self.assertEqual(change_type, 'read_flag_change') self.assertEqual(i.id, i1.id) self.assertEqual(read_state, i1.is_read) # Test that we see a delete event i1_id = i1.id i1.delete() changes = list(test_folder.sync_items()) self.assertEqual(len(changes), 1) change_type, i = changes[0] self.assertEqual(change_type, 'delete') self.assertEqual(i.id, i1_id) def _filter_events(self, notifications, event_cls, item_id): events = [] watermark = None for notification in notifications: for e in notification.events: watermark = e.watermark if not isinstance(e, event_cls): continue if item_id is None: events.append(e) continue if e.event_type == event_cls.ITEM and e.item_id.id == item_id: events.append(e) self.assertEqual(len(events), 1) event = events[0] self.assertIsInstance(event, event_cls) return event, watermark def test_pull_notifications(self): # Test that we can create a pull subscription, make changes and see the events by calling .get_events() test_folder = self.account.drafts with test_folder.pull_subscription() as (subscription_id, watermark): notifications = list(test_folder.get_events(subscription_id, watermark)) _, watermark = self._filter_events(notifications, StatusEvent, None) # Test that we see a create event i1 = self.get_test_item(folder=test_folder).save() time.sleep(5) # TODO: For some reason, events do not trigger instantly notifications = list(test_folder.get_events(subscription_id, watermark)) created_event, watermark = self._filter_events(notifications, CreatedEvent, i1.id) self.assertEqual(created_event.item_id.id, i1.id) # Test that we see an update event i1.subject = get_random_string(8) i1.save(update_fields=['subject']) time.sleep(5) # TODO: For some reason, events do not trigger instantly notifications = list(test_folder.get_events(subscription_id, watermark)) modified_event, watermark = self._filter_events(notifications, ModifiedEvent, i1.id) self.assertEqual(modified_event.item_id.id, i1.id) # Test that we see a delete event i1_id = i1.id i1.delete() time.sleep(5) # TODO: For some reason, events do not trigger instantly notifications = list(test_folder.get_events(subscription_id, watermark)) deleted_event, watermark = self._filter_events(notifications, DeletedEvent, i1_id) self.assertEqual(deleted_event.item_id.id, i1_id) def test_streaming_notifications(self): # Test that we can create a streaming subscription, make changes and see the events by calling # .get_streaming_events() test_folder = self.account.drafts with test_folder.streaming_subscription() as subscription_id: # Test that we see a create event i1 = self.get_test_item(folder=test_folder).save() # 1 minute connection timeout notifications = list(test_folder.get_streaming_events( subscription_id, connection_timeout=1, max_notifications_returned=1 )) created_event, _ = self._filter_events(notifications, CreatedEvent, i1.id) self.assertEqual(created_event.item_id.id, i1.id) # Test that we see an update event i1.subject = get_random_string(8) i1.save(update_fields=['subject']) # 1 minute connection timeout notifications = list(test_folder.get_streaming_events( subscription_id, connection_timeout=1, max_notifications_returned=1 )) modified_event, _ = self._filter_events(notifications, ModifiedEvent, i1.id) self.assertEqual(modified_event.item_id.id, i1.id) # Test that we see a delete event i1_id = i1.id i1.delete() # 1 minute connection timeout notifications = list(test_folder.get_streaming_events( subscription_id, connection_timeout=1, max_notifications_returned=1 )) deleted_event, _ = self._filter_events(notifications, DeletedEvent, i1_id) self.assertEqual(deleted_event.item_id.id, i1_id) def test_streaming_with_other_calls(self): # Test that we can call other EWS operations while we have a streaming subscription open test_folder = self.account.drafts # Test calling GetItem while the streaming connection is still open. We need to bump the # connection count because the default count is 1 but we need 2 connections. self.account.protocol._session_pool_maxsize += 1 self.account.protocol.increase_poolsize() try: with test_folder.streaming_subscription() as subscription_id: i1 = self.get_test_item(folder=test_folder).save() for notification in test_folder.get_streaming_events( subscription_id, connection_timeout=1, max_notifications_returned=1 ): for e in notification.events: if isinstance(e, CreatedEvent) and e.event_type == CreatedEvent.ITEM \ and e.item_id.id == i1.id: test_folder.all().only('id').get(id=e.item_id.id) finally: self.account.protocol.decrease_poolsize() self.account.protocol._session_pool_maxsize -= 1 def test_push_message_parsing(self): xml = b'''\ NoError XXXXX= AAAAA= false BBBBB= ''' ws = SendNotification(protocol=None) self.assertListEqual( list(ws.parse(xml)), [Notification(subscription_id='XXXXX=', previous_watermark='AAAAA=', more_events=False, events=[StatusEvent(watermark='BBBBB=')])] ) exchangelib-4.6.1/tests/test_items/test_tasks.py000066400000000000000000000155471414601472700220750ustar00rootroot00000000000000import datetime from decimal import Decimal from exchangelib.folders import Tasks from exchangelib.items import Task from exchangelib.recurrence import TaskRecurrence, DailyPattern, DailyRegeneration from .test_basics import CommonItemTest class TasksTest(CommonItemTest): """Test Task instances and the Tasks folder.""" TEST_FOLDER = 'tasks' FOLDER_CLASS = Tasks ITEM_CLASS = Task def test_task_validation(self): tz = self.account.default_timezone task = Task(due_date=datetime.date(2017, 1, 1), start_date=datetime.date(2017, 2, 1)) task.clean() # We reset due date if it's before start date self.assertEqual(task.due_date, datetime.date(2017, 2, 1)) self.assertEqual(task.due_date, task.start_date) task = Task(complete_date=datetime.datetime(2099, 1, 1, tzinfo=tz), status=Task.NOT_STARTED) task.clean() # We reset status if complete_date is set self.assertEqual(task.status, Task.COMPLETED) # We also reset complete date to now() if it's in the future self.assertEqual(task.complete_date.date(), datetime.datetime.utcnow().date()) task = Task(complete_date=datetime.datetime(2017, 1, 1, tzinfo=tz), start_date=datetime.date(2017, 2, 1)) task.clean() # We also reset complete date to start_date if it's before start_date self.assertEqual(task.complete_date.date(), task.start_date) task = Task(percent_complete=Decimal('50.0'), status=Task.COMPLETED) task.clean() # We reset percent_complete to 100.0 if state is completed self.assertEqual(task.percent_complete, Decimal(100)) task = Task(percent_complete=Decimal('50.0'), status=Task.NOT_STARTED) task.clean() # We reset percent_complete to 0.0 if state is not_started self.assertEqual(task.percent_complete, Decimal(0)) def test_complete(self): item = self.get_test_item().save() item.refresh() self.assertNotEqual(item.status, Task.COMPLETED) self.assertNotEqual(item.percent_complete, Decimal(100)) item.complete() item.refresh() self.assertEqual(item.status, Task.COMPLETED) self.assertEqual(item.percent_complete, Decimal(100)) def test_recurring_item(self): """Test that changes to an occurrence of a recurring task cause one-off tasks to be generated when the following updates are made: * The status property of a regenerating or nonregenerating recurrent task is set to Completed. * The start date or end date of a nonregenerating recurrent task is changed. """ # Create a master non-regenerating item with 4 daily occurrences start = datetime.date(2016, 1, 1) recurrence = TaskRecurrence(pattern=DailyPattern(interval=1), start=start, number=4) nonregenerating_item = self.ITEM_CLASS( folder=self.test_folder, categories=self.categories, recurrence=recurrence, ).save() nonregenerating_item.refresh() master_item_id = nonregenerating_item.id self.assertEqual(nonregenerating_item.is_recurring, True) self.assertEqual(nonregenerating_item.change_count, 1) self.assertEqual(self.test_folder.filter(categories__contains=self.categories).count(), 1) # Change the start date. We should see a new task appear. master_item = self.get_item_by_id((master_item_id, None)) master_item.recurrence.boundary.start = datetime.date(2016, 2, 1) occurrence_item = master_item.save() occurrence_item.refresh() self.assertEqual(occurrence_item.is_recurring, False) # This is now the occurrence self.assertEqual(self.test_folder.filter(categories__contains=self.categories).count(), 2) # Check fields on the recurring item master_item = self.get_item_by_id((master_item_id, None)) self.assertEqual(master_item.change_count, 2) self.assertEqual(master_item.due_date, datetime.date(2016, 1, 2)) # This is the next occurrence self.assertEqual(master_item.recurrence.boundary.number, 3) # One less # Change the status to 'Completed'. We should see a new task appear. master_item.status = Task.COMPLETED occurrence_item = master_item.save() occurrence_item.refresh() self.assertEqual(occurrence_item.is_recurring, False) # This is now the occurrence self.assertEqual(self.test_folder.filter(categories__contains=self.categories).count(), 3) # Check fields on the recurring item master_item = self.get_item_by_id((master_item_id, None)) self.assertEqual(master_item.change_count, 3) self.assertEqual(master_item.due_date, datetime.date(2016, 2, 1)) # This is the next occurrence self.assertEqual(master_item.recurrence.boundary.number, 2) # One less self.test_folder.filter(categories__contains=self.categories).delete() # Create a master regenerating item with 4 daily occurrences recurrence = TaskRecurrence(pattern=DailyRegeneration(interval=1), start=start, number=4) regenerating_item = self.ITEM_CLASS( folder=self.test_folder, categories=self.categories, recurrence=recurrence, ).save() regenerating_item.refresh() master_item_id = regenerating_item.id self.assertEqual(regenerating_item.is_recurring, True) self.assertEqual(regenerating_item.change_count, 1) self.assertEqual(self.test_folder.filter(categories__contains=self.categories).count(), 1) # Change the start date. We should *not* see a new task appear. master_item = self.get_item_by_id((master_item_id, None)) master_item.recurrence.boundary.start = datetime.date(2016, 1, 2) occurrence_item = master_item.save() occurrence_item.refresh() self.assertEqual(occurrence_item.id, master_item.id) # This is not an occurrence. No new task was created self.assertEqual(self.test_folder.filter(categories__contains=self.categories).count(), 1) # Change the status to 'Completed'. We should see a new task appear. master_item.status = Task.COMPLETED occurrence_item = master_item.save() occurrence_item.refresh() self.assertEqual(occurrence_item.is_recurring, False) # This is now the occurrence self.assertEqual(self.test_folder.filter(categories__contains=self.categories).count(), 2) # Check fields on the recurring item master_item = self.get_item_by_id((master_item_id, None)) self.assertEqual(master_item.change_count, 2) # The due date is the next occurrence after today tz = self.account.default_timezone self.assertEqual(master_item.due_date, datetime.datetime.now(tz).date() + datetime.timedelta(days=1)) self.assertEqual(master_item.recurrence.boundary.number, 3) # One less exchangelib-4.6.1/tests/test_properties.py000066400000000000000000000224671414601472700207630ustar00rootroot00000000000000from inspect import isclass from itertools import chain from exchangelib.properties import HTMLBody, Body, Mailbox, DLMailbox, UID, ItemId from exchangelib.fields import TextField, InvalidField, InvalidFieldForVersion from exchangelib.folders import Folder, RootOfHierarchy from exchangelib.indexed_properties import PhysicalAddress from exchangelib.items import Item, BulkCreateResult from exchangelib.properties import EWSElement, MessageHeader from exchangelib.extended_properties import ExternId, Flag from exchangelib.util import to_xml, TNS from exchangelib.version import Version, EXCHANGE_2010, EXCHANGE_2013 from .common import TimedTestCase class PropertiesTest(TimedTestCase): def test_ews_element_sanity(self): from exchangelib import attachments, properties, items, folders, indexed_properties, extended_properties, \ recurrence, settings for module in (attachments, properties, items, folders, indexed_properties, extended_properties, recurrence, settings): for cls in vars(module).values(): if not isclass(cls) or not issubclass(cls, EWSElement): continue with self.subTest(cls=cls): # Make sure that we have an ELEMENT_NAME on all models if cls != BulkCreateResult and not (cls.__doc__ and cls.__doc__.startswith('Base class ')): self.assertIsNotNone(cls.ELEMENT_NAME, '%s must have an ELEMENT_NAME' % cls) # Assert that all FIELDS names are unique on the model. Also assert that the class defines # __slots__, that all fields are mentioned in __slots__ and that __slots__ is unique. field_names = set() all_slots = tuple(chain(*(getattr(c, '__slots__', ()) for c in cls.__mro__))) self.assertEqual(len(all_slots), len(set(all_slots)), 'Model %s: __slots__ contains duplicates: %s' % (cls, sorted(all_slots))) for f in cls.FIELDS: with self.subTest(f=f): self.assertNotIn(f.name, field_names, 'Field name %r is not unique on model %r' % (f.name, cls.__name__)) self.assertIn(f.name, all_slots, 'Field name %s is not in __slots__ on model %s' % (f.name, cls.__name__)) field_names.add(f.name) # Finally, test that all models have a link to MSDN documentation if issubclass(cls, Folder): # We have a long list of folders subclasses. Don't require a docstring for each continue self.assertIsNotNone(cls.__doc__, '%s is missing a docstring' % cls) if cls in (DLMailbox, BulkCreateResult, ExternId, Flag): # Some classes are allowed to not have a link continue if cls.__doc__.startswith('Base class '): # Base classes don't have an MSDN link continue if issubclass(cls, RootOfHierarchy): # Root folders don't have an MSDN link continue # collapse multiline docstrings docstring = ' '.join(doc.strip() for doc in cls.__doc__.split('\n')) self.assertIn('MSDN: https://docs.microsoft.com', docstring, '%s is missing an MSDN link in the docstring' % cls) def test_uid(self): # Test translation of calendar UIDs. See #453 self.assertEqual( UID('261cbc18-1f65-5a0a-bd11-23b1e224cc2f'), b'\x04\x00\x00\x00\x82\x00\xe0\x00t\xc5\xb7\x10\x1a\x82\xe0\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00vCal-Uid\x01\x00\x00\x00' b'261cbc18-1f65-5a0a-bd11-23b1e224cc2f\x00' ) def test_internet_message_headers(self): # Message headers are read-only, and an integration test is difficult because we can't reliably AND quickly # generate emails that pass through some relay server that adds headers. Create a unit test instead. payload = b'''\ from foo by bar Hello from DKIM 1.0 Contoso Mail foo@example.com ''' headers_elem = to_xml(payload).find('{%s}InternetMessageHeaders' % TNS) headers = {} for elem in headers_elem.findall('{%s}InternetMessageHeader' % TNS): header = MessageHeader.from_xml(elem=elem, account=None) headers[header.name] = header.value self.assertDictEqual( headers, { 'Received': 'from foo by bar', 'DKIM-Signature': 'Hello from DKIM', 'MIME-Version': '1.0', 'X-Mailer': 'Contoso Mail', 'Return-Path': 'foo@example.com', } ) def test_physical_address(self): # Test that we can enter an integer zipcode and that it's converted to a string by clean() zipcode = 98765 addr = PhysicalAddress(zipcode=zipcode) addr.clean() self.assertEqual(addr.zipcode, str(zipcode)) def test_invalid_kwargs(self): with self.assertRaises(AttributeError): Mailbox(foo='XXX') def test_invalid_field(self): test_field = Item.get_field_by_fieldname(fieldname='text_body') self.assertIsInstance(test_field, TextField) self.assertEqual(test_field.name, 'text_body') with self.assertRaises(InvalidField): Item.get_field_by_fieldname(fieldname='xxx') Item.validate_field(field=test_field, version=Version(build=EXCHANGE_2013)) with self.assertRaises(InvalidFieldForVersion) as e: Item.validate_field(field=test_field, version=Version(build=EXCHANGE_2010)) self.assertEqual( e.exception.args[0], "Field 'text_body' is not supported on server version Build=14.0.0.0, API=Exchange2010, Fullname=Microsoft " "Exchange Server 2010 (supported from: 15.0.0.0, deprecated from: None)" ) def test_add_field(self): field = TextField('foo', field_uri='bar') Item.add_field(field, insert_after='subject') try: self.assertEqual(Item.get_field_by_fieldname('foo'), field) finally: Item.remove_field(field) def test_itemid_equality(self): self.assertEqual(ItemId('X', 'Y'), ItemId('X', 'Y')) self.assertNotEqual(ItemId('X', 'Y'), ItemId('X', 'Z')) self.assertNotEqual(ItemId('Z', 'Y'), ItemId('X', 'Y')) self.assertNotEqual(ItemId('X', 'Y'), ItemId('Z', 'Z')) self.assertNotEqual(ItemId('X', 'Y'), None) def test_mailbox(self): mbx = Mailbox(name='XXX') with self.assertRaises(ValueError): mbx.clean() # Must have either item_id or email_address set mbx = Mailbox(email_address='XXX') self.assertEqual(hash(mbx), hash('xxx')) mbx.item_id = 'YYY' self.assertEqual(hash(mbx), hash('YYY')) # If we have an item_id, use that for uniqueness def test_body(self): # Test that string formatting a Body and HTMLBody instance works and keeps the type self.assertEqual(str(Body('foo')), 'foo') self.assertEqual(str(Body('%s') % 'foo'), 'foo') self.assertEqual(str(Body('{}').format('foo')), 'foo') self.assertIsInstance(Body('foo'), Body) self.assertIsInstance(Body('') + 'foo', Body) foo = Body('') foo += 'foo' self.assertIsInstance(foo, Body) self.assertIsInstance(Body('%s') % 'foo', Body) self.assertIsInstance(Body('{}').format('foo'), Body) self.assertEqual(str(HTMLBody('foo')), 'foo') self.assertEqual(str(HTMLBody('%s') % 'foo'), 'foo') self.assertEqual(str(HTMLBody('{}').format('foo')), 'foo') self.assertIsInstance(HTMLBody('foo'), HTMLBody) self.assertIsInstance(HTMLBody('') + 'foo', HTMLBody) foo = HTMLBody('') foo += 'foo' self.assertIsInstance(foo, HTMLBody) self.assertIsInstance(HTMLBody('%s') % 'foo', HTMLBody) self.assertIsInstance(HTMLBody('{}').format('foo'), HTMLBody) def test_invalid_attribute(self): # For a random EWSElement subclass, test that we cannot assign an unsupported attribute item = ItemId(id='xxx', changekey='yyy') with self.assertRaises(AttributeError) as e: item.invalid_attr = 123 self.assertEqual( e.exception.args[0], "'invalid_attr' is not a valid attribute. See ItemId.FIELDS for valid field names" ) exchangelib-4.6.1/tests/test_protocol.py000066400000000000000000000666121414601472700204300ustar00rootroot00000000000000import datetime import os import pickle import socket import tempfile import warnings try: import zoneinfo except ImportError: from backports import zoneinfo import psutil import requests_mock from exchangelib.credentials import Credentials from exchangelib.configuration import Configuration from exchangelib.items import CalendarItem from exchangelib.errors import SessionPoolMinSizeReached, ErrorNameResolutionNoResults, ErrorAccessDenied, \ TransportError, SessionPoolMaxSizeReached, TimezoneDefinitionInvalidForYear from exchangelib.properties import TimeZone, RoomList, FreeBusyView, AlternateId, ID_FORMATS, EWS_ID, \ SearchableMailbox, FailedMailbox, Mailbox, DLMailbox from exchangelib.protocol import Protocol, BaseProtocol, NoVerifyHTTPAdapter, FailFast from exchangelib.services import GetServerTimeZones, GetRoomLists, GetRooms, ResolveNames, GetSearchableMailboxes from exchangelib.settings import OofSettings from exchangelib.transport import NOAUTH, NTLM from exchangelib.version import Build, Version from exchangelib.winzone import CLDR_TO_MS_TIMEZONE_MAP from .common import EWSTest, get_random_datetime_range, get_random_string, RANDOM_DATE_MIN, RANDOM_DATE_MAX class ProtocolTest(EWSTest): def test_pickle(self): # Test that we can pickle, repr and str Protocols o = Protocol(config=Configuration( service_endpoint='https://example.com/Foo.asmx', credentials=Credentials(get_random_string(8), get_random_string(8)), auth_type=NTLM, version=Version(Build(15, 1)), retry_policy=FailFast() )) pickled_o = pickle.dumps(o) unpickled_o = pickle.loads(pickled_o) self.assertIsInstance(unpickled_o, type(o)) self.assertEqual(repr(o), repr(unpickled_o)) self.assertEqual(str(o), str(unpickled_o)) @requests_mock.mock() def test_session(self, m): protocol = Protocol(config=Configuration( service_endpoint='https://example.com/Foo.asmx', credentials=Credentials(get_random_string(8), get_random_string(8)), auth_type=NTLM, version=Version(Build(15, 1)), retry_policy=FailFast() )) session = protocol.create_session() new_session = protocol.renew_session(session) self.assertNotEqual(id(session), id(new_session)) @requests_mock.mock() def test_protocol_instance_caching(self, m): # Verify that we get the same Protocol instance for the same combination of (endpoint, credentials) user, password = get_random_string(8), get_random_string(8) config = Configuration( service_endpoint='https://example.com/Foo.asmx', credentials=Credentials(user, password), auth_type=NTLM, version=Version(Build(15, 1)), retry_policy=FailFast() ) # Test CachingProtocol.__getitem__ with self.assertRaises(KeyError): _ = Protocol[config] base_p = Protocol(config=config) self.assertEqual(base_p, Protocol[config][0]) # Make sure we always return the same item when creating a Protocol with the same endpoint and creds for _ in range(10): p = Protocol(config=config) self.assertEqual(base_p, p) self.assertEqual(id(base_p), id(p)) self.assertEqual(hash(base_p), hash(p)) self.assertEqual(id(base_p._session_pool), id(p._session_pool)) # Test CachingProtocol.__delitem__ del Protocol[config] with self.assertRaises(KeyError): _ = Protocol[config] # Make sure we get a fresh instance after we cleared the cache p = Protocol(config=config) self.assertNotEqual(base_p, p) Protocol.clear_cache() def test_close(self): # Don't use example.com here - it does not resolve or answer on all ISPs proc = psutil.Process() ip_addresses = {info[4][0] for info in socket.getaddrinfo( 'httpbin.org', 80, socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_IP )} def conn_count(): return len([p for p in proc.connections() if p.raddr[0] in ip_addresses]) self.assertGreater(len(ip_addresses), 0) protocol = Protocol(config=Configuration( service_endpoint='http://httpbin.org', credentials=Credentials(get_random_string(8), get_random_string(8)), auth_type=NOAUTH, version=Version(Build(15, 1)), retry_policy=FailFast(), max_connections=3 )) # Merely getting a session should not create conections session = protocol.get_session() self.assertEqual(conn_count(), 0) # Open one URL - we have 1 connection session.get('http://httpbin.org') self.assertEqual(conn_count(), 1) # Open the same URL - we should still have 1 connection session.get('http://httpbin.org') self.assertEqual(conn_count(), 1) # Open some more connections s2 = protocol.get_session() s2.get('http://httpbin.org') s3 = protocol.get_session() s3.get('http://httpbin.org') self.assertEqual(conn_count(), 3) # Releasing the sessions does not close the connections protocol.release_session(session) protocol.release_session(s2) protocol.release_session(s3) self.assertEqual(conn_count(), 3) # But closing explicitly does protocol.close() self.assertEqual(conn_count(), 0) def test_decrease_poolsize(self): # Test increasing and decreasing the pool size max_connections = 3 protocol = Protocol(config=Configuration( service_endpoint='https://example.com/Foo.asmx', credentials=Credentials(get_random_string(8), get_random_string(8)), auth_type=NTLM, version=Version(Build(15, 1)), retry_policy=FailFast(), max_connections=max_connections, )) self.assertEqual(protocol._session_pool.qsize(), 0) self.assertEqual(protocol.session_pool_size, 0) protocol.increase_poolsize() protocol.increase_poolsize() protocol.increase_poolsize() with self.assertRaises(SessionPoolMaxSizeReached): protocol.increase_poolsize() self.assertEqual(protocol._session_pool.qsize(), max_connections) self.assertEqual(protocol.session_pool_size, max_connections) protocol.decrease_poolsize() protocol.decrease_poolsize() with self.assertRaises(SessionPoolMinSizeReached): protocol.decrease_poolsize() self.assertEqual(protocol._session_pool.qsize(), 1) def test_get_timezones(self): ws = GetServerTimeZones(self.account.protocol) data = ws.call() self.assertAlmostEqual(len(list(data)), 130, delta=30, msg=data) # Test shortcut self.assertAlmostEqual(len(list(self.account.protocol.get_timezones())), 130, delta=30, msg=data) # Test translation to TimeZone objects for _, _, periods, transitions, transitionsgroups in self.account.protocol.get_timezones( return_full_timezone_data=True): try: TimeZone.from_server_timezone( periods=periods, transitions=transitions, transitionsgroups=transitionsgroups, for_year=2018, ) except TimezoneDefinitionInvalidForYear: pass def test_get_free_busy_info(self): tz = self.account.default_timezone server_timezones = list(self.account.protocol.get_timezones(return_full_timezone_data=True)) start = datetime.datetime.now(tz=tz) end = datetime.datetime.now(tz=tz) + datetime.timedelta(hours=6) accounts = [(self.account, 'Organizer', False)] with self.assertRaises(ValueError): self.account.protocol.get_free_busy_info(accounts=[(123, 'XXX', 'XXX')], start=0, end=0) with self.assertRaises(ValueError): self.account.protocol.get_free_busy_info(accounts=[(self.account, 'XXX', 'XXX')], start=0, end=0) with self.assertRaises(ValueError): self.account.protocol.get_free_busy_info(accounts=[(self.account, 'Organizer', 'XXX')], start=0, end=0) with self.assertRaises(ValueError): self.account.protocol.get_free_busy_info(accounts=accounts, start=end, end=start) with self.assertRaises(ValueError): self.account.protocol.get_free_busy_info(accounts=accounts, start=start, end=end, merged_free_busy_interval='XXX') with self.assertRaises(ValueError): self.account.protocol.get_free_busy_info(accounts=accounts, start=start, end=end, requested_view='XXX') for view_info in self.account.protocol.get_free_busy_info(accounts=accounts, start=start, end=end): self.assertIsInstance(view_info, FreeBusyView) self.assertIsInstance(view_info.working_hours_timezone, TimeZone) ms_id = view_info.working_hours_timezone.to_server_timezone(server_timezones, start.year) self.assertIn(ms_id, {t[0] for t in CLDR_TO_MS_TIMEZONE_MAP.values()}) # Test account as simple email for view_info in self.account.protocol.get_free_busy_info( accounts=[(self.account.primary_smtp_address, 'Organizer', False)], start=start, end=end ): self.assertIsInstance(view_info, FreeBusyView) def test_get_roomlists(self): # The test server is not guaranteed to have any room lists which makes this test less useful ws = GetRoomLists(self.account.protocol) roomlists = ws.call() self.assertEqual(list(roomlists), []) # Test shortcut self.assertEqual(list(self.account.protocol.get_roomlists()), []) def test_get_roomlists_parsing(self): # Test static XML since server has no roomlists xml = b'''\ NoError Roomlist roomlist1@example.com SMTP PublicDL Roomlist roomlist2@example.com SMTP PublicDL ''' ws = GetRoomLists(self.account.protocol) self.assertSetEqual( {rl.email_address for rl in ws.parse(xml)}, {'roomlist1@example.com', 'roomlist2@example.com'} ) def test_get_rooms(self): # The test server is not guaranteed to have any rooms or room lists which makes this test less useful roomlist = RoomList(email_address='my.roomlist@example.com') ws = GetRooms(self.account.protocol) with self.assertRaises(ErrorNameResolutionNoResults): list(ws.call(roomlist=roomlist)) # Test shortcut with self.assertRaises(ErrorNameResolutionNoResults): list(self.account.protocol.get_rooms('my.roomlist@example.com')) def test_get_rooms_parsing(self): # Test static XML since server has no rooms xml = b'''\ NoError room1 room1@example.com SMTP Mailbox room2 room2@example.com SMTP Mailbox ''' ws = GetRooms(self.account.protocol) self.assertSetEqual( {r.email_address for r in ws.parse(xml)}, {'room1@example.com', 'room2@example.com'} ) def test_resolvenames(self): with self.assertRaises(ValueError): self.account.protocol.resolve_names(names=[], search_scope='XXX') with self.assertRaises(ValueError): self.account.protocol.resolve_names(names=[], shape='XXX') self.assertGreaterEqual( self.account.protocol.resolve_names(names=['xxx@example.com']), [] ) self.assertEqual( self.account.protocol.resolve_names(names=[self.account.primary_smtp_address]), [Mailbox(email_address=self.account.primary_smtp_address)] ) # Test something that's not an email self.assertEqual( self.account.protocol.resolve_names(names=['foo\\bar']), [ErrorNameResolutionNoResults('No results were found.')] ) # Test return_full_contact_data mailbox, contact = self.account.protocol.resolve_names( names=[self.account.primary_smtp_address], return_full_contact_data=True )[0] self.assertEqual( mailbox, Mailbox(email_address=self.account.primary_smtp_address) ) self.assertListEqual( [e.email.replace('SMTP:', '') for e in contact.email_addresses if e.label == 'EmailAddress1'], [self.account.primary_smtp_address] ) def test_resolvenames_parsing(self): # Test static XML since server has no roomlists xml = b'''\ Multiple results were found. ErrorNameResolutionMultipleResults 0 John Doe anne@example.com SMTP Mailbox John Deer john@example.com SMTP Mailbox ''' ws = ResolveNames(self.account.protocol) ws.return_full_contact_data = False self.assertSetEqual( {m.email_address for m in ws.parse(xml)}, {'anne@example.com', 'john@example.com'} ) def test_get_searchable_mailboxes(self): # Insufficient privileges for the test account, so let's just test the exception with self.assertRaises(ErrorAccessDenied): self.account.protocol.get_searchable_mailboxes('non_existent_distro@example.com') xml = b'''\ NoError 33a408fe-2574-4e3b-49f5-5e1e000a3035 LOLgroup@contoso.com false LOLgroup true /o=First/ou=Exchange(FYLT)/cn=Recipients/cn=81213b958a0b5295b13b3f02b812bf1bc-LOLgroup FAILgroup@contoso.com 123 Catastrophic Failure true ''' ws = GetSearchableMailboxes(protocol=self.account.protocol) self.assertListEqual(list(ws.parse(xml)), [ SearchableMailbox( guid='33a408fe-2574-4e3b-49f5-5e1e000a3035', primary_smtp_address='LOLgroup@contoso.com', is_external=False, external_email=None, display_name='LOLgroup', is_membership_group=True, reference_id='/o=First/ou=Exchange(FYLT)/cn=Recipients/cn=81213b958a0b5295b13b3f02b812bf1bc-LOLgroup', ), FailedMailbox( mailbox='FAILgroup@contoso.com', error_code=123, error_message='Catastrophic Failure', is_archive=True, ), ]) def test_expanddl(self): with self.assertRaises(ErrorNameResolutionNoResults): self.account.protocol.expand_dl('non_existent_distro@example.com') with self.assertRaises(ErrorNameResolutionNoResults): self.account.protocol.expand_dl( DLMailbox(email_address='non_existent_distro@example.com', mailbox_type='PublicDL') ) def test_oof_settings(self): # First, ensure a common starting point utc = zoneinfo.ZoneInfo('UTC') self.account.oof_settings = OofSettings( state=OofSettings.DISABLED, start=datetime.datetime.combine(RANDOM_DATE_MIN, datetime.time.min, tzinfo=utc), end=datetime.datetime.combine(RANDOM_DATE_MAX, datetime.time.max, tzinfo=utc), ) oof = OofSettings( state=OofSettings.ENABLED, external_audience='None', internal_reply="I'm on holidays. See ya guys!", external_reply='Dear Sir, your email has now been deleted.', ) self.account.oof_settings = oof self.assertEqual(self.account.oof_settings, oof) oof = OofSettings( state=OofSettings.ENABLED, external_audience='Known', internal_reply='XXX', external_reply='YYY', ) self.account.oof_settings = oof self.assertEqual(self.account.oof_settings, oof) # Scheduled duration must not be in the past tz = self.account.default_timezone start, end = get_random_datetime_range(start_date=datetime.datetime.now(tz).date()) oof = OofSettings( state=OofSettings.SCHEDULED, external_audience='Known', internal_reply="I'm in the pub. See ya guys!", external_reply="I'm having a business dinner in town", start=start, end=end, ) self.account.oof_settings = oof self.assertEqual(self.account.oof_settings, oof) oof = OofSettings( state=OofSettings.DISABLED, start=start, end=end, ) self.account.oof_settings = oof # TODO: For some reason, disabling OOF does not always work. Don't assert because we want a stable test suite if self.account.oof_settings != oof: self.skipTest('Disabling OOF did not work') def test_oof_settings_validation(self): utc = zoneinfo.ZoneInfo('UTC') with self.assertRaises(ValueError): # Needs a start and end OofSettings( state=OofSettings.SCHEDULED, ).clean(version=None) with self.assertRaises(ValueError): # Start must be before end OofSettings( state=OofSettings.SCHEDULED, start=datetime.datetime(2100, 12, 1, tzinfo=utc), end=datetime.datetime(2100, 11, 1, tzinfo=utc), ).clean(version=None) with self.assertRaises(ValueError): # End must be in the future OofSettings( state=OofSettings.SCHEDULED, start=datetime.datetime(2000, 11, 1, tzinfo=utc), end=datetime.datetime(2000, 12, 1, tzinfo=utc), ).clean(version=None) with self.assertRaises(ValueError): # Must have an internal and external reply OofSettings( state=OofSettings.SCHEDULED, start=datetime.datetime(2100, 11, 1, tzinfo=utc), end=datetime.datetime(2100, 12, 1, tzinfo=utc), ).clean(version=None) def test_convert_id(self): i = 'AAMkADQyYzZmYmUxLTJiYjItNDg2Ny1iMzNjLTIzYWE1NDgxNmZhNABGAAAAAADUebQDarW2Q7G2Ji8hKofPBwAl9iKCsfCfSa9cmjh' \ '+JCrCAAPJcuhjAAB0l+JSKvzBRYP+FXGewReXAABj6DrMAAA=' for fmt in ID_FORMATS: res = list(self.account.protocol.convert_ids( [AlternateId(id=i, format=EWS_ID, mailbox=self.account.primary_smtp_address)], destination_format=fmt)) self.assertEqual(len(res), 1) self.assertEqual(res[0].format, fmt) def test_sessionpool(self): # First, empty the calendar start = datetime.datetime(2011, 10, 12, 8, tzinfo=self.account.default_timezone) end = datetime.datetime(2011, 10, 12, 10, tzinfo=self.account.default_timezone) self.account.calendar.filter(start__lt=end, end__gt=start, categories__contains=self.categories).delete() items = [] for i in range(75): subject = 'Test Subject %s' % i item = CalendarItem( start=start, end=end, subject=subject, categories=self.categories, ) items.append(item) return_ids = self.account.calendar.bulk_create(items=items) self.assertEqual(len(return_ids), len(items)) ids = self.account.calendar.filter(start__lt=end, end__gt=start, categories__contains=self.categories) \ .values_list('id', 'changekey') self.assertEqual(ids.count(), len(items)) def test_disable_ssl_verification(self): # Test that we can make requests when SSL verification is turned off. I don't know how to mock TLS responses if not self.verify_ssl: # We can only run this test if we haven't already disabled TLS self.skipTest('TLS verification already disabled') default_adapter_cls = BaseProtocol.HTTP_ADAPTER_CLS # Just test that we can query self.account.root.all().exists() # Smash TLS verification using an untrusted certificate with tempfile.NamedTemporaryFile() as f: f.write(b'''\ -----BEGIN CERTIFICATE----- MIIENzCCAx+gAwIBAgIJAOYfYfw7NCOcMA0GCSqGSIb3DQEBBQUAMIGxMQswCQYD VQQGEwJVUzERMA8GA1UECAwITWFyeWxhbmQxFDASBgNVBAcMC0ZvcmVzdCBIaWxs MScwJQYDVQQKDB5UaGUgQXBhY2hlIFNvZnR3YXJlIEZvdW5kYXRpb24xFjAUBgNV BAsMDUFwYWNoZSBUaHJpZnQxEjAQBgNVBAMMCWxvY2FsaG9zdDEkMCIGCSqGSIb3 DQEJARYVZGV2QHRocmlmdC5hcGFjaGUub3JnMB4XDTE0MDQwNzE4NTgwMFoXDTIy MDYyNDE4NTgwMFowgbExCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNYXJ5bGFuZDEU MBIGA1UEBwwLRm9yZXN0IEhpbGwxJzAlBgNVBAoMHlRoZSBBcGFjaGUgU29mdHdh cmUgRm91bmRhdGlvbjEWMBQGA1UECwwNQXBhY2hlIFRocmlmdDESMBAGA1UEAwwJ bG9jYWxob3N0MSQwIgYJKoZIhvcNAQkBFhVkZXZAdGhyaWZ0LmFwYWNoZS5vcmcw ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqE9TE9wEXp5LRtLQVDSGQ GV78+7ZtP/I/ZaJ6Q6ZGlfxDFvZjFF73seNhAvlKlYm/jflIHYLnNOCySN8I2Xw6 L9MbC+jvwkEKfQo4eDoxZnOZjNF5J1/lZtBeOowMkhhzBMH1Rds351/HjKNg6ZKg 2Cldd0j7HbDtEixOLgLbPRpBcaYrLrNMasf3Hal+x8/b8ue28x93HSQBGmZmMIUw AinEu/fNP4lLGl/0kZb76TnyRpYSPYojtS6CnkH+QLYnsRREXJYwD1Xku62LipkX wCkRTnZ5nUsDMX6FPKgjQFQCWDXG/N096+PRUQAChhrXsJ+gF3NqWtDmtrhVQF4n AgMBAAGjUDBOMB0GA1UdDgQWBBQo8v0wzQPx3EEexJPGlxPK1PpgKjAfBgNVHSME GDAWgBQo8v0wzQPx3EEexJPGlxPK1PpgKjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3 DQEBBQUAA4IBAQBGFRiJslcX0aJkwZpzTwSUdgcfKbpvNEbCNtVohfQVTI4a/oN5 U+yqDZJg3vOaOuiAZqyHcIlZ8qyesCgRN314Tl4/JQ++CW8mKj1meTgo5YFxcZYm T9vsI3C+Nzn84DINgI9mx6yktIt3QOKZRDpzyPkUzxsyJ8J427DaimDrjTR+fTwD 1Dh09xeeMnSa5zeV1HEDyJTqCXutLetwQ/IyfmMBhIx+nvB5f67pz/m+Dv6V0r3I p4HCcdnDUDGJbfqtoqsAATQQWO+WWuswB6mOhDbvPTxhRpZq6AkgWqv4S+u3M2GO r5p9FrBgavAw5bKO54C0oQKpN/5fta5l6Ws0 -----END CERTIFICATE-----''') try: os.environ['REQUESTS_CA_BUNDLE'] = f.name # Setting the credentials is just an easy way of resetting the session pool. This will let requests # pick up the new environment variable. Now the request should fail self.account.protocol.credentials = self.account.protocol.credentials with warnings.catch_warnings(): warnings.simplefilter("ignore") # Ignore ResourceWarning for unclosed socket. It does get closed. with self.assertRaises(TransportError) as e: self.account.root.all().exists() self.assertIn('SSLError', e.exception.args[0]) # Disable insecure TLS warnings with warnings.catch_warnings(): warnings.simplefilter("ignore") # Make sure we can handle TLS validation errors when using the custom adapter BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter self.account.protocol.credentials = self.account.protocol.credentials self.account.root.all().exists() # Test that the custom adapter also works when validation is OK again del os.environ['REQUESTS_CA_BUNDLE'] self.account.protocol.credentials = self.account.protocol.credentials self.account.root.all().exists() finally: # Reset environment and connections os.environ.pop('REQUESTS_CA_BUNDLE', None) # May already have been deleted BaseProtocol.HTTP_ADAPTER_CLS = default_adapter_cls self.account.protocol.credentials = self.account.protocol.credentials exchangelib-4.6.1/tests/test_queryset.py000066400000000000000000000064361414601472700204460ustar00rootroot00000000000000# coding=utf-8 from collections import namedtuple from exchangelib.folders import FolderCollection from exchangelib.folders import Inbox from exchangelib.queryset import QuerySet, Q from .common import TimedTestCase class QuerySetTest(TimedTestCase): def test_magic(self): self.assertEqual( str( QuerySet(folder_collection=FolderCollection(account=None, folders=[Inbox(root='XXX', name='FooBox')])) ), 'QuerySet(q=Q(), folders=[Inbox (FooBox)])' ) def test_from_folder(self): MockRoot = namedtuple('Root', ['account']) folder = Inbox(root=MockRoot(account='XXX')) self.assertIsInstance(folder.all(), QuerySet) self.assertIsInstance(folder.none(), QuerySet) self.assertIsInstance(folder.filter(subject='foo'), QuerySet) self.assertIsInstance(folder.exclude(subject='foo'), QuerySet) def test_queryset_copy(self): qs = QuerySet(folder_collection=FolderCollection(account=None, folders=[Inbox(root='XXX')])) qs.q = Q() qs.only_fields = ('a', 'b') qs.order_fields = ('c', 'd') qs.return_format = QuerySet.NONE # Initially, immutable items have the same id() new_qs = qs._copy_self() self.assertNotEqual(id(qs), id(new_qs)) self.assertEqual(id(qs.folder_collection), id(new_qs.folder_collection)) self.assertNotEqual(id(qs.q), id(new_qs.q)) self.assertEqual(qs.q, new_qs.q) self.assertEqual(id(qs.only_fields), id(new_qs.only_fields)) self.assertEqual(qs.only_fields, new_qs.only_fields) self.assertEqual(id(qs.order_fields), id(new_qs.order_fields)) self.assertEqual(qs.order_fields, new_qs.order_fields) self.assertEqual(id(qs.return_format), id(new_qs.return_format)) self.assertEqual(qs.return_format, new_qs.return_format) # Set the same values, forcing a new id() new_qs.q = Q() new_qs.only_fields = ('a', 'b') new_qs.order_fields = ('c', 'd') new_qs.return_format = QuerySet.NONE self.assertNotEqual(id(qs), id(new_qs)) self.assertEqual(id(qs.folder_collection), id(new_qs.folder_collection)) self.assertNotEqual(id(qs.q), id(new_qs.q)) self.assertEqual(qs.q, new_qs.q) self.assertEqual(qs.only_fields, new_qs.only_fields) self.assertEqual(qs.order_fields, new_qs.order_fields) self.assertEqual(qs.return_format, new_qs.return_format) # Set the new values, forcing a new id() new_qs.q = Q(foo=5) new_qs.only_fields = ('c', 'd') new_qs.order_fields = ('e', 'f') new_qs.return_format = QuerySet.VALUES self.assertNotEqual(id(qs), id(new_qs)) self.assertEqual(id(qs.folder_collection), id(new_qs.folder_collection)) self.assertNotEqual(id(qs.q), id(new_qs.q)) self.assertNotEqual(qs.q, new_qs.q) self.assertNotEqual(id(qs.only_fields), id(new_qs.only_fields)) self.assertNotEqual(qs.only_fields, new_qs.only_fields) self.assertNotEqual(id(qs.order_fields), id(new_qs.order_fields)) self.assertNotEqual(qs.order_fields, new_qs.order_fields) self.assertNotEqual(id(qs.return_format), id(new_qs.return_format)) self.assertNotEqual(qs.return_format, new_qs.return_format) exchangelib-4.6.1/tests/test_recurrence.py000066400000000000000000000045741414601472700207230ustar00rootroot00000000000000import datetime from exchangelib.fields import MONDAY, FEBRUARY, AUGUST, SECOND, LAST, WEEKEND_DAY from exchangelib.recurrence import Recurrence, AbsoluteYearlyPattern, RelativeYearlyPattern, AbsoluteMonthlyPattern, \ RelativeMonthlyPattern, WeeklyPattern, DailyPattern, NoEndPattern, EndDatePattern, NumberedPattern from .common import TimedTestCase class RecurrenceTest(TimedTestCase): def test_magic(self): pattern = AbsoluteYearlyPattern(month=FEBRUARY, day_of_month=28) self.assertEqual(str(pattern), 'Occurs on day 28 of February') pattern = RelativeYearlyPattern(month=AUGUST, week_number=SECOND, weekday=MONDAY) self.assertEqual(str(pattern), 'Occurs on weekday Monday in the Second week of August') pattern = AbsoluteMonthlyPattern(interval=3, day_of_month=31) self.assertEqual(str(pattern), 'Occurs on day 31 of every 3 month(s)') pattern = RelativeMonthlyPattern(interval=2, week_number=LAST, weekday=5) self.assertEqual(str(pattern), 'Occurs on weekday Friday in the Last week of every 2 month(s)') pattern = WeeklyPattern(interval=4, weekdays=WEEKEND_DAY, first_day_of_week=7) self.assertEqual(str(pattern), 'Occurs on weekdays WeekendDay of every 4 week(s) where the first day of the week is Sunday') pattern = DailyPattern(interval=6) self.assertEqual(str(pattern), 'Occurs every 6 day(s)') def test_validation(self): p = DailyPattern(interval=3) d_start = datetime.date(2017, 9, 1) d_end = datetime.date(2017, 9, 7) with self.assertRaises(ValueError): Recurrence(pattern=p, boundary='foo', start='bar') # Specify *either* boundary *or* start, end and number with self.assertRaises(ValueError): Recurrence(pattern=p, start='foo', end='bar', number='baz') # number is invalid when end is present with self.assertRaises(ValueError): Recurrence(pattern=p, end='bar', number='baz') # Must have start r = Recurrence(pattern=p, start=d_start) self.assertEqual(r.boundary, NoEndPattern(start=d_start)) r = Recurrence(pattern=p, start=d_start, end=d_end) self.assertEqual(r.boundary, EndDatePattern(start=d_start, end=d_end)) r = Recurrence(pattern=p, start=d_start, number=1) self.assertEqual(r.boundary, NumberedPattern(start=d_start, number=1)) exchangelib-4.6.1/tests/test_restriction.py000066400000000000000000000203121414601472700211170ustar00rootroot00000000000000import datetime try: import zoneinfo except ImportError: from backports import zoneinfo from exchangelib.folders import Calendar, Root from exchangelib.queryset import Q from exchangelib.restriction import Restriction from exchangelib.util import xml_to_str from exchangelib.version import Build, Version, EXCHANGE_2007 from .common import TimedTestCase, mock_account, mock_protocol class RestrictionTest(TimedTestCase): def test_magic(self): self.assertEqual(str(Q()), 'Q()') def test_q(self): version = Version(build=EXCHANGE_2007) account = mock_account(version=version, protocol=mock_protocol(version=version, service_endpoint='example.com')) root = Root(account=account) tz = zoneinfo.ZoneInfo('Europe/Copenhagen') start = datetime.datetime(2020, 9, 26, 8, 0, 0, tzinfo=tz) end = datetime.datetime(2020, 9, 26, 11, 0, 0, tzinfo=tz) result = '''\ ''' q = Q(Q(categories__contains='FOO') | Q(categories__contains='BAR'), start__lt=end, end__gt=start) r = Restriction(q, folders=[Calendar(root=root)], applies_to=Restriction.ITEMS) self.assertEqual(str(r), ''.join(s.lstrip() for s in result.split('\n'))) # Test empty Q q = Q() self.assertEqual(q.to_xml(folders=[Calendar()], version=version, applies_to=Restriction.ITEMS), None) with self.assertRaises(ValueError): Restriction(q, folders=[Calendar(root=root)], applies_to=Restriction.ITEMS) # Test validation with self.assertRaises(ValueError): Q(datetime_created__range=(1,)) # Must have exactly 2 args with self.assertRaises(ValueError): Q(datetime_created__range=(1, 2, 3)) # Must have exactly 2 args with self.assertRaises(TypeError): Q(datetime_created=Build(15, 1)).clean(version=Version(build=EXCHANGE_2007)) # Must be serializable with self.assertRaises(ValueError): # Must be a timezone-aware datetime Q(datetime_created=datetime.datetime(2017, 1, 1)).clean(version=Version(build=EXCHANGE_2007)) with self.assertRaises(ValueError): Q(categories__contains=[[1, 2], [3, 4]]).clean(version=Version(build=EXCHANGE_2007)) # Must be single value def test_q_expr(self): self.assertEqual(Q().expr(), None) self.assertEqual((~Q()).expr(), None) self.assertEqual(Q(x=5).expr(), 'x == 5') self.assertEqual((~Q(x=5)).expr(), 'x != 5') q = (Q(b__contains='a', x__contains=5) | Q(~Q(a__contains='c'), f__gt=3, c=6)) & ~Q(y=9, z__contains='b') self.assertEqual( str(q), # str() calls expr() "((b contains 'a' AND x contains 5) OR (NOT a contains 'c' AND c == 6 AND f > 3)) " "AND NOT (y == 9 AND z contains 'b')" ) self.assertEqual( repr(q), "Q('AND', Q('OR', Q('AND', Q(b contains 'a'), Q(x contains 5)), Q('AND', Q('NOT', Q(a contains 'c')), " "Q(c == 6), Q(f > 3))), Q('NOT', Q('AND', Q(y == 9), Q(z contains 'b'))))" ) # Test simulated IN expression in_q = Q(foo__in=[1, 2, 3]) self.assertEqual(in_q.conn_type, Q.OR) self.assertEqual(len(in_q.children), 3) def test_q_inversion(self): version = Version(build=EXCHANGE_2007) account = mock_account(version=version, protocol=mock_protocol(version=version, service_endpoint='example.com')) root = Root(account=account) self.assertEqual((~Q(foo=5)).op, Q.NE) self.assertEqual((~Q(foo__not=5)).op, Q.EQ) self.assertEqual((~Q(foo__lt=5)).op, Q.GTE) self.assertEqual((~Q(foo__lte=5)).op, Q.GT) self.assertEqual((~Q(foo__gt=5)).op, Q.LTE) self.assertEqual((~Q(foo__gte=5)).op, Q.LT) # Test not not Q on a non-leaf self.assertEqual(Q(foo__contains=('bar', 'baz')).conn_type, Q.AND) self.assertEqual((~Q(foo__contains=('bar', 'baz'))).conn_type, Q.NOT) self.assertEqual((~~Q(foo__contains=('bar', 'baz'))).conn_type, Q.AND) self.assertEqual(Q(foo__contains=('bar', 'baz')), ~~Q(foo__contains=('bar', 'baz'))) # Test generated XML of 'Not' statement when there is only one child. Skip 't:And' between 't:Not' and 't:Or'. result = '''\ ''' q = ~(Q(subject='bar') | Q(subject='baz')) self.assertEqual( xml_to_str(q.to_xml(folders=[Calendar(root=root)], version=version, applies_to=Restriction.ITEMS)), ''.join(s.lstrip() for s in result.split('\n')) ) def test_q_boolean_ops(self): self.assertEqual((Q(foo=5) & Q(foo=6)).conn_type, Q.AND) self.assertEqual((Q(foo=5) | Q(foo=6)).conn_type, Q.OR) def test_q_failures(self): with self.assertRaises(ValueError): # Invalid value Q(foo=None).clean(version=Version(build=EXCHANGE_2007)) def test_q_never(self): # Tests Q with conn_type NEVER. This is for cases where the user makes queries that would return an empty # result, but the server does not support this type of query. The only example so far is 'foo__in=[]' which # should always match nothing. self.assertEqual(Q(foo__in=[]), Q(conn_type=Q.NEVER)) # __in with empty sequence should translate to NEVER self.assertTrue(Q(foo__in=[]).is_never()) # Test the flag self.assertEqual(~Q(foo__in=[]), Q()) # Negation should translate to the no-op self.assertTrue(~Q(foo__in=[]).is_empty()) # Negation should translate to a no-op # Test in combination with AND and OR self.assertEqual(Q(foo__in=[], bar='baz'), Q(conn_type=Q.NEVER)) # NEVER removes all other args self.assertEqual(Q(foo__in=[]) & Q(bar='baz'), Q(conn_type=Q.NEVER)) # NEVER removes all other args self.assertEqual(Q(foo__in=[]) | Q(bar='baz'), Q(bar='baz')) # OR removes all 'never' args def test_q_simplification(self): self.assertEqual(Q(foo='bar') & Q(), Q(foo='bar')) self.assertEqual(Q() & Q(foo='bar'), Q(foo='bar')) self.assertEqual(Q('foo') & Q(), Q('foo')) self.assertEqual(Q() & Q('foo'), Q('foo')) def test_q_querystring(self): self.assertEqual(Q('this is a QS').expr(), 'this is a QS') self.assertEqual(Q(Q('this is a QS')), Q('this is a QS')) self.assertEqual(Q(Q(Q(Q('this is a QS')))), Q('this is a QS')) with self.assertRaises(ValueError): Q('this is a QS') & Q(foo='bar') with self.assertRaises(ValueError): Q(5) exchangelib-4.6.1/tests/test_services.py000066400000000000000000000234301414601472700204010ustar00rootroot00000000000000import requests_mock from exchangelib.errors import ErrorServerBusy, ErrorNonExistentMailbox, TransportError, MalformedResponseError, \ ErrorInvalidServerVersion, ErrorTooManyObjectsOpened, SOAPError from exchangelib.folders import FolderCollection from exchangelib.protocol import FaultTolerance from exchangelib.services import GetServerTimeZones, GetRoomLists, GetRooms, ResolveNames, FindFolder from exchangelib.util import create_element from exchangelib.version import EXCHANGE_2007, EXCHANGE_2010 from .common import EWSTest, mock_protocol, mock_version, mock_account, get_random_string class ServicesTest(EWSTest): def test_invalid_server_version(self): # Test that we get a client-side error if we call a service that was only implemented in a later version version = mock_version(build=EXCHANGE_2007) account = mock_account(version=version, protocol=mock_protocol(version=version, service_endpoint='example.com')) with self.assertRaises(NotImplementedError): list(GetServerTimeZones(protocol=account.protocol).call()) with self.assertRaises(NotImplementedError): list(GetRoomLists(protocol=account.protocol).call()) with self.assertRaises(NotImplementedError): list(GetRooms(protocol=account.protocol).call('XXX')) def test_error_server_busy(self): # Test that we can parse an exception response via SOAP body xml = b'''\ a:ErrorServerBusy The server cannot service this request right now. Try again later. ErrorServerBusy The server cannot service this request right now. Try again later. 297749 ''' version = mock_version(build=EXCHANGE_2010) ws = GetRoomLists(mock_protocol(version=version, service_endpoint='example.com')) with self.assertRaises(ErrorServerBusy) as e: ws.parse(xml) self.assertEqual(e.exception.back_off, 297.749) # Test that we correctly parse the BackOffMilliseconds value @requests_mock.mock(real_http=True) def test_error_too_many_objects_opened(self, m): # Test that we can parse ErrorTooManyObjectsOpened via ResponseMessage and return version = mock_version(build=EXCHANGE_2010) protocol = mock_protocol(version=version, service_endpoint='example.com') account = mock_account(version=version, protocol=protocol) ws = FindFolder(account=account) xml = b'''\ Too many concurrent connections opened. ErrorTooManyObjectsOpened 0 ''' # Just test that we can parse the error with self.assertRaises(ErrorTooManyObjectsOpened): list(ws.parse(xml)) # Test that it gets converted to an ErrorServerBusy exception. This happens deep inside EWSService methods # so it's easier to only mock the response. self.account.root # Needed to get past the GetFolder request m.post(self.account.protocol.service_endpoint, content=xml) orig_policy = self.account.protocol.config.retry_policy try: self.account.protocol.config.retry_policy = FaultTolerance(max_wait=0) with self.assertRaises(ErrorServerBusy) as e: list(FolderCollection(account=self.account, folders=[self.account.root]).find_folders()) self.assertEqual(e.exception.back_off, None) # ErrorTooManyObjectsOpened has no BackOffMilliseconds value finally: self.account.protocol.config.retry_policy = orig_policy def test_soap_error(self): xml_template = '''\ {faultcode} {faultstring} https://CAS01.example.com/EWS/Exchange.asmx {responsecode} {message} ''' version = mock_version(build=EXCHANGE_2010) protocol = mock_protocol(version=version, service_endpoint='example.com') ws = GetRoomLists(protocol=protocol) xml = xml_template.format( faultcode='YYY', faultstring='AAA', responsecode='XXX', message='ZZZ' ).encode('utf-8') with self.assertRaises(SOAPError) as e: ws.parse(xml) self.assertIn('AAA', e.exception.args[0]) self.assertIn('YYY', e.exception.args[0]) self.assertIn('ZZZ', e.exception.args[0]) xml = xml_template.format( faultcode='ErrorNonExistentMailbox', faultstring='AAA', responsecode='XXX', message='ZZZ' ).encode('utf-8') with self.assertRaises(ErrorNonExistentMailbox) as e: ws.parse(xml) self.assertIn('AAA', e.exception.args[0]) xml = xml_template.format( faultcode='XXX', faultstring='AAA', responsecode='ErrorNonExistentMailbox', message='YYY' ).encode('utf-8') with self.assertRaises(ErrorNonExistentMailbox) as e: ws.parse(xml) self.assertIn('YYY', e.exception.args[0]) # Test bad XML (no body) xml = b'''\ ''' with self.assertRaises(MalformedResponseError): ws.parse(xml) # Test bad XML (no fault) xml = b'''\ ''' with self.assertRaises(SOAPError) as e: ws.parse(xml) self.assertEqual(e.exception.args[0], 'SOAP error code: None string: None actor: None detail: None') def test_element_container(self): ws = ResolveNames(self.account.protocol) xml = b'''\ NoError ''' with self.assertRaises(TransportError) as e: # Missing ResolutionSet elements list(ws.parse(xml)) self.assertIn('ResolutionSet elements in ResponseMessage', e.exception.args[0]) def test_get_elements(self): # Test that we can handle SOAP-level error messages # TODO: The request actually raises ErrorInvalidRequest, but we interpret that to mean a wrong API version and # end up throwing ErrorInvalidServerVersion. We should make a more direct test. svc = ResolveNames(self.account.protocol) with self.assertRaises(ErrorInvalidServerVersion): list(svc._get_elements(create_element('XXX'))) @requests_mock.mock() def test_invalid_soap_response(self, m): m.post(self.account.protocol.service_endpoint, text='XXX') with self.assertRaises(SOAPError): self.account.inbox.all().count() def test_version_renegotiate(self): # Test that we can recover from a wrong API version. This is needed in version guessing and when the # autodiscover response returns a wrong server version for the account old_version = self.account.version.api_version self.account.version.api_version = 'Exchange2016' # Newer EWS versions require a valid value try: list(self.account.inbox.filter(subject=get_random_string(16))) self.assertEqual(old_version, self.account.version.api_version) finally: self.account.version.api_version = old_version exchangelib-4.6.1/tests/test_source.py000066400000000000000000000101211414601472700200470ustar00rootroot00000000000000import flake8.defaults import flake8.main.application from exchangelib.errors import ErrorAccessDenied, ErrorFolderNotFound, ErrorItemNotFound, ErrorInvalidOperation, \ ErrorNoPublicFolderReplicaAvailable from exchangelib.properties import EWSElement from .common import EWSTest, TimedTestCase class StyleTest(TimedTestCase): def test_flake8(self): import exchangelib flake8.defaults.MAX_LINE_LENGTH = 120 app = flake8.main.application.Application() app.run(exchangelib.__path__ + ['-j', '0']) # Multiprocessing doesn't work with parallel tests runners # If this fails, look at stdout for actual error messages self.assertEqual(app.result_count, 0) class CommonTest(EWSTest): def test_magic(self): self.assertIn(self.account.protocol.version.api_version, str(self.account.protocol)) self.assertIn(self.account.protocol.credentials.username, str(self.account.protocol.credentials)) self.assertIn(self.account.primary_smtp_address, str(self.account)) self.assertIn(str(self.account.version.build.major_version), repr(self.account.version)) for item in ( self.account.protocol, self.account.version, ): with self.subTest(item=item): # Just test that these at least don't throw errors repr(item) str(item) for attr in ( 'admin_audit_logs', 'archive_deleted_items', 'archive_inbox', 'archive_msg_folder_root', 'archive_recoverable_items_deletions', 'archive_recoverable_items_purges', 'archive_recoverable_items_root', 'archive_recoverable_items_versions', 'archive_root', 'calendar', 'conflicts', 'contacts', 'conversation_history', 'directory', 'drafts', 'favorites', 'im_contact_list', 'inbox', 'journal', 'junk', 'local_failures', 'msg_folder_root', 'my_contacts', 'notes', 'outbox', 'people_connect', 'public_folders_root', 'quick_contacts', 'recipient_cache', 'recoverable_items_deletions', 'recoverable_items_purges', 'recoverable_items_root', 'recoverable_items_versions', 'search_folders', 'sent', 'server_failures', 'sync_issues', 'tasks', 'todo_search', 'trash', 'voice_mail', ): with self.subTest(attr=attr): # Test distinguished folder shortcuts. Some may raise ErrorAccessDenied try: item = getattr(self.account, attr) except (ErrorAccessDenied, ErrorFolderNotFound, ErrorItemNotFound, ErrorInvalidOperation, ErrorNoPublicFolderReplicaAvailable): continue else: repr(item) str(item) self.assertTrue(item.is_distinguished) def test_from_xml(self): # Test for all EWSElement classes that they handle None as input to from_xml() import exchangelib for mod in (exchangelib.attachments, exchangelib.extended_properties, exchangelib.indexed_properties, exchangelib.folders, exchangelib.items, exchangelib.properties): for k, v in vars(mod).items(): with self.subTest(k=k, v=v): if type(v) is not type: continue if not issubclass(v, EWSElement): continue # from_xml() does not support None input with self.assertRaises(Exception): v.from_xml(elem=None, account=None) exchangelib-4.6.1/tests/test_transport.py000066400000000000000000000140021414601472700206050ustar00rootroot00000000000000from collections import namedtuple import requests import requests_mock from exchangelib.account import Identity, DELEGATE from exchangelib.errors import UnauthorizedError from exchangelib.transport import wrap, get_auth_method_from_response, BASIC, NOAUTH, NTLM, DIGEST from exchangelib.util import PrettyXmlHandler, create_element from .common import TimedTestCase class TransportTest(TimedTestCase): @requests_mock.mock() def test_get_auth_method_from_response(self, m): url = 'http://example.com/noauth' m.get(url, status_code=200) r = requests.get(url) self.assertEqual(get_auth_method_from_response(r), NOAUTH) # No authentication needed url = 'http://example.com/redirect' m.get(url, status_code=302, headers={'location': 'http://contoso.com'}) r = requests.get(url, allow_redirects=False) with self.assertRaises(UnauthorizedError): get_auth_method_from_response(r) # Redirect to another host url = 'http://example.com/relativeredirect' m.get(url, status_code=302, headers={'location': 'http://example.com/'}) r = requests.get(url, allow_redirects=False) with self.assertRaises(UnauthorizedError): get_auth_method_from_response(r) # Redirect to same host url = 'http://example.com/internalerror' m.get(url, status_code=501) r = requests.get(url) with self.assertRaises(UnauthorizedError): get_auth_method_from_response(r) # Non-401 status code url = 'http://example.com/no_auth_headers' m.get(url, status_code=401) r = requests.get(url) with self.assertRaises(UnauthorizedError): get_auth_method_from_response(r) # 401 status code but no auth headers url = 'http://example.com/no_supported_auth' m.get(url, status_code=401, headers={'WWW-Authenticate': 'FANCYAUTH'}) r = requests.get(url) with self.assertRaises(UnauthorizedError): get_auth_method_from_response(r) # 401 status code but no auth headers url = 'http://example.com/basic_auth' m.get(url, status_code=401, headers={'WWW-Authenticate': 'Basic'}) r = requests.get(url) self.assertEqual(get_auth_method_from_response(r), BASIC) url = 'http://example.com/basic_auth_empty_realm' m.get(url, status_code=401, headers={'WWW-Authenticate': 'Basic realm=""'}) r = requests.get(url) self.assertEqual(get_auth_method_from_response(r), BASIC) url = 'http://example.com/basic_auth_realm' m.get(url, status_code=401, headers={'WWW-Authenticate': 'Basic realm="some realm"'}) r = requests.get(url) self.assertEqual(get_auth_method_from_response(r), BASIC) url = 'http://example.com/digest' m.get(url, status_code=401, headers={ 'WWW-Authenticate': 'Digest realm="foo@bar.com", qop="auth,auth-int", nonce="mumble", opaque="bumble"' }) r = requests.get(url) self.assertEqual(get_auth_method_from_response(r), DIGEST) url = 'http://example.com/ntlm' m.get(url, status_code=401, headers={'WWW-Authenticate': 'NTLM'}) r = requests.get(url) self.assertEqual(get_auth_method_from_response(r), NTLM) # Make sure we prefer the most secure auth method if multiple methods are supported url = 'http://example.com/mixed' m.get(url, status_code=401, headers={'WWW-Authenticate': 'Basic realm="X1", Digest realm="X2", NTLM'}) r = requests.get(url) self.assertEqual(get_auth_method_from_response(r), DIGEST) def test_wrap(self): # Test payload wrapper with both delegation, impersonation and timezones MockTZ = namedtuple('EWSTimeZone', ['ms_id']) MockAccount = namedtuple( 'Account', ['access_type', 'identity', 'default_timezone'] ) content = create_element('AAA') api_version = 'BBB' account = MockAccount(access_type=DELEGATE, identity=None, default_timezone=MockTZ('XXX')) wrapped = wrap(content=content, api_version=api_version, timezone=account.default_timezone) self.assertEqual( PrettyXmlHandler.prettify_xml(wrapped), b''' ''') for attr, tag in ( ('primary_smtp_address', 'PrimarySmtpAddress'), ('upn', 'PrincipalName'), ('sid', 'SID'), ('smtp_address', 'SmtpAddress'), ): val = '%s@example.com' % attr account = MockAccount( access_type=DELEGATE, identity=Identity(**{attr: val}), default_timezone=MockTZ('XXX') ) wrapped = wrap( content=content, api_version=api_version, account_to_impersonate=account.identity, timezone=account.default_timezone, ) self.assertEqual( PrettyXmlHandler.prettify_xml(wrapped), ''' {val} '''.format(tag=tag, val=val).encode()) exchangelib-4.6.1/tests/test_util.py000066400000000000000000000324321414601472700175350ustar00rootroot00000000000000import io from itertools import chain import logging import requests import requests_mock from exchangelib.errors import RelativeRedirect, TransportError, RateLimitError, RedirectError, UnauthorizedError,\ CASError from exchangelib.protocol import FailFast, FaultTolerance import exchangelib.util from exchangelib.util import chunkify, peek, get_redirect_url, get_domain, PrettyXmlHandler, to_xml, BOM_UTF8, \ ParseError, post_ratelimited, safe_b64decode, CONNECTION_ERRORS from .common import EWSTest, mock_post, mock_session_exception class UtilTest(EWSTest): def test_chunkify(self): # Test tuple, list, set, range, map, chain and generator seq = [1, 2, 3, 4, 5] self.assertEqual(list(chunkify(seq, chunksize=2)), [[1, 2], [3, 4], [5]]) seq = (1, 2, 3, 4, 6, 7, 9) self.assertEqual(list(chunkify(seq, chunksize=3)), [(1, 2, 3), (4, 6, 7), (9,)]) seq = {1, 2, 3, 4, 5} self.assertEqual(list(chunkify(seq, chunksize=2)), [[1, 2], [3, 4], [5]]) seq = range(5) self.assertEqual(list(chunkify(seq, chunksize=2)), [range(0, 2), range(2, 4), range(4, 5)]) seq = map(int, range(5)) self.assertEqual(list(chunkify(seq, chunksize=2)), [[0, 1], [2, 3], [4]]) seq = chain(*[[i] for i in range(5)]) self.assertEqual(list(chunkify(seq, chunksize=2)), [[0, 1], [2, 3], [4]]) seq = (i for i in range(5)) self.assertEqual(list(chunkify(seq, chunksize=2)), [[0, 1], [2, 3], [4]]) def test_peek(self): # Test peeking into various sequence types # tuple is_empty, seq = peek(()) self.assertEqual((is_empty, list(seq)), (True, [])) is_empty, seq = peek((1, 2, 3)) self.assertEqual((is_empty, list(seq)), (False, [1, 2, 3])) # list is_empty, seq = peek([]) self.assertEqual((is_empty, list(seq)), (True, [])) is_empty, seq = peek([1, 2, 3]) self.assertEqual((is_empty, list(seq)), (False, [1, 2, 3])) # set is_empty, seq = peek(set()) self.assertEqual((is_empty, list(seq)), (True, [])) is_empty, seq = peek({1, 2, 3}) self.assertEqual((is_empty, list(seq)), (False, [1, 2, 3])) # range is_empty, seq = peek(range(0)) self.assertEqual((is_empty, list(seq)), (True, [])) is_empty, seq = peek(range(1, 4)) self.assertEqual((is_empty, list(seq)), (False, [1, 2, 3])) # map is_empty, seq = peek(map(int, [])) self.assertEqual((is_empty, list(seq)), (True, [])) is_empty, seq = peek(map(int, [1, 2, 3])) self.assertEqual((is_empty, list(seq)), (False, [1, 2, 3])) # generator is_empty, seq = peek((i for i in [])) self.assertEqual((is_empty, list(seq)), (True, [])) is_empty, seq = peek((i for i in [1, 2, 3])) self.assertEqual((is_empty, list(seq)), (False, [1, 2, 3])) @requests_mock.mock() def test_get_redirect_url(self, m): m.get('https://httpbin.org/redirect-to', status_code=302, headers={'location': 'https://example.com/'}) r = requests.get('https://httpbin.org/redirect-to?url=https://example.com/', allow_redirects=False) self.assertEqual(get_redirect_url(r), 'https://example.com/') m.get('https://httpbin.org/redirect-to', status_code=302, headers={'location': 'http://example.com/'}) r = requests.get('https://httpbin.org/redirect-to?url=http://example.com/', allow_redirects=False) self.assertEqual(get_redirect_url(r), 'http://example.com/') m.get('https://httpbin.org/redirect-to', status_code=302, headers={'location': '/example'}) r = requests.get('https://httpbin.org/redirect-to?url=/example', allow_redirects=False) self.assertEqual(get_redirect_url(r), 'https://httpbin.org/example') m.get('https://httpbin.org/redirect-to', status_code=302, headers={'location': 'https://example.com'}) with self.assertRaises(RelativeRedirect): r = requests.get('https://httpbin.org/redirect-to?url=https://example.com', allow_redirects=False) get_redirect_url(r, require_relative=True) m.get('https://httpbin.org/redirect-to', status_code=302, headers={'location': '/example'}) with self.assertRaises(RelativeRedirect): r = requests.get('https://httpbin.org/redirect-to?url=/example', allow_redirects=False) get_redirect_url(r, allow_relative=False) def test_to_xml(self): to_xml(b'') to_xml(BOM_UTF8+b'') to_xml(BOM_UTF8+b'&broken') with self.assertRaises(ParseError): to_xml(b'foo') try: to_xml(b'Baz') except ParseError as e: # Not all lxml versions throw an error here, so we can't use assertRaises self.assertIn('Offending text: [...]Bazbar'},), exc_info=None) h.emit(r) h.stream.seek(0) self.assertEqual( h.stream.read(), "hello \x1b[36m\x1b[39;49;00m\n\x1b[94m" "\x1b[39;49;00mbar\x1b[94m\x1b[39;49;00m\n" ) def test_post_ratelimited(self): url = 'https://example.com' protocol = self.account.protocol orig_policy = protocol.config.retry_policy RETRY_WAIT = exchangelib.util.RETRY_WAIT MAX_REDIRECTS = exchangelib.util.MAX_REDIRECTS session = protocol.get_session() try: # Make sure we fail fast in error cases protocol.config.retry_policy = FailFast() # Test the straight, HTTP 200 path session.post = mock_post(url, 200, {}, 'foo') r, session = post_ratelimited(protocol=protocol, session=session, url='http://', headers=None, data='') self.assertEqual(r.content, b'foo') # Test exceptions raises by the POST request for err_cls in CONNECTION_ERRORS: session.post = mock_session_exception(err_cls) with self.assertRaises(err_cls): r, session = post_ratelimited( protocol=protocol, session=session, url='http://', headers=None, data='') # Test bad exit codes and headers session.post = mock_post(url, 401, {}) with self.assertRaises(UnauthorizedError): r, session = post_ratelimited(protocol=protocol, session=session, url='http://', headers=None, data='') session.post = mock_post(url, 999, {'connection': 'close'}) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url='http://', headers=None, data='') session.post = mock_post(url, 302, {'location': '/ews/genericerrorpage.htm?aspxerrorpath=/ews/exchange.asmx'}) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url='http://', headers=None, data='') session.post = mock_post(url, 503, {}) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url='http://', headers=None, data='') # No redirect header session.post = mock_post(url, 302, {}) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='') # Redirect header to same location session.post = mock_post(url, 302, {'location': url}) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='') # Redirect header to relative location session.post = mock_post(url, 302, {'location': url + '/foo'}) with self.assertRaises(RedirectError): r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='') # Redirect header to other location and allow_redirects=False session.post = mock_post(url, 302, {'location': 'https://contoso.com'}) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='') # Redirect header to other location and allow_redirects=True exchangelib.util.MAX_REDIRECTS = 0 session.post = mock_post(url, 302, {'location': 'https://contoso.com'}) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='', allow_redirects=True) # CAS error session.post = mock_post(url, 999, {'X-CasErrorCode': 'AAARGH!'}) with self.assertRaises(CASError): r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='') # Allow XML data in a non-HTTP 200 response session.post = mock_post(url, 500, {}, '') r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='') self.assertEqual(r.content, b'') # Bad status_code and bad text session.post = mock_post(url, 999, {}) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='') # Test rate limit exceeded exchangelib.util.RETRY_WAIT = 1 protocol.config.retry_policy = FaultTolerance(max_wait=0.5) # Fail after first RETRY_WAIT session.post = mock_post(url, 503, {'connection': 'close'}) # Mock renew_session to return the same session so the session object's 'post' method is still mocked protocol.renew_session = lambda s: s with self.assertRaises(RateLimitError) as rle: r, session = post_ratelimited(protocol=protocol, session=session, url='http://', headers=None, data='') self.assertEqual(rle.exception.status_code, 503) self.assertEqual(rle.exception.url, url) self.assertTrue(1 <= rle.exception.total_wait < 2) # One RETRY_WAIT plus some overhead # Test something larger than the default wait, so we retry at least once protocol.retry_policy.max_wait = 3 # Fail after second RETRY_WAIT session.post = mock_post(url, 503, {'connection': 'close'}) with self.assertRaises(RateLimitError) as rle: r, session = post_ratelimited(protocol=protocol, session=session, url='http://', headers=None, data='') self.assertEqual(rle.exception.status_code, 503) self.assertEqual(rle.exception.url, url) # We double the wait for each retry, so this is RETRY_WAIT + 2*RETRY_WAIT plus some overhead self.assertTrue(3 <= rle.exception.total_wait < 4, rle.exception.total_wait) finally: protocol.retire_session(session) # We have patched the session, so discard it # Restore patched attributes and functions protocol.config.retry_policy = orig_policy exchangelib.util.RETRY_WAIT = RETRY_WAIT exchangelib.util.MAX_REDIRECTS = MAX_REDIRECTS try: delattr(protocol, 'renew_session') except AttributeError: pass def test_safe_b64decode(self): # Test correctly padded string self.assertEqual(safe_b64decode('SGVsbG8gd29ybGQ='), b'Hello world') # Test incorrectly padded string self.assertEqual(safe_b64decode('SGVsbG8gd29ybGQ'), b'Hello world') # Test binary data self.assertEqual(safe_b64decode(b'SGVsbG8gd29ybGQ='), b'Hello world') # Test incorrectly padded binary data self.assertEqual(safe_b64decode(b'SGVsbG8gd29ybGQ'), b'Hello world') exchangelib-4.6.1/tests/test_version.py000066400000000000000000000070511414601472700202440ustar00rootroot00000000000000import requests_mock from exchangelib.errors import TransportError from exchangelib.version import EXCHANGE_2007, Version, Build from exchangelib.util import to_xml from .common import TimedTestCase class VersionTest(TimedTestCase): def test_default_api_version(self): # Test that a version gets a reasonable api_version value if we don't set one explicitly version = Version(build=Build(15, 1, 2, 3)) self.assertEqual(version.api_version, 'Exchange2016') @requests_mock.mock() # Just to make sure we don't make any requests def test_from_response(self, m): # Test fallback to suggested api_version value when there is a version mismatch and response version is fishy version = Version.from_soap_header( 'Exchange2007', to_xml(b'''\ ''') ) self.assertEqual(version.api_version, EXCHANGE_2007.api_version()) self.assertEqual(version.api_version, 'Exchange2007') self.assertEqual(version.build, Build(15, 1, 845, 22)) # Test that override the suggested version if the response version is not fishy version = Version.from_soap_header( 'Exchange2013', to_xml(b'''\ ''') ) self.assertEqual(version.api_version, 'HELLO_FROM_EXCHANGELIB') # Test that we override the suggested version with the version deduced from the build number if a version is not # present in the response version = Version.from_soap_header( 'Exchange2013', to_xml(b'''\ ''') ) self.assertEqual(version.api_version, 'Exchange2016') # Test that we use the version deduced from the build number when a version is not present in the response and # there was no suggested version. version = Version.from_soap_header( None, to_xml(b'''\ ''') ) self.assertEqual(version.api_version, 'Exchange2016') # Test various parse failures with self.assertRaises(TransportError) as e: Version.from_soap_header( 'Exchange2013', to_xml(b'''\ ''') ) self.assertIn('No ServerVersionInfo in header', e.exception.args[0]) with self.assertRaises(TransportError) as e: Version.from_soap_header( 'Exchange2013', to_xml(b'''\ ''') ) self.assertIn('Bad ServerVersionInfo in response', e.exception.args[0])