Python: Read and send Outlook mail using OAuth2 token and Graph API

Manoj Kumar Dhakad
7 min readJun 30, 2023

--

Microsoft has announced the deprecation of basic authentication for Outlook, which involves using a username and password for authentication using imaplib easy to use python lib. Instead, they have introduced an alternative authentication method using OAuth tokens.

In order to read or send emails using Python, there are a few steps you need to follow. First, you need to obtain an access token. The access token is a security credential that grants your Python application permission to access the email services

Please follow the below steps to get access token:

  1. Open a new incognito window in Google Chrome.
  2. Login to Outlook using the following credentials:

3. Next, login to the Azure Portal using the same credentials as above:

4. In the Azure Portal, navigate to the “App registrations” service.

5. Click on “+ New Registration” to create a new registration.

6. Fill in the required fields, including:

  • Name: [name of the registration]
  • Supported account types: [select the appropriate account types]
  • Redirect URI: https://localhost

7. Click on “Register” to complete the registration process.

Overview of your registered app should look like this

8. Now Open your registered app and proceed with the following configurations:

a. Certificates & secrets:

  • Click on “+ New client secret”.
  • Fill in the required details for the client secret, including a description.
  • Note: Make sure to copy the generated secret value for future reference and store it in a secure location, such as a text editor.

b. API permissions:

  • Click on “+ Add a permission”.
  • Select “Microsoft Graph”.
  • Choose “Delegated permissions”.
  • Search for “Mail” and select the relevant permissions you need, such as “Mail.Send” and “Mail.ReadWrite”.

9. There are multiple methods available to generate an OAuth token, including Python code, CLI (Command Line Interface), and Postman. In this case, we will be using Postman to generate the OAuth token.

(a) Open a new tab in your web browser where you have already signed in to Outlook and the Azure Portal.

Replace tenant id and client id of your app and modify scope, if you want to add any extra permission.

scope=offline_access%20Mail.ReadWrite%20Mail.send

https://login.microsoftonline.com/<<Directory (tenant) ID>>/oauth2/v2.0/authorize?client_id=<<Application (client) ID>>&response_type=code&redirect_uri=https%3A%2F%2Flocalhost&response_mode=query&scope=offline_access%20Mail.ReadWrite%20Mail.send&state=12345644

ex: https://login.microsoftonline.com/fa7xyzabc5a-5b34-4081-94ae-d2c178decxx1/oauth2/v2.0/authorize?client_id=23557290-cfe2-40b7-b646-9212d9d67456&response_type=code&redirect_uri=https%3A%2F%2Flocalhost&response_mode=query&scope=offline_access%20Mail.ReadWrite%20Mail.send&state=12345644

Once you hit enter and Accept, you will be able see a code in url itself, copy the code from it. Highleted section in image

Download (https://www.postman.com/downloads/) and Open the postman.

(b) Open new tab in postman and create a post request with below url and parameters and send

Request Type: Post
Request URL: https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/token
client_id: your client id
scope: offline_access Mail.ReadWrite Mail.send
code: you got in step (a)
redirect_uri: https://localhost
grant_type: authorization_code
client_secret: your secret

You will recieve response code 200. If status code is not 200, it means you made some mistake and you need to fix the parameters.

It’s important to note that the generated authorization code can only be used once. If you encounter an error or need to make changes to the parameters, you will need to generate a new code by repeating step (a) and following the process again.

Please ensure that you carefully validate the parameters and make any necessary changes before generating a new code using step (a) in order to proceed successfully

Request/Response:

To avoid having to recreate the token with a new code every time, you can generate the payload and headers using the refresh token and credentials obtained from the previous response. This approach allows you to refresh the token and obtain a new access token without having to repeat the entire authentication process.

Follow the below Steps:

(a).Duplicate the postman tab and use below parameters and values.

Request Type: Post
Request URL: https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/token
client_id: your client id
scope: offline_access Mail.ReadWrite Mail.send
grant_type: refresh_token
client_secret: your secret
refresh_token: refresh token from above response

(b). Once you receive a response with a code of 200, you can proceed by clicking on the Code <> symbol located at the top right corner of Postman. Copy the generated code which should include the URL, payload, headers, and request details.

You can utilize the copied URL, payload, headers, and request in your Python code to generate the OAuth token multiple times for reading and writing emails. The code provided below contains functions for reading mail, downloading attachments, sending mail, and sending attachments.

To download an attachment, you will need the message ID and the attachment ID. These IDs are specific identifiers associated with the email message and its attachments.

You need to use graph api end point in python requests.

import ast
import base64
import logging
import os
import requests
from tqdm import tqdm

logging.basicConfig(format='[%(asctime)s]:[%(levelname)s]:[%(filename)s %(lineno)d]:[%(funcName)s]:%(message)s')
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

url = "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/token"

payload = 'from post main request with refresh token in postmina'
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Cookie': 'fpc=AotDC2wIAAACaFzHcDgAAAA; stsservicecookie=estsfd; x-ms-gateway-slice=estsfd'
}

try:
response = requests.post(url, headers=headers, data=payload)
response.raise_for_status() # Raise an exception if request fails

result = ast.literal_eval(str(response.text))

except requests.exceptions.RequestException as e:
logger.exception("An error occurred while requesting the token")


def mail_read():
if "access_token" in result:
subject = r"Test"
endpoint = f'https://graph.microsoft.com/v1.0/me/messages'
params = {"$search": f'"subject:{subject}"', "top": 1}

try:
r = requests.get(endpoint, headers={'Authorization': 'Bearer ' + result['access_token']}, params=params)
r.raise_for_status() # Raise an exception if request fails

if r.ok:
print('Retrieved emails successfully')
data = r.json()["value"][0]
print(data['subject'])
print(data['bodyPreview'])
print(data['body']['content'])

except requests.exceptions.RequestException as e:
logger.exception("An error occurred while retrieving emails")


def download_attachment():
if "access_token" in result:
subject = r"Test"
endpoint = f'https://graph.microsoft.com/v1.0/me/messages'
params = {"$search": f'"subject:{subject}"', "top": 1}

try:
r = requests.get(endpoint, headers={'Authorization': 'Bearer ' + result['access_token']}, params=params)
r.raise_for_status() # Raise an exception if request fails

if r.ok:
print('Retrieved emails successfully')
data = r.json()["value"][0]

# getting message id
message_id = data["id"]

endpoint_attachment = endpoint + "/" + message_id + "/attachments/"
r = requests.get(endpoint_attachment, headers={'Authorization': 'Bearer ' + result['access_token']})
r.raise_for_status() # Raise an exception if request fails

# Getting the last attachment id
attachment_id = r.json().get('value')[-1].get('id')

endpoint_attachment_file = endpoint_attachment + "/" + attachment_id + "/$value"

res = requests.get(url=endpoint_attachment_file,
headers={'Authorization': 'Bearer ' + result['access_token']}, stream=True)
res.raise_for_status() # Raise an exception if request fails

file_size = len(r.content)
# change the .csv/.xlsx extension as per attachment
name = "mail_attachment_data" + ".csv"
with open(f"{name}", 'wb') as f, tqdm(unit='iB', unit_scale=True, unit_divisor=1024, total=file_size,
desc=f"Downloading {name}") as pbar:
for data in res.iter_content(chunk_size=1024):
pbar.update(len(data))
print(data)
f.write(data)

except requests.exceptions.RequestException as e:
logger.exception("An error occurred while downloading the attachment")


def draft_attachment(files):
if not os.path.exists(files):
logger.info('File is not found')
return

with open(files, 'rb') as upload:
media_content = base64.b64encode(upload.read())

data_body = {
'@odata.type': '#microsoft.graph.fileAttachment',
'contentBytes': media_content.decode('utf-8'),
'name': os.path.basename(files),
}
return data_body


def send_small_file_email_attachment(senders_email, subject, files, mail_text):
html_content = f"""
<html>
<body>
{mail_text}
</body>
</html>
"""

if files is not None:
request_body = {
'message': {
# recipient list
'toRecipients': [
{
'emailAddress': {
'address': senders_email
}
}
],
# email subject
'subject': subject,
'importance': 'normal',

# include attachments
'attachments': [
draft_attachment(files)

]
}
}
else:
request_body = {
'message': {
# recipient list
'toRecipients': [
{
'emailAddress': {
'address': senders_email
}
}
],
# email subject
'subject': subject,
"body": {
"contentType": "html",
"content": html_content
},
'importance': 'normal',

}
}

headers = {
'Authorization': 'Bearer ' + result['access_token']
}

GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0'
endpoint = GRAPH_ENDPOINT + '/me/sendMail'

try:
response = requests.post(endpoint, headers=headers, json=request_body)
response.raise_for_status() # Raise an exception if request fails

if response.status_code == 202:
logger.info(f"Email sent to: {senders_email}")
else:
logger.exception(f"Email not sent to: {senders_email}")

except requests.exceptions.RequestException as e:
logger.exception("An error occurred while sending the email")


mail_read()
download_attachment()
send_small_file_email_attachment("test@gmail.com", "Test", 'data.txt', "Test")

In Conclusion, leveraging the Microsoft Graph API and OAuth token in Outlook allows for powerful capabilities in reading and writing emails programmatically. By following the steps outlined in this article, you can generate an OAuth token using Postman, retrieve essential details such as the URL, payload, headers, and request, and utilize them in your Python code.

With the provided code snippets and functions, you can easily perform tasks such as reading mail, downloading attachments, sending mail, and sending attachments. This enables automation and integration of email functionality within your applications, providing enhanced productivity and efficiency.

By harnessing the potential of the Microsoft Graph API and OAuth token, you can unlock a world of possibilities for email management and streamline your workflows. Feel free to explore further resources and documentation to dive deeper into the topic and customize the implementation to suit your specific needs.

--

--