diff -r e2a52d98ad20 -r 905e7af29101 src/eric7/EricNetwork/EricGoogleMail.py --- a/src/eric7/EricNetwork/EricGoogleMail.py Thu Oct 20 10:22:41 2022 +0200 +++ b/src/eric7/EricNetwork/EricGoogleMail.py Thu Oct 20 19:19:00 2022 +0200 @@ -9,19 +9,15 @@ import os import base64 -import json -import datetime -import contextlib -from googleapiclient import discovery -from google.oauth2.credentials import Credentials -from requests_oauthlib import OAuth2Session +from googleapiclient import discovery, errors +from google.oauth2.credentials import Credentials, UserAccessTokenCredentials +from google.auth.transport.requests import Request +from google.auth.exceptions import RefreshError +from google_auth_oauthlib.flow import InstalledAppFlow -from PyQt6.QtCore import pyqtSlot, pyqtSignal, QObject, QUrl, QUrlQuery -from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout -from eric7.EricWidgets import EricMessageBox -from eric7.EricWidgets.EricTextInputDialog import EricTextInputDialog +from PyQt6.QtCore import pyqtSignal, QObject from eric7 import Globals @@ -29,89 +25,9 @@ CLIENT_SECRET_FILE, SCOPES, TOKEN_FILE, - APPLICATION_NAME, ) -class EricGoogleMailAuthBrowser(QDialog): - """ - Class implementing a simple web browser to perform the OAuth2 - authentication process. - - @signal approvalCodeReceived(str) emitted to indicate the receipt of the - approval code - """ - - approvalCodeReceived = pyqtSignal(str) - - def __init__(self, parent=None): - """ - Constructor - - @param parent reference to the parent widget - @type QWidget - """ - super().__init__(parent) - - self.__layout = QVBoxLayout(self) - - from PyQt6.QtWebEngineWidgets import QWebEngineView - - self.__browser = QWebEngineView(self) - self.__browser.titleChanged.connect(self.__titleChanged) - self.__browser.loadFinished.connect(self.__pageLoadFinished) - self.__layout.addWidget(self.__browser) - - self.__buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) - self.__buttonBox.rejected.connect(self.reject) - self.__layout.addWidget(self.__buttonBox) - - self.resize(600, 700) - - @pyqtSlot(str) - def __titleChanged(self, title): - """ - Private slot handling changes of the web page title. - - @param title web page title - @type str - """ - self.setWindowTitle(title) - - @pyqtSlot() - def __pageLoadFinished(self): - """ - Private slot handling the loadFinished signal. - """ - url = self.__browser.url() - if url.toString().startswith( - "https://accounts.google.com/o/oauth2/approval/v2" - ): - urlQuery = QUrlQuery(url) - approvalCode = urlQuery.queryItemValue( - "approvalCode", QUrl.ComponentFormattingOption.FullyDecoded - ) - elif url.toString().startswith(("http://localhost", "https://localhost")): - urlQuery = QUrlQuery(url) - approvalCode = urlQuery.queryItemValue( - "code", QUrl.ComponentFormattingOption.FullyDecoded - ) - else: - approvalCode = None - if approvalCode: - self.approvalCodeReceived.emit(approvalCode) - self.close() - - def load(self, url): - """ - Public method to start the authorization flow by loading the given URL. - - @param url URL to be laoded - @type str or QUrl - """ - self.__browser.setUrl(QUrl(url)) - - class EricGoogleMail(QObject): """ Class implementing the logic to send emails via Google Mail. @@ -133,10 +49,7 @@ self.__messages = [] - self.__session = None - self.__clientConfig = {} - - self.__browser = None + self.__credentials = None def sendMessage(self, message): """ @@ -147,10 +60,10 @@ """ self.__messages.append(message) - if not self.__session: + if not self.__credentials: self.__startSession() - else: - self.__doSendMessages() + + self.__doSendMessages() def __prepareMessage(self, message): """ @@ -167,7 +80,7 @@ def __startSession(self): """ - Private method to start an authorized session and optionally start the + Private method to start an authorized session and optionally execute the authorization flow. """ # check for availability of secrets file @@ -181,101 +94,37 @@ ) return - with open( - os.path.join(Globals.getConfigDir(), CLIENT_SECRET_FILE), "r" - ) as clientSecret: - clientData = json.load(clientSecret) - self.__clientConfig = clientData["installed"] - token = self.__loadToken() - if token is None: - # no valid OAuth2 token available - self.__session = OAuth2Session( - self.__clientConfig["client_id"], - scope=SCOPES, - redirect_uri=self.__clientConfig["redirect_uris"][0], - ) - authorizationUrl, _ = self.__session.authorization_url( - self.__clientConfig["auth_uri"], - access_type="offline", - prompt="select_account", - ) - if self.__browser is None: - with contextlib.suppress(ImportError): - self.__browser = EricGoogleMailAuthBrowser() - self.__browser.approvalCodeReceived.connect( - self.__processAuthorization - ) - if self.__browser: - self.__browser.show() - self.__browser.load(QUrl(authorizationUrl)) - else: - from PyQt6.QtGui import QDesktopServices - - QDesktopServices.openUrl(QUrl(authorizationUrl)) - ok, authCode = EricTextInputDialog.getText( - None, - self.tr("OAuth2 Authorization Code"), - self.tr("Enter the OAuth2 authorization code:"), + credentials = self.__loadCredentials() + credentials = UserAccessTokenCredentials() + # If there are no (valid) credentials available, let the user log in. + if not credentials or not credentials.valid: + if credentials and credentials.expired and credentials.refresh_token: + try: + credentials.refresh(Request()) + except RefreshError: + credentials = None + if not credentials or not credentials.valid: + flow = InstalledAppFlow.from_client_secrets_file( + os.path.join(Globals.getConfigDir(), CLIENT_SECRET_FILE), SCOPES ) - if ok and authCode: - self.__processAuthorization(authCode) - else: - self.__session = None - else: - self.__session = OAuth2Session( - self.__clientConfig["client_id"], - scope=SCOPES, - redirect_uri=self.__clientConfig["redirect_uris"][0], - token=token, - auto_refresh_kwargs={ - "client_id": self.__clientConfig["client_id"], - "client_secret": self.__clientConfig["client_secret"], - }, - auto_refresh_url=self.__clientConfig["token_uri"], - token_updater=self.__saveToken, - ) - self.__doSendMessages() + credentials = flow.run_local_server(port=0) + # Save the credentials for the next run + self.__saveCredentials(credentials) - @pyqtSlot(str) - def __processAuthorization(self, authCode): - """ - Private slot to process the received authorization code. - - @param authCode received authorization code - @type str - """ - try: - self.__session.fetch_token( - self.__clientConfig["token_uri"], - client_secret=self.__clientConfig["client_secret"], - code=authCode, - ) - self.__saveToken(self.__session.token) - - # authorization completed; now send all queued messages - self.__doSendMessages() - except Exception as err: - EricMessageBox.critical( - self, - self.tr("Google Authorization"), - self.tr( - """<p>The authorization via Google failed.</p><p>Reason: {0}</p>""" - ).format(str(err)), - ) + self.__credentials = credentials def __doSendMessages(self): """ Private method to send all queued messages. """ - if not self.__session: - self.sendResult.emit(False, self.tr("No authorized session available.")) + if not self.__credentials or not self.__credentials.valid: + self.sendResult.emit(False, self.tr("No valid credentials available.")) return try: results = [] - credentials = self.__credentialsFromSession() service = discovery.build( - "gmail", "v1", credentials=credentials, cache_discovery=False + "gmail", "v1", credentials=self.__credentials, cache_discovery=False ) count = 0 while self.__messages: @@ -286,15 +135,15 @@ results.append(self.tr("Message #{0} sent.").format(count)) self.sendResult.emit(True, "\n\n".join(results)) - except Exception as error: - self.sendResult.emit(False, str(error)) + except errors.HttpError as err: + self.sendResult.emit(False, str(err)) - def __loadToken(self): + def __loadCredentials(self): """ - Private method to load a token from the token file. + Private method to load credentials from the token file. - @return loaded token - @rtype dict or None + @return created credentials object + @rtype Credentials """ homeDir = os.path.expanduser("~") credentialsDir = os.path.join(homeDir, ".credentials") @@ -303,17 +152,16 @@ tokenPath = os.path.join(credentialsDir, TOKEN_FILE) if os.path.exists(tokenPath): - with open(tokenPath, "r") as tokenFile: - return json.load(tokenFile) + return Credentials.from_authorized_user_file(tokenPath, SCOPES) else: return None - def __saveToken(self, token): + def __saveCredentials(self, credentials): """ - Private method to save a token to the token file. + Private method to save credentials to the token file. - @param token token to be saved - @type dict + @param credentials credentials to be saved + @type Credentials """ homeDir = os.path.expanduser("~") credentialsDir = os.path.join(homeDir, ".credentials") @@ -322,34 +170,7 @@ tokenPath = os.path.join(credentialsDir, TOKEN_FILE) with open(tokenPath, "w") as tokenFile: - json.dump(token, tokenFile) - - def __credentialsFromSession(self): - """ - Private method to create a credentials object. - - @return created credentials object - @rtype google.oauth2.credentials.Credentials - """ - credentials = None - - if self.__clientConfig and self.__session: - token = self.__session.token - if token: - credentials = Credentials( - token["access_token"], - refresh_token=token.get("refresh_token"), - id_token=token.get("id_token"), - token_uri=self.__clientConfig["token_uri"], - client_id=self.__clientConfig["client_id"], - client_secret=self.__clientConfig["client_secret"], - scopes=SCOPES, - ) - credentials.expiry = datetime.datetime.fromtimestamp( - token["expires_at"] - ) - - return credentials + tokenFile.write(credentials.to_json()) def GoogleMailHelp(): @@ -365,23 +186,18 @@ "<ol>" "<li>Use <a href='{0}'>this wizard</a> to create or select a project" " in the Google Developers Console and automatically turn on the API." - " Click <b>Continue</b>, then <b>Go to credentials</b>.</li>" - "<li>At the top of the page, select the <b>OAuth consent screen</b>" - " tab. Select an <b>Email address</b>, enter a <b>Product name</b> if" - " not already set, and click the <b>Save</b> button.</li>" - "<li>Select the <b>Credentials</b> tab, click the <b>Add credentials" - "</b> button and select <b>OAuth 2.0 client ID</b>.</li>" - "<li>Select the application type <b>Other</b>, enter the name "" - "{1}", and click the <b>Create</b>" - " button.</li>" - "<li>Click <b>OK</b> to dismiss the resulting dialog.</li>" - "<li>Click the (Download JSON) button to the right of the client ID." - "</li>" - "<li>Move this file to the eric configuration directory" - " <code>{2}</code> and rename it <code>{3}</code>.</li>" + " Click <b>Continue</b>.</li>" + "<li>Select <b>Create Project</b>, fill out the form and select <b>Create</b>." + " Select <b>Continue</b> and <b>Activate</b>.</li>" + "<li>In the left side pane (menu) select <b>APIs and Services</b></li>" + "<li>Select <b>OAuth consent screen</b>, press <b>Create</b> and" + " fill out the form.</li>" + "<li>In the menu select <b>Credentials</b> and create OAuth credentials.</li>" + "<li>At the end of the process select the <b>Download</b> button below the" + "Client-ID. Save the credentials file to the eric configuration directory" + " <code>{1}</code> with the name <code>{2}</code>.</li>" "</ol>".format( "https://console.developers.google.com/start/api?id=gmail", - APPLICATION_NAME, Globals.getConfigDir(), CLIENT_SECRET_FILE, )