src/eric7/EricNetwork/EricGoogleMail.py

branch
eric7
changeset 9427
905e7af29101
parent 9413
80c06d472826
child 9473
3f23dbf37dbe
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 &quot;"
-        "{1}&quot;, 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,
         )

eric ide

mercurial