Repository: robocorp/rpaframework

Issue:
`RPA.Email.Exchange`: Enable token refresh update support with OAuth2
Currently we [extend](https://github.com/robocorp/example-oauth-email/blob/master/ExtendedExchange.py) the library to achieve this.
Would be better to implement that directly in the official library and if a `vault_name` & `secret_key` is detected to be set, then we ensure we update the token in the Vault on refresh.

Slack [thread](https://robocorp-developers.slack.com/archives/G01JZMHT50U/p1663766075284119?thread_ts=1662666166.403879&cid=G01JZMHT50U)


Discussion:
Prioritized this due to example [PR](https://github.com/robocorp/example-oauth-email/pull/2).

Customer [thread](https://robocorp-developers.slack.com/archives/C031Z489JQN/p1664225105837269?thread_ts=1662664626.561329&cid=C031Z489JQN)

Resolving patch:
diff --git a/packages/main/src/RPA/Email/Exchange.py b/packages/main/src/RPA/Email/Exchange.py
--- a/packages/main/src/RPA/Email/Exchange.py
+++ b/packages/main/src/RPA/Email/Exchange.py
@@ -30,6 +30,7 @@
 from oauthlib.oauth2 import OAuth2Token
 
 from RPA.Email.common import counter_duplicate_path
+from RPA.Robocorp.Vault import Vault
 
 
 EMAIL_CRITERIA_KEYS = {
@@ -47,6 +48,8 @@
     "importance": "importance",
 }
 
+lib_vault = Vault()
+
 
 def mailbox_to_email_address(mailbox):
     return {
@@ -67,9 +70,12 @@ class OAuth2Creds(OAuth2AuthorizationCodeCredentials):
 
     def __init__(self, *args, on_token_refresh: Callable, **kwargs):
         super().__init__(*args, **kwargs)
+        # Additional behaviour to trigger during token refresh (when it expires), like
+        #  saving the newly generated structure in the Vault.
         self._on_token_refresh = on_token_refresh
 
     def on_token_auto_refreshed(self, access_token: OAuth2Token):
+        super().on_token_auto_refreshed(access_token)
         self._on_token_refresh(access_token)
 
 
@@ -150,6 +156,24 @@ class Exchange:
             body="RPA Python message body",
         )
 
+    **OAuth2**
+
+    .. code-block:: robotframework
+
+        *** Settings ***
+        Library     RPA.Email.Exchange
+        ...     vault_name=email_oauth_microsoft    vault_token_key=token
+
+        Task Setup      Authorize   username=${ACCOUNT}
+        ...    autodiscover=${False}    server=outlook.office365.com
+        ...    is_oauth=${True}  # use the OAuth2 auth code flow
+        # With secrets retrieved from the Vault by using ``Get Secret`` and set
+        #  globally into `${SECRETS}`.
+        ...    client_id=${SECRETS}[client_id]  # app ID
+        ...    client_secret=${SECRETS}[client_secret]  # app password
+        # The entire token structure auto refreshes when it expires.
+        ...    token=${SECRETS}[token]  # token dict (access, refresh, scopes etc.)
+
     **About criterion parameter**
 
     Following table shows possible criterion keys that can be used to filter emails.
@@ -207,20 +231,38 @@ class Exchange:
     ROBOT_LIBRARY_SCOPE = "GLOBAL"
     ROBOT_LIBRARY_DOC_FORMAT = "REST"
 
-    def __init__(self) -> None:
+    def __init__(
+        self, vault_name: Optional[str] = None, vault_token_key: Optional[str] = None
+    ) -> None:
         self.logger = logging.getLogger(__name__)
         self.credentials = None
         self.config = None
         self.account = None
         self._saved_attachments = []
 
+        self._vault_name = vault_name
+        self._vault_token_key = vault_token_key
+
     def on_token_refresh(self, token: OAuth2Token):
         """Callable you can override in order to save the newly obtained token in a
         safe place.
         """
-        self.logger.info(
-            "OAuth2 token was refreshed. (new expiry: %d)", token["expires_at"]
-        )
+        if self._vault_name and self._vault_token_key:
+            secret = lib_vault.get_secret(self._vault_name)
+            secret[self._vault_token_key] = dict(token)
+            lib_vault.set_secret(secret)
+            self.logger.info(
+                "OAuth2 token was refreshed in Vault %r as %r. (new expiry: %d)",
+                self._vault_name,
+                self._vault_token_key,
+                token["expires_at"],
+            )
+        else:
+            self.logger.warning(
+                "OAuth2 token was refreshed but didn't get saved in Vault as well."
+                " (import the library with `vault_name` and `vault_token_key` set in"
+                " order to avoid this)"
+            )
 
     def authorize(
         self,


Test patch: