A simple tutorial for creating a Python app that uses the Outlook Mail API.
Python HTML
Latest commit 3139e72 Aug 2, 2016 @jasonjoh Merge branch 'refs/heads/cleanup'
# Conflicts:
#	README.md

README.md

Getting Started with the Outlook Mail API and Python

The purpose of this guide is to walk through the process of creating a simple Python web app that retrieves messages in Office 365 or Outlook.com. The source code in this repository is what you should end up with if you follow the steps outlined here.

Before you begin

This guide assumes:

  • That you already have Python and Django installed and working on your development machine. This sample was created using Python version 3.5.2 and Django 1.10.
  • That you have an Office 365 tenant, with access to an account in that tenant OR an Outlook.com developer preview account.

Create the app

Let's dive right in! Open a command prompt and set the current directory to a location where you want to store your code. Run the following command to create a new Django project.

django-admin.py startproject python_tutorial

This creates a new subdirectory called python_tutorial. Change your command prompt's current directory to this new subdirectory and run the following command to verify that everything is working.

python manage.py runserver

Once the server starts, open a web browser and browse to http://localhost:8000/. You should see a success message.

The default Django welcome page.

As the success message suggests, let's add an app to our new project. On the command prompt, run the following command.

python manage.py startapp tutorial

This creates a new subdirectory under the python_tutorial directory and a number of files.

Open the .\python_tutorial\settings.py file and add the new tutorial app to the INSTALLED_APPS setting.

New value of INSTALLED_APPS in .\python_tutorial\settings.py

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'tutorial',
)

Save your changes. From the command line, run the following command to initialize the database for the app.

python manage.py migrate

Let's add a bit of code to the app just to make sure that it is working. Open the .\tutorial\views.py file and add the following code.

home function in the .\tutorial\views.py file

from django.http import HttpResponse, HttpResponseRedirect

def home(request):
  return HttpResponse("Welcome to the tutorial.")

Create a new file in the tutorial directory called urls.py. Add the following code to this file.

Contents of the .\tutorial\urls.py file

from django.conf.urls import url 
from tutorial import views 

urlpatterns = [ 
  # The home view ('/tutorial/') 
  url(r'^$', views.home, name='home'), 
  # Explicit home ('/tutorial/home/') 
  url(r'^home/$', views.home, name='home'), 
]

Finally, open the .\python_tutorial\urls.py file and replace the contents with the following.

New contents of .\python_tutorial\urls.py

from django.conf.urls import url, include
from django.contrib import admin
from tutorial import views

urlpatterns = [
    # Invoke the home view in the tutorial app by default
    url(r'^$', views.home, name='home'),
    # Defer any URLS to the /tutorial directory to the tutorial app
    url(r'^tutorial/', include('tutorial.urls', namespace='tutorial')),
    url(r'^admin/', include(admin.site.urls)),
]

If you're familiar with Django development, this isn't anything new for you. If not, all we've done here is tell Django how to route requests to the tutorial app. Django looks first in the .\python_tutorial\urls.py file. The new lines we added there tell it to redirect requests to the root to the home view in the tutorial app, and also tells it to send any requests to /tutorial/* to the tutorial app.

The entries in the .\tutorial\urls.py file tell Django to send requests to either /tutorial or /tutorial/home to the home view. Finally, the home function in .\tutorial\views.py returns a simple HTTP response.

If you save all of your changes and navigate to http://localhost:8000 you should see "Welcome to the tutorial." Now that we've confirmed that the app is working, we're ready to do some real work.

Designing the app

Our app will be very simple. When a user visits the site, they will see a link to log in and view their email. Clicking that link will take them to the Azure login page where they can login with their Office 365 or Outlook.com account and grant access to our app. Finally, they will be redirected back to our app, which will display a list of the most recent email in the user's inbox.

Let's begin by replacing the static message with a signon link. To do that, we'll modify the home function in .\tutorial\views.py. Update the home function to match the following.

Updated home function

def home(request):
  sign_in_url = '#'
  return HttpResponse('<a href="' + sign_in_url +'">Click here to sign in and view your mail</a>')

As you can see, our home page will be very simple. For now, the link doesn't do anything, but we'll fix that soon.

Implementing OAuth2

Our goal in this section is to make the link on our home page initiate the OAuth2 Authorization Code Grant flow with Azure AD. To make things easier, we'll use the Requests: HTTP for Humans library to handle all of our HTTP requests. At your command prompt, enter the following command.

pip install requests

Now the library is installed and ready to use. Create a new file in the tutorial directory called authhelper.py. We'll start here by defining a function to generate the login URL.

Contents of the .\tutorial\authhelper.py file

from urllib.parse import quote, urlencode
import base64
import json
import time

# Client ID and secret
client_id = 'YOUR APP ID HERE'
client_secret = 'YOUR APP PASSWORD HERE'

# Constant strings for OAuth2 flow
# The OAuth authority
authority = 'https://login.microsoftonline.com'

# The authorize URL that initiates the OAuth2 client credential flow for admin consent
authorize_url = '{0}{1}'.format(authority, '/common/oauth2/v2.0/authorize?{0}')

# The token issuing endpoint
token_url = '{0}{1}'.format(authority, '/common/oauth2/v2.0/token')

# The scopes required by the app
scopes = [ 'openid',
           'https://outlook.office.com/mail.read' ]

def get_signin_url(redirect_uri):
  # Build the query parameters for the signin url
  params = { 'client_id': client_id,
             'redirect_uri': redirect_uri,
             'response_type': 'code',
             'scope': ' '.join(str(i) for i in scopes)
            }

  signin_url = authorize_url.format(urlencode(params))

  return signin_url

The first thing we do here is define our client ID and secret. The values of clientId and clientSecret are just placeholders, so we need to generate valid values.

Generate a client ID and secret

Head over to https://apps.dev.microsoft.com to quickly get a client ID and secret. Using the sign in buttons, sign in with either your Microsoft account (Outlook.com), or your work or school account (Office 365).

The Application Registration Portal Sign In Page

Once you're signed in, click the Add an app button. Enter python-tutorial for the name and click Create application. After the app is created, locate the Application Secrets section, and click the Generate New Password button. Copy the password now and save it to a safe place. Once you've copied the password, click Ok.

The new password dialog.

Locate the Platforms section, and click Add Platform. Choose Web, then enter http://localhost:8000/tutorial/gettoken/ under Redirect URIs. Click Save to complete the registration. Copy the Application Id and save it along with the password you copied earlier. We'll need those values soon.

Here's what the details of your app registration should look like when you are done.

The completed registration properties.

Replace the YOUR APP ID HERE and YOUR APP PASSWORD HERE placeholders in the .\tutorial\authhelper.py file with the values you generated and save your changes.

Back to coding

Now that we have actual values for the client ID and secret, let's put our new function to work. Modify the home function in the .\tutorial\views.py file to use the get_signin_url function to fill in the link. That function takes a parameter, redirect_uri. This value is used to set the URL in our app where Azure will redirect after signin is complete. Let's go ahead and create a placeholder view to act as our redirect target called gettoken.

Updated contents of .\tutorial\views.py

from django.shortcuts import render
from django.http import HttpResponse, HttpResponseRedirect
from django.core.urlresolvers import reverse
from tutorial.authhelper import get_signin_url

# Create your views here.

def home(request):
  redirect_uri = request.build_absolute_uri(reverse('tutorial:gettoken'))
  sign_in_url = get_signin_url(redirect_uri)
  return HttpResponse('<a href="' + sign_in_url +'">Click here to sign in and view your mail</a>')

def gettoken(request):
  return HttpResponse('gettoken view')

The view doesn't do much now, but we'll change that soon. Add this new view to the .\tutorials\urls.py file.

Updated contents of the .\tutorials\urls.py file

from django.conf.urls import url 
from tutorial import views 

urlpatterns = [
  # The home view ('/tutorial/') 
  url(r'^$', views.home, name='home'), 
  # Explicit home ('/tutorial/home/') 
  url(r'^home/$', views.home, name='home'), 
  # Redirect to get token ('/tutorial/gettoken/')
  url(r'^gettoken/$', views.gettoken, name='gettoken'),
]

Save your changes and browse to http://localhost:8000. If you hover over the link, it should look like:

https://login.microsoftonline.com/common/oauth2/v2.0/authorize?scope=openid+https%3A%2F%2Foutlook.office.com%2Fmail.read&response_type=code&client_id=<SOME GUID>&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Ftutorial%2Fgettoken%2F

The <SOME GUID> portion should match your client ID. Click on the link and you should be presented with a sign in page. Sign in with your Office 365 or Outlook.com account. Your browser should redirect to back to the gettoken view. The view doesn't do anything yet, so let's fix that now.

Exchanging the code for a token

The first thing we need to do is extract the authorization code from the request. When Azure redirects to our gettoken function, it includes a code query parameter, which contains the authorization code. Update the gettoken function to get this parameter's value and display it.

Updated gettoken function in .\tutorial\views.py

def gettoken(request):
  auth_code = request.GET['code']
  return HttpResponse('Authorization code: {0}'.format(auth_code))

Save your changes and repeat the signin process in the app. Now instead of a static message, you should see the value of the authorization code printed on the screen. We're getting closer, but that's still not very useful. Let's actually do something with that code.

First import the requests library by adding the following line to the top of .\tutorial\authhelper.py.

import requests

Now add another helper function to authhelper.py called get_token_from_code.

get_token_from_code in the .\tutorial\authhelper.py file

def get_token_from_code(auth_code, redirect_uri):
  # Build the post form for the token request
  post_data = { 'grant_type': 'authorization_code',
                'code': auth_code,
                'redirect_uri': redirect_uri,
                'scope': ' '.join(str(i) for i in scopes),
                'client_id': client_id,
                'client_secret': client_secret
              }

  r = requests.post(token_url, data = post_data)

  try:
    return r.json()
  except:
    return 'Error retrieving token: {0} - {1}'.format(r.status_code, r.text)

Getting the user's email address

Our first use of the access token will be to get the user's email address from the Outlook API. You'll see why we want this soon.

Create a new file in the tutorial directory called outlookservice.py. We'll implement all of our Outlook API functions in this file. We'll start by creating a generic method for sending API requests called make_api_call.

Contents of ./tutorial/outlookservice.py

import requests
import uuid
import json

outlook_api_endpoint = 'https://outlook.office.com/api/v2.0{0}'

# Generic API Sending
def make_api_call(method, url, token, user_email, payload = None, parameters = None):
  # Send these headers with all API calls
  headers = { 'User-Agent' : 'python_tutorial/1.0',
              'Authorization' : 'Bearer {0}'.format(token),
              'Accept' : 'application/json',
              'X-AnchorMailbox' : user_email }

  # Use these headers to instrument calls. Makes it easier
  # to correlate requests and responses in case of problems
  # and is a recommended best practice.
  request_id = str(uuid.uuid4())
  instrumentation = { 'client-request-id' : request_id,
                      'return-client-request-id' : 'true' }

  headers.update(instrumentation)

  response = None

  if (method.upper() == 'GET'):
      response = requests.get(url, headers = headers, params = parameters)
  elif (method.upper() == 'DELETE'):
      response = requests.delete(url, headers = headers, params = parameters)
  elif (method.upper() == 'PATCH'):
      headers.update({ 'Content-Type' : 'application/json' })
      response = requests.patch(url, headers = headers, data = json.dumps(payload), params = parameters)
  elif (method.upper() == 'POST'):
      headers.update({ 'Content-Type' : 'application/json' })
      response = requests.post(url, headers = headers, data = json.dumps(payload), params = parameters)

  return response

This function uses the requests library to send API requests. It sets a standard set of headers on each requests, including client instrumentation.

It also uses the email address we retrieved from the ID token to set the X-AnchorMailbox header. By setting this header, we enable the API endpoint to route API calls to the correct backend mailbox server more efficiently. This is why we want to get the user's email address.

Now let's create a function to make us of the make_api_call function to get the user. Add a function called get_me to outlookservice.py.

The get_me function in ./tutorial/outlookservice.py

def get_me(access_token):
  get_me_url = outlook_api_endpoint.format('/Me')

  # Use OData query parameters to control the results
  #  - Only return the DisplayName and EmailAddress fields
  query_parameters = {'$select': 'DisplayName,EmailAddress'}

  r = make_api_call('GET', get_me_url, access_token, "", parameters = query_parameters)

  if (r.status_code == requests.codes.ok):
    return r.json()
  else:
    return "{0}: {1}".format(r.status_code, r.text)

Let's make sure that works. Modify the gettoken function in views.py to use these helper functions and display the return values.

Updated gettoken function in .\tutorial\views.py

# Add import statement to include new function
from tutorial.outlookservice import get_me

def gettoken(request):
  auth_code = request.GET['code']
  redirect_uri = request.build_absolute_uri(reverse('tutorial:gettoken'))
  token = get_token_from_code(auth_code, redirect_uri)
  access_token = token['access_token']
  user = get_me(access_token)

  # Save the token in the session
  request.session['access_token'] = access_token
  request.session['user_email'] = user['EmailAddress']
  return HttpResponse('User Email: {0}, Access token: {1}'.format(user['EmailAddress'], access_token))

If you save your changes, restart the server, and go through the sign-in process again, you should now see a long string of seemingly nonsensical characters. If everything's gone according to plan, that should be an access token.

Refreshing the access token

Access tokens returned from Azure are valid for an hour. If you use the token after it has expired, the API calls will return 401 errors. You could ask the user to sign in again, but the better option is to refresh the token silently.

In order to do that, the app must request the offline_access scope. Add this scope to the scopes array in authhelper.py:

# The scopes required by the app
scopes = [ 'openid',
           'offline_access',
           'https://outlook.office.com/mail.read' ]

This will cause the token response from Azure to include a refresh token. Let's update gettoken in views.py to save the refresh token and the expiration time in the session.

Updated gettoken function in .\tutorial\views.py

# Add import statement to include new function
from tutorial.outlookservice import get_me

def gettoken(request):
  auth_code = request.GET['code']
  redirect_uri = request.build_absolute_uri(reverse('tutorial:gettoken'))
  token = get_token_from_code(auth_code, redirect_uri)
  access_token = token['access_token']
  user = get_me(access_token)
  refresh_token = token['refresh_token']
  expires_in = token['expires_in']

  # expires_in is in seconds
  # Get current timestamp (seconds since Unix Epoch) and
  # add expires_in to get expiration time
  # Subtract 5 minutes to allow for clock differences
  expiration = int(time.time()) + expires_in - 300

  # Save the token in the session
  request.session['access_token'] = access_token
  request.session['refresh_token'] = refresh_token
  request.session['token_expires'] = expiration
  request.session['user_email'] = user['EmailAddress']
  return HttpResponse('User Email: {0}, Access token: {1}'.format(user['EmailAddress'], access_token))

Now let's create a function to refresh the access token. Add the following function to authhelper.py.

The get_token_from_refresh_token function in ./tutorial/authhelper.py

def get_token_from_refresh_token(refresh_token, redirect_uri):
  # Build the post form for the token request
  post_data = { 'grant_type': 'refresh_token',
                'refresh_token': refresh_token,
                'redirect_uri': redirect_uri,
                'scope': ' '.join(str(i) for i in scopes),
                'client_id': client_id,
                'client_secret': client_secret
              }

  r = requests.post(token_url, data = post_data)

  try:
    return r.json()
  except:
    return 'Error retrieving token: {0} - {1}'.format(r.status_code, r.text)

Finally let's create a helper function to retrieve the access token. The function will check the expiration time, and if the token is expired, will refresh it. Otherwise it will just return the access token from the session. Add the following function to authhelper.py.

The get_access_token function in ./tutorial/authhelper.py

def get_access_token(request, redirect_uri):
  current_token = request.session['access_token']
  expiration = request.session['token_expires']
  now = int(time.time())
  if (current_token && now < expiration):
    // Token still valid
    return current_token
  else:
    // Token expired
    refresh_token = request.session['refresh_token']
    new_tokens = get_token_from_refresh_token(refresh_token, redirect_uri)

    // Update session
    # expires_in is in seconds
    # Get current timestamp (seconds since Unix Epoch) and
    # add expires_in to get expiration time
    # Subtract 5 minutes to allow for clock differences
    expiration = int(time.time()) + new_tokens['expires_in'] - 300

    # Save the token in the session
    request.session['access_token'] = new_tokens['access_token']
    request.session['refresh_token'] = new_tokens['refresh_token']
    request.session['token_expires'] = expiration

    return new_tokens['access_token']

Let's import get_access_token in views.py so we can make use of it.

from tutorial.authhelper import get_signin_url, get_token_from_code, get_access_token

Using the Mail API

Now that we can get an access token, we're in a good position to do something with the Mail API. Let's start by creating a mail view in views.py.

mail function in .\tutorial\views.py

def mail(request):
  access_token = get_access_token(request, request.build_absolute_uri(reverse('tutorial:gettoken')))
  user_email = request.session['user_email']
  # If there is no token in the session, redirect to home
  if not access_token:
    return HttpResponseRedirect(reverse('tutorial:home'))
  else:
    return HttpResponse('Access token found in session: {0}'.format(access_token))

Update the urls.py file to include an entry for the new view.

Updated contents of .\tutorial\urls.py

from django.conf.urls import url
from tutorial import views 

urlpatterns = [ 
  # The home view ('/tutorial/') 
  url(r'^$', views.home, name='home'), 
  # Explicit home ('/tutorial/home/') 
  url(r'^home/$', views.home, name='home'), 
  # Redirect to get token ('/tutorial/gettoken/')
  url(r'^gettoken/$', views.gettoken, name='gettoken'),
  # Mail view ('/tutorial/mail/')
  url(r'^mail/$', views.mail, name='mail'),
]

Update the gettoken function to redirect to the mail view after saving the token in the session.

Updated gettoken function in .\tutorial\views.py

def gettoken(request):
  auth_code = request.GET['code']
  redirect_uri = request.build_absolute_uri(reverse('tutorial:gettoken'))
  token = get_token_from_code(auth_code, redirect_uri)
  access_token = token['access_token']
  user = get_me(access_token)

  # Save the token in the session
  request.session['access_token'] = access_token
  request.session['user_email'] = user['EmailAddress']
  return HttpResponseRedirect(reverse('tutorial:mail'))

For now all this does is read the token back from the cookie and display it. Save your changes, restart the server, and go through the signon process again. You should see the token displayed. Now that we know we have access to the token in the mail function, we're ready to call the Mail API.

Let's add a function that will use the make_api_call function to implement a request to retrieve messages from the inbox. Create a new function in outlookservice.py called get_my_messages.

The get_my_messages function in ./tutorial/outlookservice.py

def get_my_messages(access_token, user_email):
  get_messages_url = outlook_api_endpoint.format('/Me/MailFolders/Inbox/Messages')

  # Use OData query parameters to control the results
  #  - Only first 10 results returned
  #  - Only return the ReceivedDateTime, Subject, and From fields
  #  - Sort the results by the ReceivedDateTime field in descending order
  query_parameters = {'$top': '10',
                      '$select': 'ReceivedDateTime,Subject,From',
                      '$orderby': 'ReceivedDateTime DESC'}

  r = make_api_call('GET', get_messages_url, access_token, user_email, parameters = query_parameters)

  if (r.status_code == requests.codes.ok):
    return r.json()
  else:
    return "{0}: {1}".format(r.status_code, r.text)

Now we can modify the mail function to call this function and retrieve email. First, import the get_my_messages function by adding the following line to the top of views.py.

from tutorial.outlookservice import get_me, get_my_messages

Then update the mail function to call the new function.

New version of the mail function in ./tutorial/views.py

def mail(request):
  access_token = get_access_token(request, request.build_absolute_uri(reverse('tutorial:gettoken')))
  user_email = request.session['user_email']
  # If there is no token in the session, redirect to home
  if not access_token:
    return HttpResponseRedirect(reverse('tutorial:home'))
  else:
    messages = get_my_messages(access_token, user_email)
    return HttpResponse('Messages: {0}'.format(messages))

If you save the changes and sign into the app, you should now see a raw listing of the JSON response.

Displaying the results

While the current listing of messages confirms that the API calls are working, we can use Django templates to display the results in a more user-friendly fashion.

Start by creating a new subdirectory in the tutorial directory called templates. In the templates directory, create a new subdirectory called tutorial. Finally, create a new file in this directory called mail.html, and add the following code.

Contents of the ./tutorial/templates/tutorial/mail.html file

<html>
  <body>
    <h1>Your Email</h1>
    <table width="100%" border="1">
      <tr>
        <th>From</th>
        <th>Subject</th>
        <th>Received</th>
      </tr>

      {% for message in messages %}
        <tr>
          <td>{{ message.From.EmailAddress.Name }}</td>
          <td>{{ message.Subject }}</td>
          <td>{{ message.ReceivedDateTime }}</td>
        </tr>
      {% endfor %}
    </table>
  </body>
</html>

Update the mail function in views.py to use this new template.

Updated mail function in ./tutorial/views.py

def mail(request):
  access_token = get_access_token(request, request.build_absolute_uri(reverse('tutorial:gettoken')))
  user_email = request.session['user_email']
  # If there is no token in the session, redirect to home
  if not access_token:
    return HttpResponseRedirect(reverse('tutorial:home'))
  else:
    messages = get_my_messages(access_token, user_email)
    context = { 'messages': messages['value'] }
    return render(request, 'tutorial/mail.html', context)

Save the changes and sign in to the app. You should now see a simple table of messages in your inbox.

The finished app displaying the user's inbox.

Next Steps

Now that you've created a working sample, you may want to learn more about the capabilities of the Mail API. If your sample isn't working, and you want to compare, you can download the end result of this tutorial from GitHub.

Copyright

Copyright (c) Microsoft. All rights reserved.


Connect with me on Twitter @JasonJohMSFT

Follow the Exchange Dev Blog