1+ import base64
2+ import mimetypes
13import requests
24from urllib .parse import urlencode , urlparse
35
@@ -18,12 +20,21 @@ def __init__(self, client_id, client_secret, api_version='v1.0', account_type='c
1820 self .base_url = self .RESOURCE + self .api_version + '/'
1921 self .token = None
2022
21- def authorization_url (self , redirect_uri , scope ):
23+ def authorization_url (self , redirect_uri , scope , state = None ):
2224 """
2325
2426 Args:
25- redirect_uri:
26- scope:
27+ redirect_uri: The redirect_uri of your app, where authentication responses can be sent and received by
28+ your app. It must exactly match one of the redirect_uris you registered in the app registration portal
29+
30+ scope: A list of the Microsoft Graph permissions that you want the user to consent to. This may also
31+ include OpenID scopes.
32+
33+ state: A value included in the request that will also be returned in the token response.
34+ It can be a string of any content that you wish. A randomly generated unique value is typically
35+ used for preventing cross-site request forgery attacks. The state is also used to encode information
36+ about the user's state in the app before the authentication request occurred, such as the page or view
37+ they were on.
2738
2839 Returns:
2940
@@ -32,17 +43,24 @@ def authorization_url(self, redirect_uri, scope):
3243 'client_id' : self .client_id ,
3344 'redirect_uri' : redirect_uri ,
3445 'scope' : ' ' .join (scope ),
35- 'response_type' : 'code'
46+ 'response_type' : 'code' ,
47+ 'response_mode' : 'query'
3648 }
49+
50+ if state :
51+ params ['state' ] = None
52+
3753 url = self .AUTHORITY_URL + self .account_type + self .AUTH_ENDPOINT + urlencode (params )
3854 return url
3955
40- def exchange_code (self , redirect_uri , code , scope ):
56+ def exchange_code (self , redirect_uri , code ):
4157 """Exchanges a code for a Token.
4258
4359 Args:
44- redirect_uri: A string with the redirect_uri set in the app config.
45- code: A string containing the code to exchange.
60+ redirect_uri: The redirect_uri of your app, where authentication responses can be sent and received by
61+ your app. It must exactly match one of the redirect_uris you registered in the app registration portal
62+
63+ code: The authorization_code that you acquired in the first leg of the flow.
4664
4765 Returns:
4866 A dict.
@@ -57,31 +75,112 @@ def exchange_code(self, redirect_uri, code, scope):
5775 }
5876 return requests .post (self .AUTHORITY_URL + self .account_type + self .TOKEN_ENDPOINT , data = params ).json ()
5977
78+ def refresh_token (self , redirect_uri , refresh_token ):
79+ """
80+
81+ Args:
82+ redirect_uri: The redirect_uri of your app, where authentication responses can be sent and received by
83+ your app. It must exactly match one of the redirect_uris you registered in the app registration portal
84+
85+ refresh_token: An OAuth 2.0 refresh token. Your app can use this token acquire additional access tokens
86+ after the current access token expires. Refresh tokens are long-lived, and can be used to retain access
87+ to resources for extended periods of time.
88+
89+ Returns:
90+
91+ """
92+ params = {
93+ 'client_id' : self .client_id ,
94+ 'redirect_uri' : redirect_uri ,
95+ 'client_secret' : self .client_secret ,
96+ 'refresh_token' : refresh_token ,
97+ 'grant_type' : 'refresh_token' ,
98+ }
99+ return requests .post (self .AUTHORITY_URL + self .account_type + self .TOKEN_ENDPOINT , data = params ).json ()
100+
60101 def set_token (self , token ):
61- """Sets the Access Token for its use in this library.
102+ """Sets the Token for its use in this library.
62103
63104 Args:
64- token: A string with the Access Token.
105+ token: A string with the Token.
65106
66107 """
67108 self .token = token
68109
69- def get_me (self ):
70- return self ._get ('me' )
110+ def get_me (self , params = None ):
111+ """Retrieve the properties and relationships of user object.
112+
113+ Note: Getting a user returns a default set of properties only (businessPhones, displayName, givenName, id,
114+ jobTitle, mail, mobilePhone, officeLocation, preferredLanguage, surname, userPrincipalName).
115+ Use $select to get the other properties and relationships for the user object.
116+
117+ Args:
118+ params: A dict.
119+
120+ Returns:
121+
122+
123+ """
124+ return self ._get ('me' , params = None )
125+
126+ def send_mail (self , subject = None , recipients = None , body = '' , content_type = 'HTML' , attachments = None ):
127+ """Helper to send email from current user.
128+
129+ Args:
130+ subject: email subject (required)
131+ recipients: list of recipient email addresses (required)
132+ body: body of the message
133+ content_type: content type (default is 'HTML')
134+ attachments: list of file attachments (local filenames)
135+
136+ Returns:
137+ Returns the response from the POST to the sendmail API.
138+ """
139+
140+ # Verify that required arguments have been passed.
141+ if not all ([subject , recipients ]):
142+ raise ValueError ('sendmail(): required arguments missing' )
143+
144+ # Create recipient list in required format.
145+ recipient_list = [{'EmailAddress' : {'Address' : address }} for address in recipients ]
146+
147+ # Create list of attachments in required format.
148+ attached_files = []
149+ if attachments :
150+ for filename in attachments :
151+ b64_content = base64 .b64encode (open (filename , 'rb' ).read ())
152+ mime_type = mimetypes .guess_type (filename )[0 ]
153+ mime_type = mime_type if mime_type else ''
154+ attached_files .append (
155+ {'@odata.type' : '#microsoft.graph.fileAttachment' , 'ContentBytes' : b64_content .decode ('utf-8' ),
156+ 'ContentType' : mime_type , 'Name' : filename })
157+
158+ # Create email message in required format.
159+ email_msg = {'Message' : {'Subject' : subject ,
160+ 'Body' : {'ContentType' : content_type , 'Content' : body },
161+ 'ToRecipients' : recipient_list ,
162+ 'Attachments' : attached_files },
163+ 'SaveToSentItems' : 'true' }
164+
165+ # Do a POST to Graph's sendMail API and return the response.
166+ return self ._post ('me/microsoft.graph.sendMail' , data = email_msg )
167+
168+ def _get_headers (self ):
169+ return {'Authorization' : 'Bearer ' + self .token ['access_token' ]}
71170
72171 def _get (self , endpoint , params = None ):
73- headers = {'Authorization' : 'Bearer ' + self .token ['access_token' ]}
74- response = requests .get (self .base_url + endpoint , params = params , headers = headers )
172+ response = requests .get (self .base_url + endpoint , params = params , headers = self ._get_headers ())
75173 return self ._parse (response )
76174
77175 def _post (self , endpoint , params = None , data = None ):
78- response = requests .post (self .base_url + endpoint , params = params , data = data )
176+ response = requests .post (self .base_url + endpoint , params = params , json = data , headers = self . _get_headers () )
79177 return self ._parse (response )
80178
81179 def _delete (self , endpoint , params = None ):
82180 response = requests .delete (self .base_url + endpoint , params = params )
83181 return self ._parse (response )
84182
85183 def _parse (self , response ):
86- return response .json ()
87-
184+ if 'application/json' in response .headers ['Content-Type' ]:
185+ return response .json ()
186+ return response .text
0 commit comments