Download Outlook Email Attachments with/without OKTA Configuration

Sheranga Gamwasam
5 min readFeb 25, 2024

--

Downloading email attachments manually is a huge headache for anyone who works on them on a daily basis. Let’s say that you are a person or a company that receives hundreds of thousands of emails daily and you want to download one attachment from a specific email recipient / specific date range / specific subject / specific file extension. Just Imagine the hustle and the time you are consuming for that 😓

We are aware that there are several Python libraries such as smtplib, imaplib, and pywin32 that we can use to download email attachments, but these won’t work if your company uses Okta for Microsoft Account.

Therefore, from this article, I’m going to provide you with step-by-step guidance which you can follow to download Outlook email attachments with/without Okta Configuration. And I’ll be explaining the process with three main criteria,

  1. Create an App in Azure App Registration and Assign required API Permission
  2. Generate Access Token
  3. Download unread Email Attachments

Create an App in Azure App Registration and Assign required API Permission

Sign into the Azure portal using your email address and password. Once you’re signed into the portal, search App registration in the search bar on the home page and click on it to be redirected to the app registration page. Then click on the + New registration icon on the top of the page and enter the required details as mentioned in the below-given screenshot and click on the register button.

Once you click on the register button, your App (emailattachmentdownloadTest) is registered. You can view the created application in the “Owned Applications” section on the App registration page. After clicking on your application, go to the overview tab (Marked as “1” on the screenshot below) and from there you can also view Application ID, Object ID, Directory ID as well.

After that click on the “Certificates & Secret” tab (Marked as “2” on the screenshot above) and create New Client Secret (Make sure to get and save the value because it won’t be visible next time).

And here the final step would be to assign permission to your app. For that navigate to API Permission tab (Marked as “3” on the screenshot above) >> click Add a Permission icon >> select Microsoft Graph >> Select Delegated Permission >> Go to Mail >> Assign Mail.Read and Mail.ReadWrite permission

Now you have successfully completed the very first stage and received the required client credentials. Let’s move to the next step to generate an access token.

Generate Access Token

An authorization code is required to generate an access token from the msal python library. When you run the below code (“Section 1” only) it will redirect to a web browser and generate the code. (Redirect URL: http://localhost:8000/callback?code=xxxxx&session_state=yyyyy)

After generating the Authorization code, paste it in the below code “Section 2” snippet. After that, you can generate an access token and it will save it into your working directory as a Json file.

# generate_access_token.py

#################################################################
# Section 1
#################################################################

import webbrowse
from msal import ConfidentialClientApplication
import json
import config

APP_CONFIG = config.APP_CONFIG

client = ConfidentialClientApplication(
client_id=APP_CONFIG['CLIENT_ID'],
client_credential=APP_CONFIG['CLIENT_SECRET'])

authorization_url = client.get_authorization_request_url(
scopes=APP_CONFIG['SCOPES'])

webbrowser.open(authorization_url)

#################################################################
# Section 2
#################################################################

authorization_code = "xxxxx"

access_token = client.acquire_token_by_authorization_code(
code=authorization_code,
scopes=APP_CONFIG['SCOPES'])

with open('access_token.json', 'w') as fp:
json.dump(access_token, fp)"

But keep in mind that the access token expires every hour, if you need to generate a new access token, you have to use a refresh token. Otherwise, you have to run the above code and need to get the authorization code, again and again, every hour.

And then let’s move to the final stage…

Download Unread Email Attachments

Please refer to the below-embedded config.py, app.py and functions.py

config.py

Assign your app credentials for CLIENT_ID, CLIENT_SECRET and change the SAVE_FOLDER

APP_CONFIG = {
'CLIENT_ID' : "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
'CLIENT_SECRET' : "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
'SCOPES' : ['Mail.Read', 'Mail.ReadWrite'],
'INBOX_ENDPOINT' : "https://graph.microsoft.com/v1.0/me/mailFolders/Inbox/messages?$filter=isRead+ne+true",
'ACCESS_TOKEN_ENDPOINT' : "https://login.microsoftonline.com/common/oauth2/v2.0/token",
'SAVE_FOLDER' : r"XXXXXX\attachments"
}

app.py — it creates an access token then download attachments and mark as read the unread messages

import config
import json
import requests
import functions

APP_CONFIG = config.APP_CONFIG

try:
with open('access_token.json') as f:
access_token = json.load(f)

access_token = functions.generate_new_access_token(
APP_CONFIG,access_token)

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

response = requests.get(
url=APP_CONFIG['INBOX_ENDPOINT'], headers=headers)

inbox_emails = response.json()

functions.download_email_attachment(
inbox_emails,
headers,
access_token,
save_folder=APP_CONFIG['SAVE_FOLDER'])

except Exception as error:
print(error)

functions.py — include three functions which generate access token from refresh token, download email attachments and mark as read once we download the attachments

import requests
import os
import json

def generate_new_access_token(APP_CONFIG, access_token):

payload_dic = {'client_id' : APP_CONFIG['CLIENT_ID'],
'scope': 'Mail.Read%20Mail.ReadWrite',
'refresh_token': access_token['refresh_token'],
'grant_type' : 'refresh_token',
'client_secret': APP_CONFIG['CLIENT_SECRET']
}

x = json.dumps(payload_dic)
payload = x.replace('"', '').replace(': ', '=').replace('{', '').replace('}', '').replace(', ', '&')
headers = {'Content-Type': 'application/x-www-form-urlencoded'}

try:
response = requests.request("POST", url=APP_CONFIG['ACCESS_TOKEN_ENDPOINT'], headers=headers, data=payload)
access_token = json.loads(response.text)

with open('access_token.json', 'w') as fp:
json.dump(access_token, fp)

except Exception as error:
print(error)

return access_token


def download_email_attachment(inbox_emails, headers, access_token, save_folder):

GRAPH_API_ENDPOINT = 'https://graph.microsoft.com/v1.0'
total_email_count = len(inbox_emails['value'])

for i in range(0,total_email_count):
message_id = inbox_emails['value'][i]['id']

if inbox_emails['value'][i]['hasAttachments']==True:
attachment_response = requests.get(GRAPH_API_ENDPOINT + '/me/messages/{0}/attachments'.format(message_id),headers=headers)

if attachment_response.status_code==200:
attachment_items = attachment_response.json()['value']

for attachment in attachment_items:
file_name = attachment['name']
attachment_id = attachment['id']
attachment_content = requests.get(GRAPH_API_ENDPOINT + '/me/messages/{0}/attachments/{1}/$value'.format(message_id, attachment_id),headers=headers)
print('Saving file {0}...'.format(file_name))
with open(os.path.join(save_folder, file_name), 'wb') as _f:
_f.write(attachment_content.content)

status_code = mark_as_read_emails(message_id, access_token)


def mark_as_read_emails(message_id, access_token):

GRAPH_API_ENDPOINT = 'https://graph.microsoft.com/v1.0'
url = GRAPH_API_ENDPOINT + '/me/messages/{0}'.format(message_id)
payload = json.dumps({"isRead": True})

headers = {'Content-Type': 'application/json',
'Authorization' : 'Bearer ' + access_token['access_token']}

response = requests.request("PATCH", url, headers=headers, data=payload)

return response.status_code

If you completed all three stages successfully you will be able to download Outlook email attachments with/without OKTA configuration

Hope this article would help you!

If you need any further explanation, do feel free to reach me via LinkedIn or add a comment to this article itself. I’m always willing to support you all 😊

References :

--

--