├── .gitattributes ├── .gitignore ├── LICENSE.TXT ├── README.md ├── manage.py ├── python_tutorial ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── readme-images ├── django_welcome.PNG ├── inbox-listing.PNG ├── new-password.PNG ├── python-tutorial.PNG ├── registration-step2.PNG ├── registration-step3.PNG └── sign-in.PNG ├── requirements.txt └── tutorial ├── __init__.py ├── admin.py ├── authhelper.py ├── migrations └── __init__.py ├── models.py ├── outlookservice.py ├── templates └── tutorial │ ├── contacts.html │ ├── events.html │ ├── home.html │ ├── layout.html │ └── mail.html ├── tests.py ├── urls.py └── views.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | 45 | # Django stuff 46 | debug.log 47 | *.sqlite3 48 | /tutorial/__pycache__/* 49 | !/tutorial/migrations 50 | /tutorial/migrations/* 51 | !/tutorial/migrations/__init__.py 52 | /python_tutorial/__pycache__/* 53 | readme.html 54 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | python_tutorial, https://github.com/jasonjoh/python_tutorial 2 | 3 | Copyright (c) Microsoft Corporation 4 | All rights reserved. 5 | 6 | MIT License: 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining 9 | a copy of this software and associated documentation files (the 10 | ""Software""), to deal in the Software without restriction, including 11 | without limitation the rights to use, copy, modify, merge, publish, 12 | distribute, sublicense, and/or sell copies of the Software, and to 13 | permit persons to whom the Software is furnished to do so, subject to 14 | the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with the Microsoft Graph Outlook Mail API and Python 2 | 3 | The sample code in this repository is the end result of going through the [Python tutorial on the Outlook Dev Center](https://docs.microsoft.com/en-us/outlook/rest/python-tutorial). If you go through that tutorial yourself, you should end up with code very similar to this. If you download or fork this repository, you'll need to follow the steps in [Configure the sample](#configure-the-sample) to run it. 4 | 5 | > **NOTE:** Looking for the version of this tutorial that used the Outlook API directly instead of Microsoft Graph? Check out the `outlook-api` branch. Note that Microsoft recommends using the Microsoft Graph to access mail, calendar, and contacts. You should use the Outlook APIs directly (via https://outlook.office.com/api) only if you require a feature that is not available on the Graph endpoints. 6 | 7 | ## Prerequisites 8 | 9 | - Python version 3.5.2 or later. 10 | - Django version 1.10 or later. 11 | - An Office 365 tenant, with access to an administrator account in that tenant, **OR** an Outlook.com account. 12 | 13 | ## Register the app 14 | 15 | Head over to https://apps.dev.microsoft.com to quickly get a application ID and password. Click the **Sign in** link and sign in with either your Microsoft account (Outlook.com), or your work or school account (Office 365). 16 | 17 | 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**. 18 | 19 | ![The new password dialog.](./readme-images/new-password.PNG) 20 | 21 | Locate the **Platforms** section, and click **Add Platform**. Choose **Web**, then enter `http://localhost:8000/tutorial/gettoken/` under **Redirect URIs**. 22 | 23 | > **NOTE:** The values in **Redirect URIs** are case-sensitive, so be sure to match the case! 24 | 25 | 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. 26 | 27 | Here's what the details of your app registration should look like when you are done. 28 | 29 | ![The completed registration properties.](./readme-images/python-tutorial.PNG) 30 | 31 | ## Configure the sample 32 | 33 | 1. Open the `.\tutorial\authhelper.py` file. 34 | 1. Replace `YOUR APP ID HERE` with the **Application Id** from the registration you just created. 35 | 1. Replace `YOUR APP PASSWORD HERE` with the password you copied earlier. 36 | 1. Install dependencies by entering `pip install -r requirements.txt` at the command prompt. 37 | 1. Run migrations by entering `python manage.py migrate` from the command prompt. 38 | 1. Run the project by entering `python manage.py runserver` from the command prompt. 39 | 40 | ## Copyright ## 41 | 42 | Copyright (c) Microsoft. All rights reserved. 43 | 44 | ---------- 45 | Connect with me on Twitter [@JasonJohMSFT](https://twitter.com/JasonJohMSFT) 46 | 47 | Follow the [Outlook/Exchange Dev Blog](https://blogs.msdn.microsoft.com/exchangedev/) -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "python_tutorial.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /python_tutorial/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonjoh/python_tutorial/b69d03986a6a3c075b3c0a3c88b5ba6caa6d8b2a/python_tutorial/__init__.py -------------------------------------------------------------------------------- /python_tutorial/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for python_tutorial project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.10. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.10/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = 'v@*)_*6xx)#t@4np043msmvg%^ez2p46ke#2*dtsf_bnvgxjuj' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | TEMPLATE_DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = ( 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'tutorial', 42 | ) 43 | 44 | MIDDLEWARE = ( 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ) 52 | 53 | ROOT_URLCONF = 'python_tutorial.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'python_tutorial.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | # Internationalization 85 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 86 | 87 | LANGUAGE_CODE = 'en-us' 88 | 89 | TIME_ZONE = 'UTC' 90 | 91 | USE_I18N = True 92 | 93 | USE_L10N = True 94 | 95 | USE_TZ = True 96 | 97 | 98 | # Static files (CSS, JavaScript, Images) 99 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 100 | 101 | STATIC_URL = '/static/' 102 | -------------------------------------------------------------------------------- /python_tutorial/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, include 2 | from django.contrib import admin 3 | from tutorial import views 4 | 5 | urlpatterns = [ 6 | # Invoke the home view in the tutorial app by default 7 | url(r'^$', views.home, name='home'), 8 | # Defer any URLS to the /tutorial directory to the tutorial app 9 | url(r'^tutorial/', include('tutorial.urls', namespace='tutorial')), 10 | url(r'^admin/', admin.site.urls), 11 | ] 12 | -------------------------------------------------------------------------------- /python_tutorial/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for python_tutorial project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "python_tutorial.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /readme-images/django_welcome.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonjoh/python_tutorial/b69d03986a6a3c075b3c0a3c88b5ba6caa6d8b2a/readme-images/django_welcome.PNG -------------------------------------------------------------------------------- /readme-images/inbox-listing.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonjoh/python_tutorial/b69d03986a6a3c075b3c0a3c88b5ba6caa6d8b2a/readme-images/inbox-listing.PNG -------------------------------------------------------------------------------- /readme-images/new-password.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonjoh/python_tutorial/b69d03986a6a3c075b3c0a3c88b5ba6caa6d8b2a/readme-images/new-password.PNG -------------------------------------------------------------------------------- /readme-images/python-tutorial.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonjoh/python_tutorial/b69d03986a6a3c075b3c0a3c88b5ba6caa6d8b2a/readme-images/python-tutorial.PNG -------------------------------------------------------------------------------- /readme-images/registration-step2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonjoh/python_tutorial/b69d03986a6a3c075b3c0a3c88b5ba6caa6d8b2a/readme-images/registration-step2.PNG -------------------------------------------------------------------------------- /readme-images/registration-step3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonjoh/python_tutorial/b69d03986a6a3c075b3c0a3c88b5ba6caa6d8b2a/readme-images/registration-step3.PNG -------------------------------------------------------------------------------- /readme-images/sign-in.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonjoh/python_tutorial/b69d03986a6a3c075b3c0a3c88b5ba6caa6d8b2a/readme-images/sign-in.PNG -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonjoh/python_tutorial/b69d03986a6a3c075b3c0a3c88b5ba6caa6d8b2a/requirements.txt -------------------------------------------------------------------------------- /tutorial/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonjoh/python_tutorial/b69d03986a6a3c075b3c0a3c88b5ba6caa6d8b2a/tutorial/__init__.py -------------------------------------------------------------------------------- /tutorial/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /tutorial/authhelper.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE.txt in the project root for license information. 2 | from urllib.parse import quote, urlencode 3 | import requests 4 | import base64 5 | import json 6 | import time 7 | 8 | # Client ID and secret 9 | client_id = 'YOUR APP ID HERE' 10 | client_secret = 'YOUR APP PASSWORD HERE' 11 | 12 | # Constant strings for OAuth2 flow 13 | # The OAuth authority 14 | authority = 'https://login.microsoftonline.com' 15 | 16 | # The authorize URL that initiates the OAuth2 client credential flow for admin consent 17 | authorize_url = '{0}{1}'.format(authority, '/common/oauth2/v2.0/authorize?{0}') 18 | 19 | # The token issuing endpoint 20 | token_url = '{0}{1}'.format(authority, '/common/oauth2/v2.0/token') 21 | 22 | # The scopes required by the app 23 | scopes = [ 'openid', 24 | 'offline_access', 25 | 'User.Read', 26 | 'Mail.Read', 27 | 'Calendars.Read', 28 | 'Contacts.Read' ] 29 | 30 | def get_signin_url(redirect_uri): 31 | # Build the query parameters for the signin url 32 | params = { 'client_id': client_id, 33 | 'redirect_uri': redirect_uri, 34 | 'response_type': 'code', 35 | 'scope': ' '.join(str(i) for i in scopes) 36 | } 37 | 38 | signin_url = authorize_url.format(urlencode(params)) 39 | 40 | return signin_url 41 | 42 | def get_token_from_code(auth_code, redirect_uri): 43 | # Build the post form for the token request 44 | post_data = { 'grant_type': 'authorization_code', 45 | 'code': auth_code, 46 | 'redirect_uri': redirect_uri, 47 | 'scope': ' '.join(str(i) for i in scopes), 48 | 'client_id': client_id, 49 | 'client_secret': client_secret 50 | } 51 | 52 | r = requests.post(token_url, data = post_data) 53 | 54 | try: 55 | return r.json() 56 | except: 57 | return 'Error retrieving token: {0} - {1}'.format(r.status_code, r.text) 58 | 59 | def get_token_from_refresh_token(refresh_token, redirect_uri): 60 | # Build the post form for the token request 61 | post_data = { 'grant_type': 'refresh_token', 62 | 'refresh_token': refresh_token, 63 | 'redirect_uri': redirect_uri, 64 | 'scope': ' '.join(str(i) for i in scopes), 65 | 'client_id': client_id, 66 | 'client_secret': client_secret 67 | } 68 | 69 | r = requests.post(token_url, data = post_data) 70 | 71 | try: 72 | return r.json() 73 | except: 74 | return 'Error retrieving token: {0} - {1}'.format(r.status_code, r.text) 75 | 76 | def get_access_token(request, redirect_uri): 77 | current_token = request.session['access_token'] 78 | expiration = request.session['token_expires'] 79 | now = int(time.time()) 80 | if (current_token and now < expiration): 81 | # Token still valid 82 | return current_token 83 | else: 84 | # Token expired 85 | refresh_token = request.session['refresh_token'] 86 | new_tokens = get_token_from_refresh_token(refresh_token, redirect_uri) 87 | 88 | # Update session 89 | # expires_in is in seconds 90 | # Get current timestamp (seconds since Unix Epoch) and 91 | # add expires_in to get expiration time 92 | # Subtract 5 minutes to allow for clock differences 93 | expiration = int(time.time()) + new_tokens['expires_in'] - 300 94 | 95 | # Save the token in the session 96 | request.session['access_token'] = new_tokens['access_token'] 97 | request.session['refresh_token'] = new_tokens['refresh_token'] 98 | request.session['token_expires'] = expiration 99 | 100 | return new_tokens['access_token'] -------------------------------------------------------------------------------- /tutorial/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonjoh/python_tutorial/b69d03986a6a3c075b3c0a3c88b5ba6caa6d8b2a/tutorial/migrations/__init__.py -------------------------------------------------------------------------------- /tutorial/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /tutorial/outlookservice.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE.txt in the project root for license information. 2 | import requests 3 | import uuid 4 | import json 5 | 6 | graph_endpoint = 'https://graph.microsoft.com/v1.0{0}' 7 | 8 | # Generic API Sending 9 | def make_api_call(method, url, token, payload = None, parameters = None): 10 | # Send these headers with all API calls 11 | headers = { 'User-Agent' : 'python_tutorial/1.0', 12 | 'Authorization' : 'Bearer {0}'.format(token), 13 | 'Accept' : 'application/json' } 14 | 15 | # Use these headers to instrument calls. Makes it easier 16 | # to correlate requests and responses in case of problems 17 | # and is a recommended best practice. 18 | request_id = str(uuid.uuid4()) 19 | instrumentation = { 'client-request-id' : request_id, 20 | 'return-client-request-id' : 'true' } 21 | 22 | headers.update(instrumentation) 23 | 24 | response = None 25 | 26 | if (method.upper() == 'GET'): 27 | response = requests.get(url, headers = headers, params = parameters) 28 | elif (method.upper() == 'DELETE'): 29 | response = requests.delete(url, headers = headers, params = parameters) 30 | elif (method.upper() == 'PATCH'): 31 | headers.update({ 'Content-Type' : 'application/json' }) 32 | response = requests.patch(url, headers = headers, data = json.dumps(payload), params = parameters) 33 | elif (method.upper() == 'POST'): 34 | headers.update({ 'Content-Type' : 'application/json' }) 35 | response = requests.post(url, headers = headers, data = json.dumps(payload), params = parameters) 36 | 37 | return response 38 | 39 | def get_me(access_token): 40 | get_me_url = graph_endpoint.format('/me') 41 | 42 | # Use OData query parameters to control the results 43 | # - Only return the displayName and mail fields 44 | query_parameters = {'$select': 'displayName,mail'} 45 | 46 | r = make_api_call('GET', get_me_url, access_token, "", parameters = query_parameters) 47 | 48 | if (r.status_code == requests.codes.ok): 49 | return r.json() 50 | else: 51 | return "{0}: {1}".format(r.status_code, r.text) 52 | 53 | def get_my_messages(access_token): 54 | get_messages_url = graph_endpoint.format('/me/mailfolders/inbox/messages') 55 | 56 | # Use OData query parameters to control the results 57 | # - Only first 10 results returned 58 | # - Only return the ReceivedDateTime, Subject, and From fields 59 | # - Sort the results by the ReceivedDateTime field in descending order 60 | query_parameters = {'$top': '10', 61 | '$select': 'receivedDateTime,subject,from', 62 | '$orderby': 'receivedDateTime DESC'} 63 | 64 | r = make_api_call('GET', get_messages_url, access_token, parameters = query_parameters) 65 | 66 | if (r.status_code == requests.codes.ok): 67 | return r.json() 68 | else: 69 | return "{0}: {1}".format(r.status_code, r.text) 70 | 71 | def get_my_events(access_token): 72 | get_events_url = graph_endpoint.format('/me/events') 73 | 74 | # Use OData query parameters to control the results 75 | # - Only first 10 results returned 76 | # - Only return the Subject, Start, and End fields 77 | # - Sort the results by the Start field in ascending order 78 | query_parameters = {'$top': '10', 79 | '$select': 'subject,start,end', 80 | '$orderby': 'start/dateTime ASC'} 81 | 82 | r = make_api_call('GET', get_events_url, access_token, parameters = query_parameters) 83 | 84 | if (r.status_code == requests.codes.ok): 85 | return r.json() 86 | else: 87 | return "{0}: {1}".format(r.status_code, r.text) 88 | 89 | def get_my_contacts(access_token): 90 | get_contacts_url = graph_endpoint.format('/me/contacts') 91 | 92 | # Use OData query parameters to control the results 93 | # - Only first 10 results returned 94 | # - Only return the GivenName, Surname, and EmailAddresses fields 95 | # - Sort the results by the GivenName field in ascending order 96 | query_parameters = {'$top': '10', 97 | '$select': 'givenName,surname,emailAddresses', 98 | '$orderby': 'givenName ASC'} 99 | 100 | r = make_api_call('GET', get_contacts_url, access_token, parameters = query_parameters) 101 | 102 | if (r.status_code == requests.codes.ok): 103 | return r.json() 104 | else: 105 | return "{0}: {1}".format(r.status_code, r.text) -------------------------------------------------------------------------------- /tutorial/templates/tutorial/contacts.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "tutorial/layout.html" %} 3 | {% block content %} 4 |

Your Contacts

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% for contact in contacts %} 13 | 14 | 15 | 16 | 17 | 18 | {% endfor %} 19 |
First NameLast NameEmail Address
{{ contact.givenName }}{{ contact.surname }}{{ contact.emailAddresses.0.address }}
20 | {% endblock %} -------------------------------------------------------------------------------- /tutorial/templates/tutorial/events.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "tutorial/layout.html" %} 3 | {% block content %} 4 |

Your Events

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% for event in events %} 13 | 14 | 15 | 16 | 17 | 18 | {% endfor %} 19 |
SubjectStartEnd
{{ event.subject }}{{ event.start.dateTime }} ({{ event.start.timeZone }}){{ event.end.dateTime }} ({{ event.end.timeZone }})
20 | {% endblock %} -------------------------------------------------------------------------------- /tutorial/templates/tutorial/home.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "tutorial/layout.html" %} 3 | {% block content %} 4 |
5 |

Python Outlook Sample

6 |

This example shows how to get an OAuth token from Azure using the authorization code grant flow and to use that token to make calls to the Outlook APIs.

7 |

8 | Connect to Outlook 9 |

10 |
11 | {% endblock %} -------------------------------------------------------------------------------- /tutorial/templates/tutorial/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Python Outlook API Sample 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 34 | 35 |
36 | {% block content %}{% endblock %} 37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tutorial/templates/tutorial/mail.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "tutorial/layout.html" %} 3 | {% block content %} 4 |

Your Email

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% for message in messages %} 13 | 14 | 15 | 16 | 17 | 18 | {% endfor %} 19 |
FromSubjectReceived
{{ message.from.emailAddress.name }}{{ message.subject }}{{ message.receivedDateTime }}
20 | {% endblock %} -------------------------------------------------------------------------------- /tutorial/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /tutorial/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE.txt in the project root for license information. 2 | from django.conf.urls import url 3 | from tutorial import views 4 | 5 | app_name = 'tutorial' 6 | urlpatterns = [ 7 | # The home view ('/tutorial/') 8 | url(r'^$', views.home, name='home'), 9 | # Explicit home ('/tutorial/home/') 10 | url(r'^home/$', views.home, name='home'), 11 | # Redirect to get token ('/tutorial/gettoken/') 12 | url(r'^gettoken/$', views.gettoken, name='gettoken'), 13 | # Mail view ('/tutorial/mail/') 14 | url(r'^mail/$', views.mail, name='mail'), 15 | # Events view ('/tutorial/events/') 16 | url(r'^events/$', views.events, name='events'), 17 | # Contacts view ('/tutorial/contacts/') 18 | url(r'^contacts/$', views.contacts, name='contacts'), 19 | ] -------------------------------------------------------------------------------- /tutorial/views.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE.txt in the project root for license information. 2 | from django.shortcuts import render 3 | from django.http import HttpResponse, HttpResponseRedirect 4 | from django.urls import reverse 5 | from tutorial.authhelper import get_signin_url, get_token_from_code, get_access_token 6 | from tutorial.outlookservice import get_me, get_my_messages, get_my_events, get_my_contacts 7 | import time 8 | 9 | # Create your views here. 10 | 11 | def home(request): 12 | redirect_uri = request.build_absolute_uri(reverse('tutorial:gettoken')) 13 | sign_in_url = get_signin_url(redirect_uri) 14 | context = { 'signin_url': sign_in_url } 15 | return render(request, 'tutorial/home.html', context) 16 | 17 | def gettoken(request): 18 | auth_code = request.GET['code'] 19 | redirect_uri = request.build_absolute_uri(reverse('tutorial:gettoken')) 20 | token = get_token_from_code(auth_code, redirect_uri) 21 | access_token = token['access_token'] 22 | user = get_me(access_token) 23 | refresh_token = token['refresh_token'] 24 | expires_in = token['expires_in'] 25 | 26 | # expires_in is in seconds 27 | # Get current timestamp (seconds since Unix Epoch) and 28 | # add expires_in to get expiration time 29 | # Subtract 5 minutes to allow for clock differences 30 | expiration = int(time.time()) + expires_in - 300 31 | 32 | # Save the token in the session 33 | request.session['access_token'] = access_token 34 | request.session['refresh_token'] = refresh_token 35 | request.session['token_expires'] = expiration 36 | 37 | return HttpResponseRedirect(reverse('tutorial:mail')) 38 | 39 | def mail(request): 40 | access_token = get_access_token(request, request.build_absolute_uri(reverse('tutorial:gettoken'))) 41 | # If there is no token in the session, redirect to home 42 | if not access_token: 43 | return HttpResponseRedirect(reverse('tutorial:home')) 44 | else: 45 | messages = get_my_messages(access_token) 46 | context = { 'messages': messages['value'] } 47 | return render(request, 'tutorial/mail.html', context) 48 | 49 | def events(request): 50 | access_token = get_access_token(request, request.build_absolute_uri(reverse('tutorial:gettoken'))) 51 | # If there is no token in the session, redirect to home 52 | if not access_token: 53 | return HttpResponseRedirect(reverse('tutorial:home')) 54 | else: 55 | events = get_my_events(access_token) 56 | context = { 'events': events['value'] } 57 | return render(request, 'tutorial/events.html', context) 58 | 59 | def contacts(request): 60 | access_token = get_access_token(request, request.build_absolute_uri(reverse('tutorial:gettoken'))) 61 | # If there is no token in the session, redirect to home 62 | if not access_token: 63 | return HttpResponseRedirect(reverse('tutorial:home')) 64 | else: 65 | contacts = get_my_contacts(access_token) 66 | context = { 'contacts': contacts['value'] } 67 | return render(request, 'tutorial/contacts.html', context) --------------------------------------------------------------------------------