eric6/E5Network/E5GoogleMail.py

changeset 6942
2602857055c5
parent 6838
cd9b76b2967a
child 7192
a22eee00b052
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2017 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to send bug reports.
8 """
9
10 from __future__ import unicode_literals
11 try:
12 str = unicode # __IGNORE_EXCEPTION__
13 except NameError:
14 pass
15
16 import os
17 import sys
18 import base64
19 import json
20 import datetime
21
22 from googleapiclient import discovery
23 from google.oauth2.credentials import Credentials
24 from requests_oauthlib import OAuth2Session
25
26 from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QUrl
27 from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout
28
29 from E5Gui.E5TextInputDialog import E5TextInputDialog
30
31 import Globals
32
33 from .E5GoogleMailHelpers import CLIENT_SECRET_FILE, SCOPES, TOKEN_FILE, \
34 APPLICATION_NAME
35
36
37 class E5GoogleMailAuthBrowser(QDialog):
38 """
39 Class implementing a simple web browser to perform the OAuth2
40 authentication process.
41
42 @signal approvalCodeReceived(str) emitted to indicate the receipt of the
43 approval code
44 """
45 approvalCodeReceived = pyqtSignal(str)
46
47 def __init__(self, parent=None):
48 """
49 Constructor
50
51 @param parent reference to the parent widget
52 @type QWidget
53 """
54 super(E5GoogleMailAuthBrowser, self).__init__(parent)
55
56 self.__layout = QVBoxLayout(self)
57
58 try:
59 from PyQt5.QtWebEngineWidgets import QWebEngineView
60 self.__browser = QWebEngineView(self)
61 self.__browser.titleChanged.connect(self.__titleChanged)
62 self.__browser.loadFinished.connect(self.__pageLoadFinished)
63 except ImportError:
64 from PyQt5.QtWebKitWidgets import QWebView
65 self.__browser = QWebView(self)
66 self.__browser.titleChanged.connect(self.__titleChanged)
67 self.__browser.loadFinished.connect(self.__pageLoadFinished)
68 self.__layout.addWidget(self.__browser)
69
70 self.__buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
71 self.__buttonBox.rejected.connect(self.reject)
72 self.__layout.addWidget(self.__buttonBox)
73
74 self.resize(600, 700)
75
76 @pyqtSlot(str)
77 def __titleChanged(self, title):
78 """
79 Private slot handling changes of the web page title.
80
81 @param title web page title
82 @type str
83 """
84 self.setWindowTitle(title)
85
86 @pyqtSlot()
87 def __pageLoadFinished(self):
88 """
89 Private slot handling the loadFinished signal.
90 """
91 url = self.__browser.url()
92 if url.toString().startswith(
93 "https://accounts.google.com/o/oauth2/approval/v2"):
94 if Globals.qVersionTuple() >= (5, 0, 0):
95 from PyQt5.QtCore import QUrlQuery
96 urlQuery = QUrlQuery(url)
97 approvalCode = urlQuery.queryItemValue(
98 "approvalCode", QUrl.FullyDecoded)
99 else:
100 approvalCode = QUrl.fromPercentEncoding(
101 url.encodedQueryItemValue(b"approvalCode"))
102 if approvalCode:
103 self.approvalCodeReceived.emit(approvalCode)
104 self.close()
105
106 def load(self, url):
107 """
108 Public method to start the authorization flow by loading the given URL.
109
110 @param url URL to be laoded
111 @type str or QUrl
112 """
113 self.__browser.setUrl(QUrl(url))
114
115
116 class E5GoogleMail(QObject):
117 """
118 Class implementing the logic to send emails via Google Mail.
119
120 @signal sendResult(bool, str) emitted to indicate the transmission result
121 and a result message
122 """
123 sendResult = pyqtSignal(bool, str)
124
125 def __init__(self, parent=None):
126 """
127 Constructor
128
129 @param parent reference to the parent object
130 @type QObject
131 """
132 super(E5GoogleMail, self).__init__(parent=parent)
133
134 self.__messages = []
135
136 self.__session = None
137 self.__clientConfig = {}
138
139 self.__browser = None
140
141 def sendMessage(self, message):
142 """
143 Public method to send a message via Google Mail.
144
145 @param message email message to be sent
146 @type email.mime.text.MIMEBase
147 """
148 self.__messages.append(message)
149
150 if not self.__session:
151 self.__startSession()
152 else:
153 self.__doSendMessages()
154
155 def __prepareMessage_v2(self, message):
156 """
157 Private method to prepare the message for sending (Python2 Variant).
158
159 @param message message to be prepared
160 @type email.mime.text.MIMEBase
161 @return prepared message dictionary
162 @rtype dict
163 """
164 raw = base64.urlsafe_b64encode(message.as_string())
165 return {'raw': raw}
166
167 def __prepareMessage_v3(self, message):
168 """
169 Private method to prepare the message for sending (Python2 Variant).
170
171 @param message message to be prepared
172 @type email.mime.text.MIMEBase
173 @return prepared message dictionary
174 @rtype dict
175 """
176 messageAsBase64 = base64.urlsafe_b64encode(message.as_bytes())
177 raw = messageAsBase64.decode()
178 return {'raw': raw}
179
180 def __startSession(self):
181 """
182 Private method to start an authorized session and optionally start the
183 authorization flow.
184 """
185 # check for availability of secrets file
186 if not os.path.exists(os.path.join(Globals.getConfigDir(),
187 CLIENT_SECRET_FILE)):
188 self.sendResult.emit(
189 False,
190 self.tr("The client secrets file is not present. Has the Gmail"
191 " API been enabled?")
192 )
193 return
194
195 with open(os.path.join(Globals.getConfigDir(), CLIENT_SECRET_FILE),
196 "r") as clientSecret:
197 clientData = json.load(clientSecret)
198 self.__clientConfig = clientData['installed']
199 token = self.__loadToken()
200 if token is None:
201 # no valid OAuth2 token available
202 self.__session = OAuth2Session(
203 self.__clientConfig['client_id'],
204 scope=SCOPES,
205 redirect_uri=self.__clientConfig['redirect_uris'][0]
206 )
207 authorizationUrl, _ = self.__session.authorization_url(
208 self.__clientConfig['auth_uri'],
209 access_type="offline",
210 prompt="select_account"
211 )
212 if self.__browser is None:
213 try:
214 self.__browser = E5GoogleMailAuthBrowser()
215 self.__browser.approvalCodeReceived.connect(
216 self.__processAuthorization)
217 except ImportError:
218 pass
219 if self.__browser:
220 self.__browser.show()
221 self.__browser.load(QUrl(authorizationUrl))
222 else:
223 from PyQt5.QtGui import QDesktopServices
224 QDesktopServices.openUrl(QUrl(authorizationUrl))
225 ok, authCode = E5TextInputDialog.getText(
226 None,
227 self.tr("OAuth2 Authorization Code"),
228 self.tr("Enter the OAuth2 authorization code:"))
229 if ok and authCode:
230 self.__processAuthorization(authCode)
231 else:
232 self.__session = None
233 else:
234 self.__session = OAuth2Session(
235 self.__clientConfig['client_id'],
236 scope=SCOPES,
237 redirect_uri=self.__clientConfig['redirect_uris'][0],
238 token=token,
239 auto_refresh_kwargs={
240 'client_id': self.__clientConfig['client_id'],
241 'client_secret': self.__clientConfig['client_secret'],
242 },
243 auto_refresh_url=self.__clientConfig['token_uri'],
244 token_updater=self.__saveToken)
245 self.__doSendMessages()
246
247 @pyqtSlot(str)
248 def __processAuthorization(self, authCode):
249 """
250 Private slot to process the received authorization code.
251
252 @param authCode received authorization code
253 @type str
254 """
255 self.__session.fetch_token(
256 self.__clientConfig['token_uri'],
257 client_secret=self.__clientConfig['client_secret'],
258 code=authCode)
259 self.__saveToken(self.__session.token)
260
261 # authorization completed; now send all queued messages
262 self.__doSendMessages()
263
264 def __doSendMessages(self):
265 """
266 Private method to send all queued messages.
267 """
268 if not self.__session:
269 self.sendResult.emit(
270 False,
271 self.tr("No authorized session available.")
272 )
273 return
274
275 try:
276 results = []
277 credentials = self.__credentialsFromSession()
278 service = discovery.build('gmail', 'v1', credentials=credentials,
279 cache_discovery=False)
280 count = 0
281 while self.__messages:
282 count += 1
283 message = self.__messages.pop(0)
284 if sys.version_info[0] == 2:
285 message1 = self.__prepareMessage_v2(message)
286 else:
287 message1 = self.__prepareMessage_v3(message)
288 service.users().messages()\
289 .send(userId="me", body=message1).execute()
290 results.append(self.tr("Message #{0} sent.").format(count))
291
292 self.sendResult.emit(True, "\n\n".join(results))
293 except Exception as error:
294 self.sendResult.emit(False, str(error))
295
296 def __loadToken(self):
297 """
298 Private method to load a token from the token file.
299
300 @return loaded token
301 @rtype dict or None
302 """
303 homeDir = os.path.expanduser('~')
304 credentialsDir = os.path.join(homeDir, '.credentials')
305 if not os.path.exists(credentialsDir):
306 os.makedirs(credentialsDir)
307 tokenPath = os.path.join(credentialsDir, TOKEN_FILE)
308
309 if os.path.exists(tokenPath):
310 with open(tokenPath, "r") as tokenFile:
311 return json.load(tokenFile)
312 else:
313 return None
314
315 def __saveToken(self, token):
316 """
317 Private method to save a token to the token file.
318
319 @param token token to be saved
320 @type dict
321 """
322 homeDir = os.path.expanduser('~')
323 credentialsDir = os.path.join(homeDir, '.credentials')
324 if not os.path.exists(credentialsDir):
325 os.makedirs(credentialsDir)
326 tokenPath = os.path.join(credentialsDir, TOKEN_FILE)
327
328 with open(tokenPath, "w") as tokenFile:
329 json.dump(token, tokenFile)
330
331 def __credentialsFromSession(self):
332 """
333 Private method to create a credentials object.
334
335 @return created credentials object
336 @rtype google.oauth2.credentials.Credentials
337 """
338 credentials = None
339
340 if self.__clientConfig and self.__session:
341 token = self.__session.token
342 if token:
343 credentials = Credentials(
344 token['access_token'],
345 refresh_token=token.get('refresh_token'),
346 id_token=token.get('id_token'),
347 token_uri=self.__clientConfig['token_uri'],
348 client_id=self.__clientConfig['client_id'],
349 client_secret=self.__clientConfig['client_secret'],
350 scopes=SCOPES
351 )
352 credentials.expiry = datetime.datetime.fromtimestamp(
353 token['expires_at'])
354
355 return credentials
356
357
358 def GoogleMailHelp():
359 """
360 Module function to get some help about how to enable the Google Mail
361 OAuth2 service.
362
363 @return help text
364 @rtype str
365 """
366 return (
367 "<h2>Steps to turn on the Gmail API</h2>"
368 "<ol>"
369 "<li>Use <a href='{0}'>this wizard</a> to create or select a project"
370 " in the Google Developers Console and automatically turn on the API."
371 " Click <b>Continue</b>, then <b>Go to credentials</b>.</li>"
372 "<li>At the top of the page, select the <b>OAuth consent screen</b>"
373 " tab. Select an <b>Email address</b>, enter a <b>Product name</b> if"
374 " not already set, and click the <b>Save</b> button.</li>"
375 "<li>Select the <b>Credentials</b> tab, click the <b>Add credentials"
376 "</b> button and select <b>OAuth 2.0 client ID</b>.</li>"
377 "<li>Select the application type <b>Other</b>, enter the name &quot;"
378 "{1}&quot;, and click the <b>Create</b>"
379 " button.</li>"
380 "<li>Click <b>OK</b> to dismiss the resulting dialog.</li>"
381 "<li>Click the (Download JSON) button to the right of the client ID."
382 "</li>"
383 "<li>Move this file to the eric configuration directory"
384 " <code>{2}</code> and rename it <code>{3}</code>.</li>"
385 "</ol>".format(
386 "https://console.developers.google.com/start/api?id=gmail",
387 APPLICATION_NAME,
388 Globals.getConfigDir(),
389 CLIENT_SECRET_FILE
390 )
391 )

eric ide

mercurial