E5Network/E5GoogleMail.py

changeset 6828
bb6667ea9ae7
parent 6825
e659bb96cdfa
child 6838
cd9b76b2967a
equal deleted inserted replaced
6825:e659bb96cdfa 6828:bb6667ea9ae7
14 pass 14 pass
15 15
16 import os 16 import os
17 import sys 17 import sys
18 import base64 18 import base64
19 import pickle 19 import json
20 import datetime
20 21
21 from googleapiclient import discovery 22 from googleapiclient import discovery
22 from google_auth_oauthlib.flow import InstalledAppFlow 23 from google.oauth2.credentials import Credentials
23 from google.auth.transport.requests import Request 24 from requests_oauthlib import OAuth2Session
24 25
25 from PyQt5.QtCore import QCoreApplication 26 from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QUrl
27 from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout
28
29 from E5Gui.E5TextInputDialog import E5TextInputDialog
26 30
27 import Globals 31 import Globals
28 32
29 33 from .E5GoogleMailHelpers import CLIENT_SECRET_FILE, SCOPES, TOKEN_FILE, \
30 SCOPES = 'https://www.googleapis.com/auth/gmail.send' 34 APPLICATION_NAME
31 CLIENT_SECRET_FILE = 'eric_client_secret.json' 35
32 CREDENTIALS_FILE = 'eric-python-email-send.pickle' 36
33 APPLICATION_NAME = 'Eric Python Send Email' 37 class E5GoogleMailAuthBrowser(QDialog):
34
35
36 def isClientSecretFileAvailable():
37 """
38 Module function to check, if the client secret file has been installed.
39
40 @return flag indicating, that the credentials file is there
41 @rtype bool
42 """
43 return os.path.exists(
44 os.path.join(Globals.getConfigDir(), CLIENT_SECRET_FILE))
45
46
47 def getCredentials():
48 """ 38 """
49 Module function to get the Google credentials. 39 Module function to get the Google credentials.
50 40
41 Class implementing a simple web browser to perform the OAuth2
42 authentication process.
43
51 @return Google Mail credentials 44 @return Google Mail credentials
52 """ 45 @signal approvalCodeReceived(str) emitted to indicate the receipt of the
53 homeDir = os.path.expanduser('~') 46 approval code
54 credentialsDir = os.path.join(homeDir, '.credentials') 47 """
55 if not os.path.exists(credentialsDir): 48 approvalCodeReceived = pyqtSignal(str)
56 os.makedirs(credentialsDir) 49
57 credentialsPath = os.path.join(credentialsDir, CREDENTIALS_FILE) 50 def __init__(self, parent=None):
58 51 """
59 credentials = None 52 Constructor
60 # The file eric-python-email-send.pickle stores the user's access and 53
61 # refresh tokens, and is created automatically when the authorization 54 @param parent reference to the parent widget
62 # flow completes for the first time. 55 @type QWidget
63 if os.path.exists(credentialsPath): 56 """
64 with open(credentialsPath, 'rb') as token: 57 super(E5GoogleMailAuthBrowser, self).__init__(parent)
65 credentials = pickle.load(token) 58
66 # If there are no (valid) credentials available, let the user log in. 59 self.__layout = QVBoxLayout(self)
67 if not credentials or not credentials.valid: 60
68 if credentials and credentials.expired and credentials.refresh_token: 61 try:
69 credentials.refresh(Request()) 62 from PyQt5.QtWebEngineWidgets import QWebEngineView
63 self.__browser = QWebEngineView(self)
64 self.__browser.titleChanged.connect(self.__titleChanged)
65 self.__browser.loadFinished.connect(self.__pageLoadFinished)
66 except ImportError:
67 from PyQt5.QtWebKitWidgets import QWebView
68 self.__browser = QWebView(self)
69 self.__browser.titleChanged.connect(self.__titleChanged)
70 self.__browser.loadFinished.connect(self.__pageLoadFinished)
71 self.__layout.addWidget(self.__browser)
72
73 self.__buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
74 self.__buttonBox.rejected.connect(self.reject)
75 self.__layout.addWidget(self.__buttonBox)
76
77 self.resize(600, 700)
78
79 @pyqtSlot(str)
80 def __titleChanged(self, title):
81 """
82 Private slot handling changes of the web page title.
83
84 @param title web page title
85 @type str
86 """
87 self.setWindowTitle(title)
88
89 @pyqtSlot()
90 def __pageLoadFinished(self):
91 """
92 Private slot handling the loadFinished signal.
93 """
94 url = self.__browser.url()
95 if url.toString().startswith(
96 "https://accounts.google.com/o/oauth2/approval/v2"):
97 if Globals.qVersionTuple() >= (5, 0, 0):
98 from PyQt5.QtCore import QUrlQuery
99 urlQuery = QUrlQuery(url)
100 approvalCode = urlQuery.queryItemValue(
101 "approvalCode", QUrl.FullyDecoded)
102 else:
103 approvalCode = QUrl.fromPercentEncoding(
104 url.encodedQueryItemValue(b"approvalCode"))
105 if approvalCode:
106 self.approvalCodeReceived.emit(approvalCode)
107 self.close()
108
109 def load(self, url):
110 """
111 Public method to start the authorization flow by loading the given URL.
112
113 @param url URL to be laoded
114 @type str or QUrl
115 """
116 self.__browser.setUrl(QUrl(url))
117
118
119 class E5GoogleMail(QObject):
120 """
121 Class implementing the logic to send emails via Google Mail.
122
123 @signal sendResult(bool, str) emitted to indicate the transmission result
124 and a result message
125 """
126 sendResult = pyqtSignal(bool, str)
127
128 def __init__(self, parent=None):
129 """
130 Constructor
131
132 @param parent reference to the parent object
133 @type QObject
134 """
135 super(E5GoogleMail, self).__init__(parent=parent)
136
137 self.__messages = []
138
139 self.__session = None
140 self.__clientConfig = {}
141
142 self.__browser = None
143
144 def sendMessage(self, message):
145 """
146 Public method to send a message via Google Mail.
147
148 @param message email message to be sent
149 @type email.mime.text.MIMEBase
150 """
151 self.__messages.append(message)
152
153 if not self.__session:
154 self.__startSession()
70 else: 155 else:
71 flow = InstalledAppFlow.from_client_secrets_file( 156 self.__doSendMessages()
72 os.path.join(Globals.getConfigDir(), CLIENT_SECRET_FILE), 157
73 SCOPES) 158 def __prepareMessage_v2(self, message):
74 credentials = flow.run_local_server() 159 """
75 # Save the credentials for the next run 160 Private method to prepare the message for sending (Python2 Variant).
76 with open(credentialsPath, 'wb') as credentialsFile: 161
77 pickle.dump(credentials, credentialsFile) 162 @param message message to be prepared
78 return credentials 163 @type email.mime.text.MIMEBase
79 164 @return prepared message dictionary
80 165 @rtype dict
81 def GoogleMailSendMessage(message): 166 """
82 """ 167 raw = base64.urlsafe_b64encode(message.as_string())
83 Module function to send an email message via Google Mail. 168 return {'raw': raw}
84 169
85 @param message email message to be sent 170 def __prepareMessage_v3(self, message):
86 @type email.mime.text.MIMEBase 171 """
87 @return tuple containing a success flag and a result or error message 172 Private method to prepare the message for sending (Python2 Variant).
88 @rtype tuple of (bool, str) 173
89 """ 174 @param message message to be prepared
90 # check for secrets file first 175 @type email.mime.text.MIMEBase
91 if not os.path.exists(os.path.join(Globals.getConfigDir(), 176 @return prepared message dictionary
92 CLIENT_SECRET_FILE)): 177 @rtype dict
93 return False, QCoreApplication.translate( 178 """
94 "GoogleMailSendMessage", 179 messageAsBase64 = base64.urlsafe_b64encode(message.as_bytes())
95 "The credentials file is not present. Has the Gmail API" 180 raw = messageAsBase64.decode()
96 " been enabled?") 181 return {'raw': raw}
97 182
98 try: 183 def __startSession(self):
99 credentials = getCredentials() 184 """
100 service = discovery.build('gmail', 'v1', credentials=credentials) 185 Private method to start an authorized session and optionally start the
101 if sys.version_info[0] == 2: 186 authorization flow.
102 message1 = _prepareMessage_v2(message) 187 """
188 # check for availability of secrets file
189 if not os.path.exists(os.path.join(Globals.getConfigDir(),
190 CLIENT_SECRET_FILE)):
191 self.sendResult.emit(
192 False,
193 self.tr("The client secrets file is not present. Has the Gmail"
194 " API been enabled?")
195 )
196 return
197
198 with open(os.path.join(Globals.getConfigDir(), CLIENT_SECRET_FILE),
199 "r") as clientSecret:
200 clientData = json.load(clientSecret)
201 self.__clientConfig = clientData['installed']
202 token = self.__loadToken()
203 if token is None:
204 # no valid OAuth2 token available
205 self.__session = OAuth2Session(
206 self.__clientConfig['client_id'],
207 scope=SCOPES,
208 redirect_uri=self.__clientConfig['redirect_uris'][0]
209 )
210 authorizationUrl, _ = self.__session.authorization_url(
211 self.__clientConfig['auth_uri'],
212 access_type="offline",
213 prompt="select_account"
214 )
215 if self.__browser is None:
216 try:
217 self.__browser = E5GoogleMailAuthBrowser()
218 self.__browser.approvalCodeReceived.connect(
219 self.__processAuthorization)
220 except ImportError:
221 pass
222 if self.__browser:
223 self.__browser.show()
224 self.__browser.load(QUrl(authorizationUrl))
225 else:
226 from PyQt5.QtGui import QDesktopServices
227 QDesktopServices.openUrl(QUrl(authorizationUrl))
228 ok, authCode = E5TextInputDialog.getText(
229 None,
230 self.tr("OAuth2 Authorization Code"),
231 self.tr("Enter the OAuth2 authorization code:"))
232 if ok and authCode:
233 self.__processAuthorization(authCode)
234 else:
235 self.__session = None
103 else: 236 else:
104 message1 = _prepareMessage_v3(message) 237 self.__session = OAuth2Session(
105 result = service.users().messages()\ 238 self.__clientConfig['client_id'],
106 .send(userId="me", body=message1).execute() 239 scope=SCOPES,
107 240 redirect_uri=self.__clientConfig['redirect_uris'][0],
108 return True, result 241 token=token,
109 except Exception as error: 242 auto_refresh_kwargs={
110 return False, str(error) 243 'client_id': self.__clientConfig['client_id'],
111 244 'client_secret': self.__clientConfig['client_secret'],
112 245 },
113 def _prepareMessage_v2(message): 246 auto_refresh_url=self.__clientConfig['token_uri'],
114 """ 247 token_updater=self.__saveToken)
115 Module function to prepare the message for sending (Python2 Variant). 248 self.__doSendMessages()
116 249
117 @param message message to be prepared 250 @pyqtSlot(str)
118 @type email.mime.text.MIMEBase 251 def __processAuthorization(self, authCode):
119 @return prepared message dictionary 252 """
120 @rtype dict 253 Private slot to process the received authorization code.
121 """ 254
122 raw = base64.urlsafe_b64encode(message.as_string()) 255 @param authCode received authorization code
123 return {'raw': raw} 256 @type str
124 257 """
125 258 self.__session.fetch_token(
126 def _prepareMessage_v3(message): 259 self.__clientConfig['token_uri'],
127 """ 260 client_secret=self.__clientConfig['client_secret'],
128 Module function to prepare the message for sending (Python2 Variant). 261 code=authCode)
129 262 self.__saveToken(self.__session.token)
130 @param message message to be prepared 263
131 @type email.mime.text.MIMEBase 264 # authorization completed; now send all queued messages
132 @return prepared message dictionary 265 self.__doSendMessages()
133 @rtype dict 266
134 """ 267 def __doSendMessages(self):
135 messageAsBase64 = base64.urlsafe_b64encode(message.as_bytes()) 268 """
136 raw = messageAsBase64.decode() 269 Private method to send all queued messages.
137 return {'raw': raw} 270 """
271 if not self.__session:
272 self.sendResult.emit(
273 False,
274 self.tr("No authorized session available.")
275 )
276 return
277
278 try:
279 results = []
280 credentials = self.__credentialsFromSession()
281 service = discovery.build('gmail', 'v1', credentials=credentials,
282 cache_discovery=False)
283 count = 0
284 while self.__messages:
285 count += 1
286 message = self.__messages.pop(0)
287 if sys.version_info[0] == 2:
288 message1 = self.__prepareMessage_v2(message)
289 else:
290 message1 = self.__prepareMessage_v3(message)
291 service.users().messages()\
292 .send(userId="me", body=message1).execute()
293 results.append(self.tr("Message #{0} sent.").format(count))
294
295 self.sendResult.emit(True, "\n\n".join(results))
296 except Exception as error:
297 self.sendResult.emit(False, str(error))
298
299 def __loadToken(self):
300 """
301 Private method to load a token from the token file.
302
303 @return loaded token
304 @rtype dict or None
305 """
306 homeDir = os.path.expanduser('~')
307 credentialsDir = os.path.join(homeDir, '.credentials')
308 if not os.path.exists(credentialsDir):
309 os.makedirs(credentialsDir)
310 tokenPath = os.path.join(credentialsDir, TOKEN_FILE)
311
312 if os.path.exists(tokenPath):
313 with open(tokenPath, "r") as tokenFile:
314 return json.load(tokenFile)
315 else:
316 return None
317
318 def __saveToken(self, token):
319 """
320 Private method to save a token to the token file.
321
322 @param token token to be saved
323 @type dict
324 """
325 homeDir = os.path.expanduser('~')
326 credentialsDir = os.path.join(homeDir, '.credentials')
327 if not os.path.exists(credentialsDir):
328 os.makedirs(credentialsDir)
329 tokenPath = os.path.join(credentialsDir, TOKEN_FILE)
330
331 with open(tokenPath, "w") as tokenFile:
332 json.dump(token, tokenFile)
333
334 def __credentialsFromSession(self):
335 """
336 Private method to create a credentials object.
337
338 @return created credentials object
339 @rtype google.oauth2.credentials.Credentials
340 """
341 credentials = None
342
343 if self.__clientConfig and self.__session:
344 token = self.__session.token
345 if token:
346 credentials = Credentials(
347 token['access_token'],
348 refresh_token=token.get('refresh_token'),
349 id_token=token.get('id_token'),
350 token_uri=self.__clientConfig['token_uri'],
351 client_id=self.__clientConfig['client_id'],
352 client_secret=self.__clientConfig['client_secret'],
353 scopes=SCOPES
354 )
355 credentials.expiry = datetime.datetime.fromtimestamp(
356 token['expires_at'])
357
358 return credentials
138 359
139 360
140 def GoogleMailHelp(): 361 def GoogleMailHelp():
141 """ 362 """
142 Module function to get some help about how to enable the Google Mail 363 Module function to get some help about how to enable the Google Mail

eric ide

mercurial