src/eric7/EricNetwork/EricGoogleMail.py

branch
eric7
changeset 9221
bf71ee032bb4
parent 9209
b99e7fd55fd3
child 9245
6ee1705a1cb9
equal deleted inserted replaced
9220:e9e7eca7efee 9221:bf71ee032bb4
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>"
352 " <code>{2}</code> and rename it <code>{3}</code>.</li>" 364 " <code>{2}</code> and rename it <code>{3}</code>.</li>"
353 "</ol>".format( 365 "</ol>".format(
354 "https://console.developers.google.com/start/api?id=gmail", 366 "https://console.developers.google.com/start/api?id=gmail",
355 APPLICATION_NAME, 367 APPLICATION_NAME,
356 Globals.getConfigDir(), 368 Globals.getConfigDir(),
357 CLIENT_SECRET_FILE 369 CLIENT_SECRET_FILE,
358 ) 370 )
359 ) 371 )

eric ide

mercurial