What OAuth permissions needed for exchangelib?

Copper Contributor

My end goal is to have a script that moves a single users mail around (archiving stuff etc.). Right now I'm just trying to be able to look at the mail. I'm using a python library called exchangelib. However, I can't seem to get the permissions right. Here's the code I'm using

 

from exchangelib import (
    Account,
    Configuration,
    OAuth2Credentials,
    DELEGATE,
    OAUTH2,
)
from os import environ

username = environ["USERNAME"]
client_id = environ["CLIENT_ID"]
tenant_id = environ["TENANT_ID"]
secret_value = environ["VALUE"]

credentials = OAuth2Credentials(
    client_id=client_id, tenant_id=tenant_id, client_secret=secret_value
)
conf = Configuration(
    credentials=credentials, server="outlook.office365.com", auth_type=OAUTH2
)
account = Account(
    primary_smtp_address=username,
    autodiscover=False,
    config=conf,
    access_type=DELEGATE,
)

And here's what the permissions look like in AzureAD

image161.png

 

And here's the error

 

Traceback (most recent call last):
  File "test.py", line 21, in <module>
    account = Account(
  File ".../account.py", line 133, in __init__
    self.version = self.protocol.version
  File ".../protocol.py", line 470, in version
    self.config.version = Version.guess(self, api_version_hint=self._api_version_hint)
  File ".../version.py", line 229, in guess
    list(ResolveNames(protocol=protocol).call(unresolved_entries=[name]))
  File ".../services/resolve_names.py", line 52, in _elems_to_objs
    for elem in elems:
  File ".../services/common.py", line 212, in _chunked_get_elements
    yield from self._get_elements(payload=payload_func(chunk, **kwargs))
  File ".../services/common.py", line 230, in _get_elements
    yield from self._response_generator(payload=payload)
  File ".../services/common.py", line 196, in _response_generator
    response = self._get_response_xml(payload=payload)
  File ".../services/common.py", line 310, in _get_response_xml
    r = self._get_response(payload=payload, api_version=api_version)
  File ".../services/common.py", line 265, in _get_response
    r, session = post_ratelimited(
  File ".../util.py", line 877, in post_ratelimited
    protocol.retry_policy.raise_response_errors(r)  # Always raises an exception
  File ".../protocol.py", line 689, in raise_response_errors
    raise UnauthorizedError('Invalid credentials for %s' % response.url)
exchangelib.errors.UnauthorizedError: Invalid credentials for https://outlook.office365.com/EWS/Exchange.asmx

 

By the way I've looked into a bunch of different methods of moving email but I'm dealing with a few hundred thousand emails and nothing else will do it in a reasonable time (except IMAP but... its IMAP). Specifically:

  • The web interface doesn't allow selecting and moving more than like 100 emails
  • The outlook desktop app wont move more than about 1000 emails at a time without the move crashing.
  • For some reason using the addon interface with C# was also unstable (I got a test to complete once but it failed like 6 times with no exceptions or anything)
  • The powershell command line thing that you connect to with like Connect-ExchangeOnline doesn't allow you to move individual emails.
  • Microsoft Graph rate limits you at 10,000 email moves per day.
2 Replies

@GarbledSpaceman 

 

I have exact the same problem ... help very appreciated

Maybe you're missing the `full_access_as_app` Office 365 Application permission. Here are some full instructions on how I managed to do the auth and send an e-mail to myself into an Exchange Online sandbox tenant: https://github.com/robocorp/example-oauth-email#microsoft-exchange-outlook

Main takeaways:
- Created Web App and authorized with the Authorization Code flow (with OAuth2 enabled in the tenant)
- Ensured enough permissions (Delegated / Application) for the app (even accepted them all in advance as the Principal user)
- Enabled Impersonation as well through PowerShell (so the app can send mails on user's behalf)