-
Notifications
You must be signed in to change notification settings - Fork 4
/
o365_api.py
278 lines (227 loc) · 10 KB
/
o365_api.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# ----------------------------------------------------------------------------#
# (C) British Crown Copyright 2019 Met Office. #
# Author: Steve Wardle #
# #
# This file is part of OWA Checker. #
# OWA Checker is free software: you can redistribute it and/or modify it #
# under the terms of the Modified BSD License, as published by the #
# Open Source Initiative. #
# #
# OWA Checker is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# Modified BSD License for more details. #
# #
# You should have received a copy of the Modified BSD License #
# along with OWA Checker... #
# If not, see <http://opensource.org/licenses/BSD-3-Clause> #
# ----------------------------------------------------------------------------#
import requests
import uuid
import json
from datetime import datetime, timedelta
GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0{0}'
class o365Error(ValueError):
"""
Raised when the API fails to return anything (could be for a
number of reasons; the response will be attached)
"""
pass
# Generic API Sending
def make_api_call(method, url, token, user_email,
payload=None, parameters=None, timeout=None):
# Send these headers with all API calls
headers = {'User-Agent': 'OWA Checker',
'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,
timeout=timeout)
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
def get_user_info(access_token):
"""
Returns the logged-in user's username and email address
"""
get_me_url = GRAPH_ENDPOINT.format('/me')
# Use OData query parameters to control the results
# - Only return the displayName and mail fields
query_parameters = {'$select': 'displayName,mail'}
try:
r = make_api_call('GET', get_me_url, access_token,
"", parameters=query_parameters)
except Exception as err:
raise o365Error(err)
if (r.status_code == requests.codes.ok):
return r.json()
else:
raise o365Error("{0}: {1}".format(r.status_code, r.text))
def get_new_messages(access_token, user_email, last_seen=None, folders=None):
"""
Returns unread messages in the logged-in user's Inbox; note that
the 365 API limits the maximum amount of results that are returned
to 10 by default.
If provided with a "last_seen" time (a string in the same format as
returned in the JSON) the call will only return unread messages
which arrived *more recently* than that time. Otherwise it will
return any unread messages (up to 10, see above!)
"""
if folders is None:
folders = ["inbox"]
else:
folders = [folder.lower() for folder in folders]
if last_seen is None:
# If no last-seen time was provided, get all unread messages
query_parameters = {
'$filter': 'isRead eq false',
'$select': 'receivedDateTime,subject,from',
'$orderby': 'receivedDateTime DESC'}
else:
# Otherwise retrieve only those since the last-seen message
query_parameters = {
'$filter': ('isRead eq false and receivedDateTime gt {0}'
.format(last_seen)),
'$select': 'receivedDateTime,subject,from',
'$orderby': 'receivedDateTime DESC'}
folder_ids = []
get_folders_url = GRAPH_ENDPOINT.format('/me/mailfolders')
next_page = True
while next_page:
# Get the folder ids
try:
r = make_api_call('GET', get_folders_url, access_token,
user_email)
except Exception as err:
raise o365Error(err)
if (r.status_code == requests.codes.ok):
output = r.json()
for folder in output["value"]:
if folder["displayName"].lower() in folders:
folder_ids.append(folder["id"])
if len(folder_ids) == len(folders):
break
if '@odata.nextLink' in output:
get_folders_url = output['@odata.nextLink']
else:
next_page = False
messages = []
for folder_id in folder_ids:
# Make the call and return the result
get_messages_url = GRAPH_ENDPOINT.format(
"/me/mailfolders/{0}/messages".format(folder_id))
try:
r = make_api_call('GET', get_messages_url, access_token,
user_email, parameters=query_parameters)
except Exception as err:
raise o365Error(err)
if (r.status_code == requests.codes.ok):
output = r.json()
if output is not None:
messages += output["value"]
else:
raise o365Error("{0}: {1}".format(r.status_code, r.text))
return messages
def get_num_messages(access_token, user_email, folders=None):
"""
Returns the total number of unread messages in the logged-in
user's Inbox. This should be used in preference to counting the
result of "get_new_messages" since it isn't affected by the limit
on returned values (see "get_new_messages" for details)
"""
# Base url for messages
get_messages_url = GRAPH_ENDPOINT.format('/me/mailfolders')
# If no list of folders supplied, default to just the Inbox
if folders is None:
folders = ['inbox']
else:
folders = [folder.lower() for folder in folders]
# Logical stores if there are more pages of results to return
next_page = True
message_count = 0
while next_page:
try:
r = make_api_call('GET', get_messages_url, access_token,
user_email)
except Exception as err:
raise o365Error(err)
if (r.status_code == requests.codes.ok):
output = r.json()
for folder in output['value']:
if folder['displayName'].lower() in folders:
message_count += folder['unreadItemCount']
if '@odata.nextLink' in output:
get_messages_url = output['@odata.nextLink']
else:
next_page = False
else:
raise o365Error("{0}: {1}".format(r.status_code, r.text))
return message_count
def get_week_events(access_token, user_email):
"""
Returns all calendar events on the logged-in user's calendar starting
from the current time and for the next week; this is arbitrary but
considered a long enough time to hopefully not miss meetings with
the longest expected reminder duration
"""
today_start = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:00.0000000")
next_week_start = (
datetime.utcnow()
+ timedelta(days=7)).strftime("%Y-%m-%dT00:00:00.0000000")
url_query = ("?startDateTime={0}&endDateTime={1}"
.format(today_start, next_week_start))
get_events_url = GRAPH_ENDPOINT.format('/me/calendarView' + url_query)
# Use OData query parameters to control the results
query_parameters = {
'$select': ('isReminderOn, reminderMinutesBeforeStart,'
'subject,start,location,isCancelled'),
'$orderby': 'start/dateTime ASC'}
try:
r = make_api_call('GET', get_events_url, access_token,
user_email, parameters=query_parameters)
except Exception as err:
raise o365Error(err)
if (r.status_code == requests.codes.ok):
return r.json()
else:
raise o365Error("{0}: {1}".format(r.status_code, r.text))
def get_user_portrait(access_token, user_email, size='64x64'):
"""
Returns the user's portrait image (if they have one!)
"""
get_portrait_url = GRAPH_ENDPOINT.format(
"/users/{0}/photos/{1}/$value".format(user_email, size))
try:
r = make_api_call('GET', get_portrait_url,
access_token, user_email, timeout=2)
except Exception as err:
raise o365Error(err)
if (r.status_code == requests.codes.ok):
return r.content
else:
raise o365Error("{0}: {1}".format(r.status_code, r.text))