From 0f42bc77599990d1c4eb20b7a9463334513e72b4 Mon Sep 17 00:00:00 2001 From: Gerrit Oomens Date: Sun, 7 Feb 2021 15:33:04 +0100 Subject: [PATCH] Initial commit --- .gitignore | 2 ++ README.md | 37 +++++++++++++++++++++++++++++++++++++ config.py | 4 ++++ get_token.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config.py create mode 100644 get_token.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5016749 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +refresh_token \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..881c9ab --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Using OfflineIMAP with M365 + +This instruction describes how [OfflineIMAP](https://www.offlineimap.org/) can be used with an IMAP-enabled Exchange Online (M365) environment using OAuth2. Note that the tenant must be configured to support IMAP over 'modern authentication' and that it requires app consent to be granted for a mail app for which you have the client ID and secret. + +A variation of the below should work with any OAuth-enabled mail client or script. + +## Step 1: get a client ID/secret +In order to connect to Azure AD for authentication, you need a client ID and secret (an "app registration" in AAD). Confusingly, the client secret doesn't actually need to be a secret (a client app like a mail client can't keep secrets, after all). You can your own app registration, or use an existing one such as Thunderbird's, which is [publicly available](https://hg.mozilla.org/comm-central/file/tip/mailnews/base/src/OAuth2Providers.jsm) (see the `login.microsoft.com` section). Whatever client ID you use, it will need to have been granted the `IMAP.AccessAsUser.All` permission in your M365 tenant. + +## Step 2: get a token +Since OfflineIMAP doesn't support an interactive flow for getting a token, you need to do this step yourself. You can use `get_token.py` for this purpose, which uses Microsoft's [MSAL](https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-overview) wrapper library to perform the OAuth2 flow: + +```sh +git clone https://github.com/UvA-FNWI/M365-IMAP +cd M365-IMAP +pip install msal +# add the client ID and secret from step 1 to config.py +python3 get_token.py +``` + +Follow the instructions to obtain a `refresh_token` file containing an AAD refresh token. Note that the token allows access to your full mailbox (in combination with the client 'secret') and hence should be stored securely. + +## Step 3: configure OfflineIMAP +Edit your `.offlineimaprc` file so that your remote repository section looks like this: + +```toml +[Repository Remote] +type = IMAP +sslcacertfile = +remotehost = outlook.office365.com +remoteuser = +auth_mechanisms = XOAUTH2 +oauth2_request_url = https://login.microsoftonline.com/common/oauth2/v2.0/token +oauth2_client_id = +oauth2_client_secret = +oauth2_refresh_token = +``` \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..361f160 --- /dev/null +++ b/config.py @@ -0,0 +1,4 @@ +ClientId = "" +ClientSecret = "" +Scopes = ['https://outlook.office365.com/IMAP.AccessAsUser.All'] +OutputFileName = "refresh_token" \ No newline at end of file diff --git a/get_token.py b/get_token.py new file mode 100644 index 0000000..51fca8f --- /dev/null +++ b/get_token.py @@ -0,0 +1,34 @@ +from msal import ConfidentialClientApplication, SerializableTokenCache +import config +import sys + +redirect_uri = "http://localhost" + +# We use the cache to extract the refresh token +cache = SerializableTokenCache() +app = ConfidentialClientApplication(config.ClientId, client_credential=config.ClientSecret, token_cache=cache) + +url = app.get_authorization_request_url(config.Scopes, redirect_uri=redirect_uri) + +print('Navigate to the following url in a web browser:') +print(url) + +print() + +print('After login, you will be redirected to a blank (or error) page with a url containing an access code. Paste the url below.') +resp = input('Response url: ') + +i = resp.find('code') + 5 +code = resp[i : resp.find('&', i)] if i > 4 else resp + +token = app.acquire_token_by_authorization_code(code, config.Scopes, redirect_uri=redirect_uri) + +print() + +if 'error' in token: + print(token) + sys.exit("Failed to get access token") + +print(f'Access token acquired, writing to file {config.OutputFileName}') +with open(config.OutputFileName, 'w') as f: + f.write(cache.find('RefreshToken')[0]['secret'])