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 |