{ "info": { "author": "Erik Cederstrand", "author_email": "erik@cederstrand.dk", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Communications" ], "description": "Exchange Web Services client library\n====================================\n\nThis module provides an well-performing, well-behaving,\nplatform-independent and simple interface for communicating with a\nMicrosoft Exchange 2007-2016 Server or Office365 using Exchange Web\nServices (EWS). It currently implements autodiscover, and functions for\nsearching, creating, updating, deleting, exporting and uploading\ncalendar, mailbox, task, contact and distribution list items.\n\n[![image](https://img.shields.io/pypi/v/exchangelib.svg)](https://pypi.org/project/exchangelib/)\n[![image](https://img.shields.io/pypi/pyversions/exchangelib.svg)](https://pypi.org/project/exchangelib/)\n[![image](https://api.codacy.com/project/badge/Grade/5f805ad901054a889f4b99a82d6c1cb7)](https://www.codacy.com/app/ecederstrand/exchangelib?utm_source=github.com&utm_medium=referral&utm_content=ecederstrand/exchangelib&utm_campaign=Badge_Grade)\n[![image](https://secure.travis-ci.org/ecederstrand/exchangelib.png)](http://travis-ci.org/ecederstrand/exchangelib)\n[![image](https://coveralls.io/repos/github/ecederstrand/exchangelib/badge.svg?branch=master)](https://coveralls.io/github/ecederstrand/exchangelib?branch=master)\n\n[![image](https://liberapay.com/assets/widgets/donate.svg)](https://en.liberapay.com/ecederstrand)\n\n## Teaser\n\nHere's a short example of how `exchangelib` works. Let's print the first\n100 inbox messages in reverse order:\n\n```python\nfrom exchangelib import Credentials, Account\n\ncredentials = Credentials('john@example.com', 'topsecret')\naccount = Account('john@example.com', credentials=credentials, autodiscover=True)\n\nfor item in account.inbox.all().order_by('-datetime_received')[:100]:\n print(item.subject, item.sender, item.datetime_received)\n```\n\n\n## Installation\nYou can install this package from PyPI:\n\n```bash\npip install exchangelib\n```\n\nThe default installation does not support Kerberos. For additional Kerberos support, install\nwith the extra `kerberos` dependencies:\n\n```bash\npip install exchangelib[kerberos]\n```\n\nTo install the very latest code, install directly from GitHub instead:\n\n```bash\npip install git+https://github.com/ecederstrand/exchangelib.git\n```\n\n`exchangelib` uses the `lxml` package, and `pykerberos` to support Kerberos authentication.\nTo be able to install these, you may need to install some additional operating system packages.\n\nOn Ubuntu:\n```bash\napt-get install libxml2-dev libxslt1-dev\n\n# For Kerberos support, also install these:\napt-get install libkrb5-dev build-essential libssl-dev libffi-dev python-dev\n```\n\nOn CentOS:\n```bash\n# For Kerberos support, install these:\nyum install gcc python-devel krb5-devel krb5-workstation python-devel\n```\n\nOn FreeBSD, `pip` needs a little help:\n```bash\npkg install libxml2 libxslt\nCFLAGS=-I/usr/local/include pip install lxml\n\n# For Kerberos support, also install these:\npkg install krb5\nCFLAGS=-I/usr/local/include pip install kerberos pykerberos\n```\n\nFor other operating systems, please consult the documentation for the Python package that\nfails to install.\n\n\n## Setup and connecting\n\n```python\nfrom exchangelib import DELEGATE, IMPERSONATION, Account, Credentials, ServiceAccount, \\\n Configuration, NTLM, GSSAPI, Build, Version\n\n# Specify your credentials. Username is usually in WINDOMAIN\\username format, where WINDOMAIN is\n# the name of the Windows Domain your username is connected to, but some servers also\n# accept usernames in PrimarySMTPAddress ('myusername@example.com') format (Office365 requires it).\n# UPN format is also supported, if your server expects that.\ncredentials = Credentials(username='MYWINDOMAIN\\\\myusername', password='topsecret')\n\n# If you're running long-running jobs, you may want to enable fault-tolerance. Fault-tolerance\n# means that requests to the server do an exponential backoff and sleep for up to a certain\n# threshold before giving up, if the server is unavailable or responding with error messages.\n# This prevents automated scripts from overwhelming a failing or overloaded server, and hides\n# intermittent service outages that often happen in large Exchange installations.\n\n# If you want to enable the fault tolerance, create credentials as a service account instead:\ncredentials = ServiceAccount(username='FOO\\\\bar', password='topsecret')\n\n# An Account is the account on the Exchange server that you want to connect to. This can be\n# the account associated with the credentials you connect with, or any other account on the\n# server that you have been granted access to. If, for example, you want to access a shared\n# folder, create an Account instance using the email address of the account that the shared \n# folder belongs to, and access the shared folder through this account.\n\n# 'primary_smtp_address' is the primary SMTP address assigned the account. If you enable\n# autodiscover, an alias address will work, too. In this case, 'Account.primary_smtp_address'\n# will be set to the primary SMTP address.\nmy_account = Account(primary_smtp_address='myusername@example.com', credentials=credentials,\n autodiscover=True, access_type=DELEGATE)\njohns_account = Account(primary_smtp_address='john@example.com', credentials=credentials,\n autodiscover=True, access_type=DELEGATE)\nmarys_account = Account(primary_smtp_address='mary@example.com', credentials=credentials,\n autodiscover=True, access_type=DELEGATE)\nstill_marys_account = Account(primary_smtp_address='alias_for_mary@example.com',\n credentials=credentials, autodiscover=True, access_type=DELEGATE)\n\n# Set up a target account and do an autodiscover lookup to find the target EWS endpoint.\naccount = Account(primary_smtp_address='john@example.com', credentials=credentials,\n autodiscover=True, access_type=DELEGATE)\n\n# If your credentials have been given impersonation access to the target account, set a\n# different 'access_type':\naccount = Account(primary_smtp_address='john@example.com', credentials=credentials,\n autodiscover=True, access_type=IMPERSONATION)\n\n# If the server doesn't support autodiscover, or you want to avoid the overhead of autodiscover,\n# use a Configuration object to set the server location instead:\nconfig = Configuration(server='mail.example.com', credentials=credentials)\naccount = Account(primary_smtp_address='john@example.com', config=config,\n autodiscover=False, access_type=DELEGATE)\n\n# 'exchangelib' will attempt to guess the server version and authentication method. If you\n# have a really bizarre or locked-down installation and the guessing fails, or you want to avoid\n# the extra network traffic, you can set the auth method and version explicitly instead:\nversion = Version(build=Build(15, 0, 12, 34))\nconfig = Configuration(\n server='example.com', credentials=credentials, version=version, auth_type=NTLM\n)\n\n# Kerberos authentication is supported via the 'gssapi' auth type. Enabling it is slightly awkward,\n# does not work with autodiscover (yet) and is likely to change in future versions.\ncredentials = Credentials('', '')\nconfig = Configuration(server='example.com', credentials=credentials, auth_type=GSSAPI)\n\n# If you're connecting to the same account very often, you can cache the autodiscover result for\n# later so you can skip the autodiscover lookup:\news_url = account.protocol.service_endpoint\news_auth_type = account.protocol.auth_type\nprimary_smtp_address = account.primary_smtp_address\n\n# You can now create the Account without autodiscovering, using the cached values:\nconfig = Configuration(service_endpoint=ews_url, credentials=credentials, auth_type=ews_auth_type)\naccount = Account(\n primary_smtp_address=primary_smtp_address, \n config=config, autodiscover=False, \n access_type=DELEGATE,\n)\n\n# Autodiscover can take a lot of time, specially the part that figures out the autodiscover \n# server to contact for a specific email domain. For this reason, we will create a persistent, \n# per-user, on-disk cache containing a map of previous, successful domain -> autodiscover server\n# lookups. This cache is shared between processes and is not deleted when your program exits.\n\n# A cache entry for a domain is removed automatically if autodiscovery fails for an email in that\n# domain. It's possible to clear the entire cache completely if you want:\nfrom exchangelib.autodiscover import _autodiscover_cache\n_autodiscover_cache.clear()\n```\n\n## Proxies and custom TLS validation\n\nIf you need proxy support or custom TLS validation, you can supply a\ncustom 'requests' transport adapter class, as described in\n.\n\nHere's an example using different custom root certificates depending on\nthe server to connect to:\n\n```python\nfrom urllib.parse import urlparse\nimport requests.adapters\nfrom exchangelib.protocol import BaseProtocol\n\nclass RootCAAdapter(requests.adapters.HTTPAdapter):\n # An HTTP adapter that uses a custom root CA certificate at a hard coded location\n def cert_verify(self, conn, url, verify, cert):\n cert_file = {\n 'example.com': '/path/to/example.com.crt',\n 'mail.internal': '/path/to/mail.internal.crt',\n }[urlparse(url).hostname]\n super(RootCAAdapter, self).cert_verify(conn=conn, url=url, verify=cert_file, cert=cert)\n\n# Tell exchangelib to use this adapter class instead of the default\nBaseProtocol.HTTP_ADAPTER_CLS = RootCAAdapter\n```\n\nHere's an example of adding proxy support:\n\n```python\nimport requests.adapters\nfrom exchangelib.protocol import BaseProtocol\n\nclass ProxyAdapter(requests.adapters.HTTPAdapter):\n def send(self, *args, **kwargs):\n kwargs['proxies'] = {\n 'http': 'http://10.0.0.1:1243',\n 'https': 'http://10.0.0.1:4321',\n }\n return super(ProxyAdapter, self).send(*args, **kwargs)\n\n# Tell exchangelib to use this adapter class instead of the default\nBaseProtocol.HTTP_ADAPTER_CLS = ProxyAdapter\n```\n\n`exchangelib` provides a sample adapter which ignores TLS validation\nerrors. Use at own risk.\n\n```python\nfrom exchangelib.protocol import BaseProtocol, NoVerifyHTTPAdapter\n\n# Tell exchangelib to use this adapter class instead of the default\nBaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter\n```\n\n## Folders\nAll wellknown folders are available as properties on the account, e.g. as `account.root`, `account.calendar`,\n`account.trash`, `account.inbox`, `account.outbox`, `account.sent`, `account.junk`, `account.tasks` and\n`account.contacts`.\n\n```python\n# There are multiple ways of navigating the folder tree and searching for folders. Globbing and \n# absolute path may create unexpected results if your folder names contain slashes.\n\n# The folder structure is cached after first access to a folder hierarchy. This means that external\n# changes to the folder structure will not show up until you clear the cache. Here's how to clear\n# the cache of each of the currently supported folder hierarchies:\nfrom exchangelib import Account, Folder\n\na = Account(...)\na.root.refresh()\na.public_folders_root.refresh()\na.archive_root.refresh()\n\nsome_folder = a.root / 'Some Folder'\nsome_folder.parent\nsome_folder.parent.parent.parent\nsome_folder.root # Returns the root of the folder structure, at any level. Same as Account.root\nsome_folder.children # A generator of child folders\nsome_folder.absolute # Returns the absolute path, as a string\nsome_folder.walk() # A generator returning all subfolders at arbitrary depth this level\n# Globbing uses the normal UNIX globbing syntax\nsome_folder.glob('foo*') # Return child folders matching the pattern\nsome_folder.glob('*/foo') # Return subfolders named 'foo' in any child folder\nsome_folder.glob('**/foo') # Return subfolders named 'foo' at any depth\nsome_folder / 'sub_folder' / 'even_deeper' / 'leaf' # Works like pathlib.Path\n# You can also drill down into the folder structure without using the cache. This works like\n# the single slash syntax, but does not start by creating a cache the folder hierarchy. This is\n# useful if your account contains a huge number of folders, and you already know where to go.\nsome_folder // 'sub_folder' // 'even_deeper' // 'leaf'\nsome_folder.parts # returns some_folder and all its parents, as Folder instances\n# tree() returns a string representation of the tree structure at the given level\nprint(a.root.tree())\n'''\nroot\n\u251c\u2500\u2500 inbox\n\u2502 \u2514\u2500\u2500 todos\n\u2514\u2500\u2500 archive\n \u251c\u2500\u2500 Last Job\n \u251c\u2500\u2500 exchangelib issues\n \u2514\u2500\u2500 Mom\n'''\n\n# Folders have some useful counters:\na.inbox.total_count\na.inbox.child_folder_count\na.inbox.unread_count\n# Update the counters\na.inbox.refresh()\n\n# Folders can be created, updated and deleted:\nf = Folder(parent=a.inbox, name='My New Folder')\nf.save()\n\nf.name = 'My New Subfolder'\nf.save()\nf.delete()\n\n# Delete all items in a folder\nf.empty()\n# Also delete all subfolders in the folder\nf.empty(delete_sub_folders=True)\n# Recursively delete all items in a folder, and all subfolders and their content. This is\n# like `empty(delete_sub_folders=True)` but attempts to protect distinguished folders from\n# being deleted. Use with caution!\nf.wipe()\n```\n\n## Dates, datetimes and timezones\n\nEWS has some special requirements on datetimes and timezones. You need\nto use the special `EWSDate`, `EWSDateTime` and `EWSTimeZone` classes\nwhen working with dates.\n\n```python\nfrom datetime import datetime, timedelta\nimport pytz\nfrom exchangelib import EWSTimeZone, EWSDateTime, EWSDate\n\n# EWSTimeZone works just like pytz.timezone()\ntz = EWSTimeZone.timezone('Europe/Copenhagen')\n# You can also get the local timezone defined in your operating system\ntz = EWSTimeZone.localzone()\n\n# EWSDate and EWSDateTime work just like datetime.datetime and datetime.date. Always create\n# timezone-aware datetimes with EWSTimeZone.localize():\nlocalized_dt = tz.localize(EWSDateTime(2017, 9, 5, 8, 30))\nright_now = tz.localize(EWSDateTime.now())\n\n# Datetime math works transparently\ntwo_hours_later = localized_dt + timedelta(hours=2)\ntwo_hours = two_hours_later - localized_dt\ntwo_hours_later += timedelta(hours=2)\n\n# Dates\nmy_date = EWSDate(2017, 9, 5)\ntoday = EWSDate.today()\nalso_today = right_now.date()\nalso_today += timedelta(days=10)\n\n# UTC helpers. 'UTC' is the UTC timezone as an EWSTimeZone instance.\n# 'UTC_NOW' returns a timezone-aware UTC timestamp of current time.\nfrom exchangelib import UTC, UTC_NOW\n\nright_now_in_utc = UTC.localize(EWSDateTime.now())\nright_now_in_utc = UTC_NOW()\n\n# Already have a Python datetime object you want to use? Make sure it's localized. Then pass \n# it to from_datetime().\npytz_tz = pytz.timezone('Europe/Copenhagen')\npy_dt = pytz_tz.localize(datetime(2017, 12, 11, 10, 9, 8))\news_now = EWSDateTime.from_datetime(py_dt)\n```\n\n## Creating, updating, deleting, sending and moving\n\n```python\n# Here's an example of creating a calendar item in the user's standard calendar. If you want to\n# access a non-standard calendar, choose a different one from account.folders[Calendar].\n#\n# You can create, update and delete single items:\nfrom exchangelib import Account, CalendarItem, Message, Mailbox, FileAttachment, HTMLBody\nfrom exchangelib.items import SEND_ONLY_TO_ALL, SEND_ONLY_TO_CHANGED\n\na = Account(...)\nitem = CalendarItem(folder=a.calendar, subject='foo')\nitem.save() # This gives the item an 'id' and a 'changekey' value\nitem.save(send_meeting_invitations=SEND_ONLY_TO_ALL) # Send a meeting invitation to attendees\n# Update a field. All fields have a corresponding Python type that must be used.\nitem.subject = 'bar'\n# Print all available fields on the 'CalendarItem' class. Beware that some fields are read-only, or\n# read-only after the item has been saved or sent, and some fields are not supported on old\n# versions of Exchange.\nprint(CalendarItem.FIELDS)\nitem.save() # When the items has an item_id, this will update the item\nitem.save(update_fields=['subject']) # Only updates certain fields. Accepts a list of field names.\nitem.save(send_meeting_invitations=SEND_ONLY_TO_CHANGED) # Send invites only to attendee changes\nitem.delete() # Hard deletinon\nitem.delete(send_meeting_cancellations=SEND_ONLY_TO_ALL) # Send cancellations to all attendees\nitem.soft_delete() # Delete, but keep a copy in the recoverable items folder\nitem.move_to_trash() # Move to the trash folder\nitem.move(a.trash) # Also moves the item to the trash folder\nitem.copy(a.trash) # Creates a copy of the item to the trash folder\n\n# You can also send emails. If you don't want a local copy:\nm = Message(\n account=a,\n subject='Daily motivation',\n body='All bodies are beautiful',\n to_recipients=[\n Mailbox(email_address='anne@example.com'),\n Mailbox(email_address='bob@example.com'),\n ],\n cc_recipients=['carl@example.com', 'denice@example.com'], # Simple strings work, too\n bcc_recipients=[\n Mailbox(email_address='erik@example.com'),\n 'felicity@example.com',\n ], # Or a mix of both\n)\nm.send()\n\n# Or, if you want a copy in e.g. the 'Sent' folder\nm = Message(\n account=a,\n folder=a.sent,\n subject='Daily motivation',\n body='All bodies are beautiful',\n to_recipients=[Mailbox(email_address='anne@example.com')]\n)\nm.send_and_save()\n\n# Likewise, you can reply to and forward messages that are stored in your mailbox (i.e. they\n# have an item ID).\nm = a.sent.get(subject='Daily motivation')\nm.reply(\n subject='Re: Daily motivation',\n body='I agree',\n to_recipients=['carl@example.com', 'denice@example.com']\n)\nm.reply_all(subject='Re: Daily motivation', body='I agree')\nm.forward(\n subject='Fwd: Daily motivation',\n body='Hey, look at this!', \n to_recipients=['carl@example.com', 'denice@example.com']\n)\n\n# You can also edit a draft of a reply or forward\nforward_draft = m.create_forward(\n subject='Fwd: Daily motivation',\n body='Hey, look at this!',\n to_recipients=['carl@example.com', 'denice@example.com']\n).save(a.drafts) # gives you back the item\nforward_draft.reply_to = 'eric@example.com'\nforward_draft.attach(FileAttachment(name='my_file.txt', content='hello world'.encode('utf-8')))\nforward_draft.send() # now our forward has an extra reply_to field and an extra attachment.\n\n# EWS distinguishes between plain text and HTML body contents. If you want to send HTML body\n# content, use the HTMLBody helper. Clients will see this as HTML and display the body correctly:\nitem.body = HTMLBody('Hello happy OWA user!')\n```\n\n## Bulk operations\n\n```python\n# Build a list of calendar items\nfrom exchangelib import Account, CalendarItem, EWSDateTime, EWSTimeZone, Attendee, Mailbox\n\na = Account(...)\ntz = EWSTimeZone.timezone('Europe/Copenhagen')\nyear, month, day = 2016, 3, 20\ncalendar_items = []\nfor hour in range(7, 17):\n calendar_items.append(CalendarItem(\n start=tz.localize(EWSDateTime(year, month, day, hour, 30)),\n end=tz.localize(EWSDateTime(year, month, day, hour + 1, 15)),\n subject='Test item',\n body='Hello from Python',\n location='devnull',\n categories=['foo', 'bar'],\n required_attendees = [Attendee(\n mailbox=Mailbox(email_address='user1@example.com'),\n response_type='Accept'\n )]\n ))\n\n# Create all items at once\nreturn_ids = a.bulk_create(folder=a.calendar, items=calendar_items)\n\n# Bulk fetch, when you have a list of item IDs and want the full objects. Returns a generator.\ncalendar_ids = [(i.id, i.changekey) for i in calendar_items]\nitems_iter = a.fetch(ids=calendar_ids)\n# If you only want some fields, use the 'only_fields' attribute\nitems_iter = a.fetch(ids=calendar_ids, only_fields=['start', 'subject'])\n\n# Bulk update items. Each item must be accompanied by a list of attributes to update\nupdated_ids = a.bulk_update(items=[(i, ('start', 'subject')) for i in calendar_items])\n\n# Move many items to a new folder\nnew_ids = a.bulk_move(ids=calendar_ids, to_folder=a.other_calendar)\n\n# Send draft messages in bulk\nmessage_ids = a.drafts.all().only('id', 'changekey')\nnew_ids = a.bulk_send(ids=message_ids, save_copy=False)\n\n# Delete in bulk\ndelete_results = a.bulk_delete(ids=calendar_ids)\n\n# Bulk delete items found as a queryset\na.inbox.filter(subject__startswith='Invoice').delete()\n```\n\n## Searching\n\nSearching is modeled after the Django QuerySet API, and a large part of\nthe API is supported. Like in Django, the QuerySet is lazy and doesn't\nfetch anything before the QuerySet is iterated. QuerySets support\nchaining, so you can build the final query in multiple steps, and you\ncan re-use a base QuerySet for multiple sub-searches. The QuerySet\nreturns an iterator, and results are cached when the QuerySet is fully\niterated the first time.\n\nHere are some examples of using the API:\n\n```python\nfrom datetime import timedelta\nfrom exchangelib import Account, EWSDateTime, FolderCollection, Q\n\na = Account(...)\nall_items = a.inbox.all() # Get everything\nall_items_without_caching = a.inbox.all().iterator() # Get everything, but don't cache\n# Chain multiple modifiers ro refine the query\nfiltered_items = a.inbox.filter(subject__contains='foo').exclude(categories__icontains='bar')\nstatus_report = a.inbox.all().delete() # Delete the items returned by the QuerySet\nstart = a.default_timezone.localize(EWSDateTime(2017, 1, 1))\nend = a.default_timezone.localize(EWSDateTime(2018, 1, 1))\nitems_for_2017 = a.calendar.filter(start__range=(start, end)) # Filter by a date range\n\n# Same as filter() but throws an error if exactly one item isn't returned\nitem = a.inbox.get(subject='unique_string')\n\n# If you only have the ID and possibly the changekey of an item, you can get the full item:\na.inbox.get(id='AAMkADQy=')\na.inbox.get(id='AAMkADQy=', changekey='FwAAABYA')\n\n# You can sort by a single or multiple fields. Prefix a field with '-' to reverse the sorting. \n# Sorting is efficient since it is done server-side, except when a calendar view sorting on \n# multiple fields.\nordered_items = a.inbox.all().order_by('subject')\nreverse_ordered_items = a.inbox.all().order_by('-subject')\n # Indexed properties can be ordered on their individual components\nsorted_by_home_street = a.contacts.all().order_by('physical_addresses__Home__street')\n# Beware that sorting is done client-side here\na.calendar.view(start=start, end=end).order_by('subject', 'categories')\n\n# Counting and exists\nn = a.inbox.all().count() # Efficient counting\nfolder_is_empty = not a.inbox.all().exists() # Efficient tasting\n\n# Restricting returned attributes\nsparse_items = a.inbox.all().only('subject', 'start')\n# Dig deeper on indexed properties\nsparse_items = a.contacts.all().only('phone_numbers')\nsparse_items = a.contacts.all().only('phone_numbers__CarPhone')\nsparse_items = a.contacts.all().only('physical_addresses__Home__street')\n\n# Return values as dicts, not objects\nids_as_dict = a.inbox.all().values('id', 'changekey')\n# Return values as nested lists\nvalues_as_list = a.inbox.all().values_list('subject', 'body')\n# Return values as a flat list\nall_subjects = a.inbox.all().values_list('physical_addresses__Home__street', flat=True)\n\n# A QuerySet can be indexed and sliced like a normal Python list. Slicing and indexing of the\n# QuerySet is efficient because it only fetches the necessary items to perform the slicing.\n# Slicing from the end is also efficient, but then you might as well reverse the sorting.\nfirst_ten = a.inbox.all().order_by('-subject')[:10] # Efficient. We only fetch 10 items\nlast_ten = a.inbox.all().order_by('-subject')[:-10] # Efficient, but convoluted\nnext_ten = a.inbox.all().order_by('-subject')[10:20] # Efficient. We only fetch 10 items\nsingle_item = a.inbox.all().order_by('-subject')[34298] # Efficient. We only fetch 1 item\nten_items = a.inbox.all().order_by('-subject')[3420:3430] # Efficient. We only fetch 10 items\nrandom_emails = a.inbox.all().order_by('-subject')[::3] # This is just stupid, but works\n\n# The syntax for filter() is modeled after Django QuerySet filters. The following filter lookup \n# types are supported. Some lookups only work with string attributes. Range and less/greater \n# operators only work for date or numerical attributes. Some attributes are not searchable at all \n# via EWS:\nqs = a.calendar.all()\nqs.filter(subject='foo') # Returns items where subject is exactly 'foo'. Case-sensitive\nqs.filter(start__range=(start, end)) # Returns items within range\nqs.filter(subject__in=('foo', 'bar')) # Return items where subject is either 'foo' or 'bar'\nqs.filter(subject__not='foo') # Returns items where subject is not 'foo'\nqs.filter(start__gt=start) # Returns items starting after 'dt'\nqs.filter(start__gte=start) # Returns items starting on or after 'dt'\nqs.filter(start__lt=start) # Returns items starting before 'dt'\nqs.filter(start__lte=start) # Returns items starting on or before 'dt'\nqs.filter(subject__exact='foo') # Same as filter(subject='foo')\nqs.filter(subject__iexact='foo') # Returns items where subject is 'foo', 'FOO' or 'Foo'\nqs.filter(subject__contains='foo') # Returns items where subject contains 'foo'\nqs.filter(subject__icontains='foo') # Returns items where subject contains 'foo', 'FOO' or 'Foo'\nqs.filter(subject__startswith='foo') # Returns items where subject starts with 'foo'\n# Returns items where subject starts with 'foo', 'FOO' or 'Foo'\nqs.filter(subject__istartswith='foo')\n# Returns items that have at least one category assigned, i.e. the field exists on the item on the \n# server.\nqs.filter(categories__exists=True)\n# Returns items that have no categories set, i.e. the field does not exist on the item on the \n# server.\nqs.filter(categories__exists=False)\n\n# filter() also supports EWS QueryStrings. Just pass the string to filter(). QueryStrings cannot\n# be combined with other filters. We make no attempt at validating the syntax of the QueryString \n# - we just pass the string verbatim to EWS.\n#\n# Read more about the QueryString syntax here:\n# https://msdn.microsoft.com/en-us/library/ee693615.aspx\na.inbox.filter('subject:XXX')\n\n# filter() also supports Q objects that are modeled after Django Q objects, for building complex\n# boolean logic search expressions.\nq = (Q(subject__iexact='foo') | Q(subject__contains='bar')) & ~Q(subject__startswith='baz')\na.inbox.filter(q)\n\n# In this example, we filter by categories so we only get the items created by us.\na.calendar.filter(\n start__lt=a.default_timezone.localize(EWSDateTime(2019, 1, 1)),\n end__gt=a.default_timezone.localize(EWSDateTime(2019, 1, 31)),\n categories__contains=['foo', 'bar'],\n)\n\n# By default, EWS returns only the master recurring item. If you want recurring calendar\n# items to be expanded, use calendar.view(start=..., end=...) instead.\nitems = a.calendar.view(\n start=a.default_timezone.localize(EWSDateTime(2019, 1, 31)),\n end=a.default_timezone.localize(EWSDateTime(2019, 1, 31)) + timedelta(days=1),\n)\nfor item in items:\n print(item.start, item.end, item.subject, item.body, item.location)\n\n# You can combine view() with other modifiers. For example, to check for conflicts before \n# adding a meeting from 8:00 to 10:00:\nhas_conflicts = a.calendar.view(\n start=a.default_timezone.localize(EWSDateTime(2019, 1, 31, 8)),\n end=a.default_timezone.localize(EWSDateTime(2019, 1, 31, 10)),\n max_items=1\n).exists()\n\n# The filtering syntax also works on collections of folders, so you can search multiple folders in \n# a single request.\na.inbox.children.filter(subject='foo')\na.inbox.walk().filter(subject='foo')\na.inbox.glob('foo*').filter(subject='foo')\n# Or select the folders individually\nFolderCollection(account=a, folders=[a.inbox, a.calendar]).filter(subject='foo')\n```\n\n## Paging\n\nPaging EWS services, e.g. FindItem and, have a default page size of 100. You can\nchange this value globally if you want:\n\n```python\nimport exchangelib.services\nexchangelib.services.CHUNK_SIZE = 25\n```\n\nIf you are working with very small or very large items, this may not be a reasonable\nvalue. For example, if you want to retrieve and save emails with large attachments,\nyou can change this value on a per-queryset basis:\n\n```python\nfrom exchangelib import Account\n\na = Account(...)\nqs = a.inbox.all().only('mime_content')\nqs.page_size = 5\nfor msg in qs.iterator():\n with open('%s.eml' % msg.item_id, 'wb') as f:\n f.write(msg.mime_content)\n```\n\nFinally, the bulk methods defined on the `Account` class have an optional `chunk_size`\nargument that you can use to set a non-default page size when fetching, creating, updating\nor deleting items.\n\n```python\nfrom exchangelib import Account, Message\n\na = Account(...)\nhuge_list_of_items = [Message(...) for i in range(10000)]\nreturn_ids = a.bulk_create(folder=a.inbox, items=huge_list_of_items, chunk_size=5)\n```\n\n## Meetings\n\nThe `CalendarItem` class allows you send out requests for meetings that\nyou initiate or to cancel meetings that you already set out before. It\nis also possible to process `MeetingRequest` messages that are received.\nYou can reply to these messages using the `AcceptItem`,\n`TentativelyAcceptItem` and `DeclineItem` classes. If you receive a\ncancellation for a meeting (class `MeetingCancellation`) that you\nalready accepted then you can also process these by removing the entry\nfrom the calendar.\n\n```python\nfrom exchangelib import Account, CalendarItem, EWSDateTime\nfrom exchangelib.items import MeetingRequest, MeetingCancellation, SEND_TO_ALL_AND_SAVE_COPY\n\na = Account(...)\n\n# create a meeting request and send it out\nitem = CalendarItem(\n account=a,\n folder=a.calendar,\n start=a.default_timezone.localize(EWSDateTime(2019, 1, 31, 8, 15)),\n end=a.default_timezone.localize(EWSDateTime(2019, 1, 31, 8, 45)),\n subject=\"Subject of Meeting\",\n body=\"Please come to my meeting\",\n required_attendees=['anne@example.com', 'bob@example.com']\n)\nitem.save(send_meeting_invitations=SEND_TO_ALL_AND_SAVE_COPY)\n\n# cancel a meeting that was sent out using the CalendarItem class\nfor calendar_item in a.calendar.all().order_by('-datetime_received')[:5]:\n # only the organizer of a meeting can cancel it\n if calendar_item.organizer.email_address == a.primary_smtp_address:\n calendar_item.cancel()\n\n# processing an incoming MeetingRequest\nfor item in a.inbox.all().order_by('-datetime_received')[:5]:\n if isinstance(item, MeetingRequest):\n item.accept(body=\"Sure, I'll come\")\n # Or:\n item.decline(body=\"No way!\")\n # Or:\n item.tentatively_accept(body=\"Maybe...\")\n\n# meeting requests can also be handled from the calendar - e.g. decline the meeting that was \n# received last.\nfor calendar_item in a.calendar.all().order_by('-datetime_received')[:1]:\n calendar_item.decline()\n\n# processing an incoming MeetingCancellation (also delete from calendar)\nfor item in a.inbox.all().order_by('-datetime_received')[:5]:\n if isinstance(item, MeetingCancellation):\n if item.associated_calendar_item_id:\n calendar_item = a.inbox.get(\n id=item.associated_calendar_item_id.id,\n changekey=item.associated_calendar_item_id.changekey\n )\n calendar_item.delete()\n item.move_to_trash()\n```\n\n## Searching contacts\n\nFetching personas from a contact folder is supported using the same\nsyntax as folders. Just start your query with `.people()`:\n\n```python\n# Navigate to a contact folder and start the search\nfrom exchangelib import Account\n\na = Account(...)\nfolder = a.root / 'AllContacts'\nfor p in folder.people():\n print(p)\nfor p in folder.people().only('display_name').filter(display_name='john').order_by('display_name'):\n print(p)\n```\n\n## Extended properties\n\nExtended properties makes it possible to attach custom key-value pairs\nto items and folders on the Exchange server. There are multiple online\nresources that describe working with extended properties, and list many\nof the magic values that are used by existing Exchange clients to store\ncommon and custom properties. The following is not a comprehensive\ndescription of the possibilities, but we do intend to support all the\npossibilities provided by EWS.\n\n```python\n# If folder items have extended properties, you need to register them before you can access them. \n# Create a subclass of ExtendedProperty and define a set of matching setup values:\nfrom exchangelib import Account, ExtendedProperty, CalendarItem, Folder, Message\n\na = Account(...)\n\nclass LunchMenu(ExtendedProperty):\n property_set_id = '12345678-1234-1234-1234-123456781234'\n property_name = 'Catering from the cafeteria'\n property_type = 'String'\n\n# Register the property on the item type of your choice\nCalendarItem.register('lunch_menu', LunchMenu)\n# Now your property is available as the attribute 'lunch_menu', just like any other attribute\nitem = CalendarItem(..., lunch_menu='Foie gras et consomm\u00e9 de l\u00e9gumes')\nitem.save()\nfor i in a.calendar.all():\n print(i.lunch_menu)\n# If you change your mind, jsut remove the property again\nCalendarItem.deregister('lunch_menu')\n\n# You can also create named properties (e.g. created from User Defined Fields in Outlook, see \n# issue #137):\nclass LunchMenu(ExtendedProperty):\n distinguished_property_set_id = 'PublicStrings'\n property_name = 'Catering from the cafeteria'\n property_type = 'String'\n\n# We support extended properties with tags. This is the definition for the 'completed' and \n# 'followup' flag you can add to items in Outlook (see also issue #85):\nclass Flag(ExtendedProperty):\n property_tag = 0x1090\n property_type = 'Integer'\n\n# Or with property ID:\nclass MyMeetingArray(ExtendedProperty):\n property_set_id = '00062004-0000-0000-C000-000000000046'\n property_type = 'BinaryArray'\n property_id = 32852\n\n# Or using distinguished property sets combined with property ID (here as a hex value to align \n# with the format usually mentioned in Microsoft docs). This is the definition for a response to\n# an Outlook Vote request (see issue #198):\nclass VoteResponse(ExtendedProperty):\n distinguished_property_set_id = 'Common'\n property_id = 0x00008524\n property_type = 'String'\n\n# Extended properties also work with folders. Here's an example of getting the size (in bytes) of\n# a folder:\nclass FolderSize(ExtendedProperty):\n property_tag = 0x0e08\n property_type = 'Integer'\n\nFolder.register('size', FolderSize)\nprint(a.inbox.size)\n\n# In general, here's how to work with any MAPI property as listed in e.g.\n# https://msdn.microsoft.com/EN-US/library/office/cc815517.aspx. Let's take `PidLidTaskDueDate` as\n# an example. This is the due date for a message maked with the follow-up flag in Microsoft \n# Outlook.\n#\n# PidLidTaskDueDate is documented at https://msdn.microsoft.com/en-us/library/office/cc839641.aspx.\n# The property ID is `0x00008105` and the property set is `PSETID_Task`. But EWS wants the UUID for\n# `PSETID_Task`, so we look that up in the MS-OXPROPS pdf:\n# https://msdn.microsoft.com/en-us/library/cc433490(v=exchg.80).aspx. The UUID is\n# `00062003-0000-0000-C000-000000000046`. The property type is `PT_SYSTIME` which is also called\n# `SystemTime` (see\n# https://msdn.microsoft.com/en-us/library/microsoft.exchange.webservices.data.mapipropertytype(v=exchg.80).aspx).\n#\n# In conclusion, the definition for the due date becomes:\n\nclass FlagDue(ExtendedProperty):\n property_set_id = '00062003-0000-0000-C000-000000000046'\n property_id = 0x8105\n property_type = 'SystemTime'\n\nMessage.register('flag_due', FlagDue)\n```\n\n## Attachments\n\n```python\n# It's possible to create, delete and get attachments connected to any item type:\n# Process attachments on existing items. FileAttachments have a 'content' attribute\n# containing the binary content of the file, and ItemAttachments have an 'item' attribute\n# containing the item. The item can be a Message, CalendarItem, Task etc.\nimport os.path\nfrom exchangelib import Account, FileAttachment, ItemAttachment, Message, CalendarItem, HTMLBody\n\na = Account\nfor item in a.inbox.all():\n for attachment in item.attachments:\n if isinstance(attachment, FileAttachment):\n local_path = os.path.join('/tmp', attachment.name)\n with open(local_path, 'wb') as f:\n f.write(attachment.content)\n print('Saved attachment to', local_path)\n elif isinstance(attachment, ItemAttachment):\n if isinstance(attachment.item, Message):\n print(attachment.item.subject, attachment.item.body)\n\n# Streaming downloads of file attachment is supported. This reduces memory consumption since we\n# never store the full content of the file in-memory:\nfor item in a.inbox.all():\n for attachment in item.attachments:\n if isinstance(attachment, FileAttachment):\n local_path = os.path.join('/tmp', attachment.name)\n with open(local_path, 'wb') as f, attachment.fp as fp:\n buffer = fp.read(1024)\n while buffer:\n f.write(buffer)\n buffer = fp.read(1024)\n print('Saved attachment to', local_path)\n\n# Create a new item with an attachment\nitem = Message(...)\nbinary_file_content = 'Hello from unicode \u00e6\u00f8\u00e5'.encode('utf-8') # Or read from file, BytesIO etc.\nmy_file = FileAttachment(name='my_file.txt', content=binary_file_content)\nitem.attach(my_file)\nmy_calendar_item = CalendarItem(...)\nmy_appointment = ItemAttachment(name='my_appointment', item=my_calendar_item)\nitem.attach(my_appointment)\nitem.save()\n\n# Add an attachment on an existing item\nmy_other_file = FileAttachment(name='my_other_file.txt', content=binary_file_content)\nitem.attach(my_other_file)\n\n# Remove the attachment again\nitem.detach(my_file)\n\n# If you want to embed an image in the item body, you can link to the file in the HTML\nmessage = Message(...)\nlogo_filename = 'logo.png'\nwith open(logo_filename, 'rb') as f:\n my_logo = FileAttachment(name=logo_filename, content=f.read(), is_inline=True, content_id=logo_filename)\nmessage.attach(my_logo)\nmessage.body = HTMLBody('Hello logo: ' % logo_filename)\n\n# Attachments cannot be updated via EWS. In this case, you must to detach the attachment, update\n# the relevant fields, and attach the updated attachment.\n\n# Be aware that adding and deleting attachments from items that are already created in Exchange\n# (items that have an item_id) will update the changekey of the item.\n```\n\n## Recurring calendar items\n\nThere is full read-write support for creating recurring calendar items.\nYou can create daily, weekly, monthly and yearly recurrences (the latter\ntwo in relative and absolute versions).\n\nHere's an example of creating 7 occurrences on Mondays and Wednesdays of\nevery third week, starting September 1, 2017:\n\n```python\nfrom datetime import timedelta\nfrom exchangelib import Account, CalendarItem, EWSDateTime\nfrom exchangelib.fields import MONDAY, WEDNESDAY\nfrom exchangelib.recurrence import Recurrence, WeeklyPattern\n\na = Account(...)\nstart = a.default_timezone.localize(EWSDateTime(2017, 9, 1, 11))\nend = start + timedelta(hours=2)\nitem = CalendarItem(\n folder=a.calendar,\n start=start,\n end=end,\n subject='Hello Recurrence',\n recurrence=Recurrence(\n pattern=WeeklyPattern(interval=3, weekdays=[MONDAY, WEDNESDAY]),\n start=start.date(),\n number=7\n ),\n)\n\n# Occurrence data for the master item\nfor i in a.calendar.filter(start__lt=end, end__gt=start):\n print(i.subject, i.start, i.end)\n print(i.recurrence)\n print(i.first_occurrence)\n print(i.last_occurrence)\n for o in i.modified_occurrences:\n print(o)\n for o in i.deleted_occurrences:\n print(o)\n\n# All occurrences expanded. The recurrence will span over 4 iterations of a 3-week period\nfor i in a.calendar.view(start=start, end=start + timedelta(days=4*3*7)):\n print(i.subject, i.start, i.end)\n\n# 'modified_occurrences' and 'deleted_occurrences' of master items are read-only fields. To \n# delete or modify an occurrence, you must use 'view()' to fetch the occurrence and modify or \n# delete it:\nfor occurrence in a.calendar.view(start=start, end=start + timedelta(days=4*3*7)):\n # Delete or update random occurrences. This will affect 'modified_occurrences' and \n # 'deleted_occurrences' of the master item.\n if occurrence.start.milliseconds % 2:\n # We receive timestamps as UTC but want to write them back as local timezone\n occurrence.start = occurrence.start.astimezone(a.default_timezone)\n occurrence.start += timedelta(minutes=30)\n occurrence.end = occurrence.end.astimezone(a.default_timezone)\n occurrence.end += timedelta(minutes=30)\n occurrence.subject = 'My new subject'\n occurrence.save()\n else:\n occurrence.delete()\n```\n\n## Message timestamp fields\n\nEach `Message` item has four timestamp fields:\n\n- `datetime_created`\n- `datetime_sent`\n- `datetime_received`\n- `last_modified_time`\n\nThe values for these fields are set by the Exchange server and are not\nmodifiable via EWS. All values are timezone-aware `EWSDateTime`\ninstances.\n\nThe `datetime_sent` value may be earlier than `datetime_created`.\n\n## Out of Facility\n\nYou can get and set OOF messages using the `Account.oof_settings`\nproperty:\n\n```python\nfrom exchangelib import Account, OofSettings, EWSDateTime\n\na = Account(...)\n\n# Get the current OOF settings\na.oof_settings\n# Change the OOF settings to something else\na.oof_settings = OofSettings(\n state=OofSettings.SCHEDULED,\n external_audience='Known',\n internal_reply=\"I'm in the pub. See ya guys!\",\n external_reply=\"I'm having a business dinner in town\",\n start=a.default_timezone.localize(EWSDateTime(2017, 11, 1, 11)),\n end=a.default_timezone.localize(EWSDateTime(2017, 12, 1, 11)),\n)\n# Disable OOF messages\na.oof_settings = OofSettings(\n state=OofSettings.DISABLED,\n internal_reply='',\n external_reply='',\n)\n```\n\n## Export and upload\n\nExchange supports backup and restore of folder contents using special\nexport and upload services. They are available on the `Account` model:\n\n```python\nfrom exchangelib import Account\n\na = Account(...)\nitems = a.inbox.all().only('id', 'changekey')\ndata = a.export(items) # Pass a list of Item instances or (item_id, changekey) tuples\na.upload((a.inbox, d) for d in data) # Restore the items. Expects a list of (folder, data) tuples\n```\n\n## Non-account methods\n\n```python\nfrom exchangelib import Account, DLMailbox\n\na = Account(...)\n\n# Get timezone information from the server\na.protocol.get_timezones()\n\n# Get room lists defined on the server\na.protocol.get_roomlists()\n\n# Get rooms belonging to a specific room list\nfor rl in a.protocol.get_roomlists():\n a.protocol.get_rooms(rl)\n\n# Get account information for a list of names or email addresses\nfor mailbox in a.protocol.resolve_names(['ann@example.com', 'bart@example.com']):\n print(mailbox.email_address)\nfor mailbox, contact in a.protocol.resolve_names(['anne', 'bart'], return_full_contact_data=True):\n print(mailbox.email_address, contact.display_name)\n\n# Get all mailboxes on a distribution list\nfor mailbox in a.protocol.expand_dl(DLMailbox(email_address='distro@example.com', mailbox_type='PublicDL'):\n print(mailbox.email_address)\n# Or just pass a string containing the SMTP address\nfor mailbox in a.protocol.expand_dl('distro@example.com'):\n print(mailbox.email_address)\n\n# Get searchable mailboxes. This method is only available to users who have been assigned\n# the Discovery Management RBAC role. (This feature works on Exchange 2013 onwards)\nfor mailbox in a.protocol.get_searchable_mailboxes():\n print(mailbox)\n```\n\nEWS supports getting availability information for a set of users in a certain\ntimeframe. The server returns an object for each account containing free/busy\ninformation, including a list of calendar events in the user's calendar, and\nthe working hours and timezone of the user.\n\n```python\nfrom datetime import timedelta\nfrom exchangelib import Account, EWSDateTime\n\na = Account(...)\nstart = a.default_timezone.localize(EWSDateTime.now())\nend = start + timedelta(hours=6)\naccounts = [(a, 'Organizer', False)]\nfor busy_info in a.protocol.get_free_busy_info(accounts=accounts, start=start, end=end):\n print(busy_info)\n```\n\nThe calendar events and working hours are returned as naive datetimes. To convert\nto timezone-aware datetimes, a bit of extra work is needed if the users are not\nknown to be in the same timezone.\n\n```python\n# Get all server timezones. We need that to convert 'working_hours_timezone'\nfrom datetime import timedelta\nfrom exchangelib import Account, EWSDateTime, EWSTimeZone\n\na = Account(...)\ntimezones = list(a.protocol.get_timezones(return_full_timezone_data=True))\n\n# Get availability information for a list of accounts\nstart = a.default_timezone.localize(EWSDateTime.now())\nend = start + timedelta(hours=6)\n# get_free_busy_info() expects a list of (account, attendee_type, exclude_conflicts) tuples\naccounts = [(a, 'Organizer', False)]\nfor busy_info in a.protocol.get_free_busy_info(accounts=accounts, start=start, end=end):\n # Convert the TimeZone object to a Microsoft timezone ID\n ms_id = busy_info.working_hours_timezone.to_server_timezone(timezones, start.year)\n account_tz = EWSTimeZone.from_ms_id(ms_id)\n print(account_tz, busy_info.working_hours)\n for event in busy_info.calendar_events:\n print(account_tz.localize(event.start), account_tz.localize(event.end))\n```\n\n\n## Troubleshooting\n\nIf you are having trouble using this library, the first thing to try is\nto enable debug logging. This will output a huge amount of information\nabout what is going on, most notable the actual XML documents that are\ngoing over the wire. This can be really handy to see which fields are\nbeing sent and received.\n\n```python\nimport logging\n# This handler will pretty-print and syntax highlight the request and response XML documents\nfrom exchangelib.util import PrettyXmlHandler\n\nlogging.basicConfig(level=logging.DEBUG, handlers=[PrettyXmlHandler()])\n# Your code using exchangelib goes here\n```\n\nMost class definitions have a docstring containing at least a URL to the\nMSDN page for the corresponding XML element.\n\n```python\nfrom exchangelib import CalendarItem\nprint(CalendarItem.__doc__)\n```\n\n# Notes\n\nAlmost all item fields are supported. The remaining ones are tracked in\n.", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/ecederstrand/exchangelib", "keywords": "ews exchange autodiscover microsoft outlook exchange-web-services o365 office365", "license": "BSD", "maintainer": "", "maintainer_email": "", "name": "exchangelibtmp", "package_url": "https://pypi.org/project/exchangelibtmp/", "platform": "", "project_url": "https://pypi.org/project/exchangelibtmp/", "project_urls": { "Homepage": "https://github.com/ecederstrand/exchangelib" }, "release_url": "https://pypi.org/project/exchangelibtmp/1.12.2/", "requires_dist": null, "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "summary": "Client for Microsoft Exchange Web Services (EWS)", "version": "1.12.2" }, "last_serial": 5183368, "releases": { "1.12.2": [ { "comment_text": "", "digests": { "md5": "56cfe3cc55b6a92680955c6a5c082705", "sha256": "90caed969f9bff7eea2d8cd14ce59d03328ee52e6591444addc15b2d740977fc" }, "downloads": -1, "filename": "exchangelibtmp-1.12.2.tar.gz", "has_sig": false, "md5_digest": "56cfe3cc55b6a92680955c6a5c082705", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 186210, "upload_time": "2019-04-24T16:51:20", "url": "https://files.pythonhosted.org/packages/77/1a/67d2a05165548f2a4a147aef234e6211487204f7e734e8e0f072ab74045c/exchangelibtmp-1.12.2.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "56cfe3cc55b6a92680955c6a5c082705", "sha256": "90caed969f9bff7eea2d8cd14ce59d03328ee52e6591444addc15b2d740977fc" }, "downloads": -1, "filename": "exchangelibtmp-1.12.2.tar.gz", "has_sig": false, "md5_digest": "56cfe3cc55b6a92680955c6a5c082705", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 186210, "upload_time": "2019-04-24T16:51:20", "url": "https://files.pythonhosted.org/packages/77/1a/67d2a05165548f2a4a147aef234e6211487204f7e734e8e0f072ab74045c/exchangelibtmp-1.12.2.tar.gz" } ] }