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