How to install and use Exchangelib Python

pip install exchangelib

Try a faster and easier way to work with Python packages like Exchangelib. Use Python 3.9 by ActiveState and build your own runtime with the packages and dependencies you need. Get started for free by creating an account on the ActiveState Platform or logging in with your GitHub account.

Exchangelib is a Python client library that provides an interface for accessing and working with Microsoft Exchange Web Services (EWS). EWS is both a messaging protocol and an Application Programming Interface (API) for locating and connecting with EWS/Exchange Server hosts, and provides a library of functions for working with host applications and user data.

If you’re not familiar with Microsoft Exchange Server, it provides organizations with email and calendaring services for their users. 

Exchange Server pipeline
Figure 1. Exchange Server pipeline

Although EWS and Exchange Servers are Windows-only, Exchangelib is a Python cross-platform tool.

Exchangelib Package Installation

The simplest way to create an exchangelib project, is to install Python 3.9 from ActiveState and then run:

state install exchangelib

This will build all components from source code, and then install Python 3.9 in a virtual directory along with exchangelib and all it’s dependencies, ready to be worked with.

If you have already have Python 3 installed, the Exchangelib package can be installed from the Python Package Index (PyPI) by entering the following command in a terminal or command window: 

pip install exchangelib 

or 

Python3 -m pip install exchangelib

Or you can install the latest version directly from GitHub:

pip install git+https://github.com/ecederstrand/exchangelib.git

You’ll also need to install either Kerberos or SSPI (required on Windows only): 

pip install exchangelib[kerberos]

pip install exchangelib[sspi]

Exchangelib Python: Getting Started

Exchangelib Python includes: 

  • Services for locating EWS/Exchange Server hosts
  • Protocols for establishing a secure connection with host applications
  • Functions for accessing, searching, creating, updating, deleting, exporting and uploading data to user accounts

Before connecting to EWS and Exchange Server mail and calendaring applications, you’ll first need to import the packages required for interoperability with EWS/Exchange Server:

# Import Exchangelib into Python:

Import exchangelib

# Packages required by Exchangelib Python:

from datetime import datetime, timedelta

import pytz

from exchangelib import DELEGATE, IMPERSONATION, Account, Credentials, EWSDateTime, EWSTimeZone, Configuration, NTLM, GSSAPI, CalendarItem, Message, Mailbox, Attendee, Q, ExtendedProperty, FileAttachment, ItemAttachment, HTMLBody, Build, Version, FolderCollection

Use Exchangelib to Connect to Exchange Server

  1. First establish your credentials. Your username is usually in WINDOMAIN\username format  but may be in PrimarySMTPAddress (‘myusername@example.com’) format (ie., Office365 requires this). For example:
credentials = Credentials(username='MYWINDOMAIN\\myusername', password='topsecret')
  1. Establish a connection with your EWS user account. For example:

my_account = Account(primary_smtp_address='myusername@example.com', credentials=credentials, autodiscover=True, access_type=DELEGATE)

Where:

  • ‘Account’ can be your account or (if access_type=DELEGATE) any other account on the server that you have been granted access to. 
  • primary_smtp_address’ is the primary SMTP address assigned to the account.
  • autodiscover=True performs an autodiscover lookup to find the target EWS endpoint
  • You can also set access_type=IMPERSONATION in order to  gain impersonation access to the target account

You can set the auth method and version explicitly using config = Configuration(). For example:

config = Configuration(

    server='example.com', credentials=credentials, version=version, auth_type=NTLM

To enable Kerberos authentication, you can use:

credentials = Credentials('', '')

config = Configuration(server='example.com', credentials=credentials, auth_type=GSSAPI)

Here, Kerberos is supported via the ‘GSSAPI’ authentication type. 

Exchangelib – Proxies and Custom TLS Validation

If proxy support or custom TLS validation is needed, you can supply a custom ‘requests’ transport adapter class.

# Example of using different custom root certificates depending on the server to connect to:

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(RootCAAdapter, self).cert_verify(conn=conn, url=url, verify=cert_file, cert=cert)

# Tell Exchangelib Python to use this adapter class instead of the default:

BaseProtocol.HTTP_ADAPTER_CLS = RootCAAdapter

# Example of adding proxy support:

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(ProxyAdapter, self).send(*args, **kwargs)

# Tell Exchangelib Python to use this adapter class instead of the default:

BaseProtocol.HTTP_ADAPTER_CLS = ProxyAdapter

# Exchangelib Python provides a sample adapter which ignores TLS validation errors. (Use at your own risk):

from exchangelib.protocol import NoVerifyHTTPAdapter

# Tell Exchangelib Python to use this adapter class instead of the default

BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter

Exchangelib – How to Access Folders

There are multiple ways to navigate and search the folder tree. Be aware that if your folder names contain slashes, then globbing and absolute path may create unexpected results. (globs are patterns that specify sets of filenames with wildcard characters. Eg. Unix bash cmd: mv *.txt textfiles/).

You can use some_folder.root to return the root of the folder structure, at any level. For example:

print(Account.root)
Print Account.root
Figure 1. Print Account.root

Other examples include:

  • print(account.root.children) which returns child folders
  • print(account.root.absolute)which returns the absolute path
  • print(account.root.walk) which is a generator returning all subfolders
  • print(account.root.parts) which returns some_folder and all its parents

Or else you can use UNIX globbing syntax. For example:

# Return child folders that match the pattern, using the foo placeholder::

print(account.root.glob('foo*'))

# Return subfolders named ‘foo’ in any child folder:

print(account.root.glob('*/foo'))

# Return subfolders named ‘foo’ at any depth:

print(account.root.glob('**/foo'))

 # Works like pathlib.Path:

some_folder / 'sub_folder' / 'even_deeper' / 'leaf'  

Exchangelib – How to Use Dates, Datetimes and Timezones

EWS has special requirements on datetimes and timezones. You need to use the  EWSDate, EWSDateTime and EWSTimeZone classes when working with dates. For example:

# You can use the local timezone defined in your OS:

tz = EWSTimeZone.localzone()

# EWSDATE, EWSDATETIME and DATETIME.DATETIME, datetime.date are

# similar. Always localize time zones, by creating timezone-aware datetimes 

# with EWSTimeZone.localize():

localized_dt = tz.localize(EWSDateTime(2017, 9, 5, 8, 30))

right_now = tz.localize(EWSDateTime.now())

# Add datetime:

two_hours_later = localized_dt + timedelta(hours=2)

two_hours = two_hours_later - localized_dt

two_hours_later += timedelta(hours=2)

Exchangelib – Create, Update, Delete, Send & Move Data 

Python applications can be used to create, update, delete, send and move data to user accounts. In this example, a calendar item is created in the user’s standard calendar:

# Create, update and delete single items:

from exchangelib.items import SEND_ONLY_TO_ALL, SEND_ONLY_TO_CHANGED

item = CalendarItem(folder=account.calendar, subject='foo')

# Give the item an ‘id’ and a ‘changekey’ value:

item.save()  

item.save(send_meeting_invitations=SEND_ONLY_TO_ALL)  # Send a meeting invitation to attendees

# Update a field. All fields have a corresponding Python type that must be used:

item.subject = 'bar'

# Print all available fields on the CalendarItem class:

print(CalendarItem.FIELDS)

# Items with item_id, are updated with save():

item.save()  

# Only update certain fields:

item.save(update_fields=['subject']) 

# Send invites only to attendee changes:

item.save(send_meeting_invitations=SEND_ONLY_TO_CHANGED)

# Hard deletion:

item.delete()

# Send cancellations to all attendees:

item.delete(send_meeting_cancellations=SEND_ONLY_TO_ALL) 

# Delete, but keep a copy in the recoverable items folder:

item.soft_delete() 

# Move to the trash folder: 

item.move_to_trash() 

# Also move item to the account.trash folder:

item.move(account.trash)

# Create a copy of item in the account.trash folder: 

item.copy(account.trash) folder

# Send emails without local copies:

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 also work:

    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 send emails, and get local copies, eg. in the ‘Sent’ folder:

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

# You can reply to and forward messages:

m.reply(

    subject='Re: foo',

    body='I agree',

    to_recipients=['carl@example.com', 'denice@example.com']

)

m.reply_all(subject='Re: foo', body='I agree')

m.forward(

    subject='Fwd: foo', 

    body='Hey, look at this!', 

    to_recipients=['carl@example.com', 'denice@example.com']

)

# EWS distinquishes between plain text and HTML body contents. 

# To send HTML body content, use the HTMLBody helper.

# Clients will see this as HTML and display the body correctly:

item.body = HTMLBody('<html><body>Hello happy <blink>OWA user!</blink></body></html>')

Exchangelib – Bulk Operations

In this example, a bulk list of calendar items is built:

tz = EWSTimeZone.timezone('Europe/Copenhagen')

year, month, day = 2016, 3, 20

calendar_items = []

for hour in range(7, 17):

    calendar_items.append(CalendarItem(

        start=tz.localize(EWSDateTime(year, month, day, hour, 30)),

        end=tz.localize(EWSDateTime(year, month, day, hour + 1, 15)),

        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 = account.bulk_create(folder=account.calendar, items=calendar_items)

# Bulk fetch, for 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 = account.fetch(ids=calendar_ids)

# If you only want some fields, use the 'only_fields' attribute

items_iter = account.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 = account.bulk_create(items=[(i, ('start', 'subject')) for i in calendar_items])

# Move many items to a new folder:

new_ids = account.bulk_move(ids=calendar_ids, to_folder=account.other_calendar)

# Send draft messages in bulk:

new_ids = account.bulk_send(ids=message_ids, save_copy=False)

# Delete in bulk:

delete_results = account.bulk_delete(ids=calendar_ids)

# Bulk delete items found as a queryset:

account.inbox.filter(subject__startswith='Invoice').delete()

Exchangelib – Searching

Searching is modeled after the Django QuerySet API. In fact a large part of the API is supported in Exchangelib Python. As 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 returns an iterator, and results are cached when the QuerySet is fully iterated the first time.

In this example, the Django QuerySet API is used to query the calendaring application (that has been located and accessed using Exchanglib protocols and web services):

# Get the calendar items created in the previous Bulk Operations section:

Django QuerySet API  # Get everything

 # Get everything, but don’t cache:

all_items_without_caching = my_folder.all().iterator() 

# Chain multiple modifiers ro refine the query:

filtered_items = my_folder.filter(subject__contains='foo').exclude(categories__icontains='bar')

# Delete the items returned by the QuerySet:

status_report = my_folder.all().delete()  

items_for_2017 = my_calendar.filter(start__range=(

    tz.localize(EWSDateTime(2017, 1, 1)),

    tz.localize(EWSDateTime(2018, 1, 1))

))  # Filter by a date range

# Same as filter(), but throws an error if exactly one item isn’t returned:

item = my_folder.get(subject='unique_string')

# You can sort by a single or multiple fields. 

# Prefix a field with ‘-‘ to reverse the sorting:

ordered_items = my_folder.all().order_by('subject')

reverse_ordered_items = my_folder.all().order_by('-subject')

 # Indexed properties can be ordered on their individual components:

sorted_by_home_street = my_contacts.all().order_by('physical_addresses__Home__street')

dont_do_this = my_huge_folder.all().order_by('subject', 'categories')[:10]  

Exchangelib – Working with Meetings

The CalendarItem class allows you to send out requests for meetings that you initiate, or to cancel meetings that you set out previously. 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.

import datetime

from exchangelib import Account, CalendarItem

from exchangelib.items import MeetingRequest, MeetingCancellation, \

  SEND_TO_ALL_AND_SAVE_COPY

credentials = Credentials(username='MYWINDOMAIN\\myusername', password='topsecret')
a = Account(primary_smtp_address='john@example.com', credentials=credentials,

                  autodiscover=True, access_type=DELEGATE)

# Create and send out a meeting request:

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

# Process an incoming MeetingCancellation 

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

Exchangelib Troubleshooting

If you are having trouble using Exchangelib Python, the first thing to try is to enable debug logging. This will output information about what is going on, most notably the actual XML documents at the source of the problem. 

import logging

from exchangelib.util import PrettyXmlHandler

from exchangelib import CalendarItem

# Handler that  pretty-prints and syntax highlights 

# request and response XML documents:

logging.basicConfig(level=logging.DEBUG, handlers=[PrettyXmlHandler()])

# Most class definitions have a docstring containing 

# a URL to the MSDN page for the corresponding XML element.

# Your code that uses Exchangelib Python, and needs debugging goes here:

print(CalendarItem.__doc__)
Credentials function code - exchangelib
Figure 2. Credentials function code is checked to see if it needs debugging

A modern solution to Python package management – Try ActiveState’s Platform

Dependency resolution is at the core of the ActiveState Platform. When you create a project and start adding requirements, the Platforms tell you what dependencies those requirements have.

The ActiveState Platform is a cloud-based build tool for Python. It provides build automation and vulnerability remediation for:

  • Python language cores, including Python 2.7 and Python 3.5+
  • Python packages and their dependencies, including:
  • Transitive dependencies (ie., dependencies of dependencies)
  • Linked C and Fortran libraries, so you can build data science packages
  • Operating system-level dependencies for Windows, Linux, and macOS
  • Shared dependencies (ie., OpenSSL)
  • Find, fix and automatically rebuild a secure version of Python packages like Django and environments in minutes

Python 3.9 Web GUI ScreenshotThe ActiveState Platform aims to handle every dependency for every language. That means handling libraries down to the C/C++ level, external tools, and all the conditional dependencies that exist. To take things even further, our ultimate goal is to support multi-language projects. That means that you can create a project using both Python and Perl packages, and we’ll make sure that both languages are using the same (up to date) OpenSSL version.

Python Package Management In Action

Get a hands-on appreciation for how the ActiveState Platform can help you manage your dependencies for Python environments. Just run the following command to install Python 3.9 and our package manager, the State Tool:

Windows

powershell -Command "& $([scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://platform.activestate.com/dl/cli/install.ps1'))) -activate-default ActiveState-Labs/Python-3.9Beta"

Linux

sh <(curl -q https://platform.activestate.com/dl/cli/install.sh) --activate-default ActiveState-Labs/Python-3.9Beta

Now you can run state install <packagename>. Learn more about how to use the State Tool to manage your Python environment.

Let us know your experience in the ActiveState Community forum.

Watch this video to learn how to use the ActiveState Platform to create a Python 3.9 environment, and then use the Platform’s CLI (State Tool) to install and manage it.

Suhani S