(20241126) GMail v2 started with better data handling and feedback.

This commit is contained in:
2024-11-26 20:36:50 +05:30
parent db80153cb4
commit 629922431d
10 changed files with 1245 additions and 333 deletions
+130 -60
View File
@@ -45,6 +45,7 @@ from utils_v2.string import json
from utils_v2.string import regex
from utils_v2.date_time import date_time
from utils_v2.oauth.services.goog import GoogleOAuth
from utils_v2.mail import mail_parser
# Related to Google:
from google.auth.transport.requests import Request
@@ -69,6 +70,9 @@ from icecream import IceCreamDebugger
# For computational help:
import math
# For base64 encoding:
import base64
# *****************************************************************************************************************
# ***** ****
@@ -679,63 +683,26 @@ class AsyncGMailClient:
# Done here:
return messages
@staticmethod
def format_message(
raw_message: Dict[str, Any],
consider_timezone = date_time.TIMEZONE_UTC,
) -> Dict[str, Any] | None:
message_ts = date_time.parse_date_time(raw_message["internalDate"], timezone = consider_timezone)
message_ts = date_time.to_timezone(message_ts, date_time.TIMEZONE_UTC)
message_headers = {h["name"]: h["value"] for h in raw_message["payload"]["headers"]}
message = {
"ts": message_ts,
"messageId": raw_message["id"],
"threadId": raw_message["threadId"],
"labels": raw_message["labelIds"],
"summary": raw_message["snippet"],
"headers": message_headers,
"from": regex.find_first(text = message_headers["From"], pattern = regex.REGEX_EMAIL_ID),
"to": regex.find_first(text = message_headers["To"], pattern = regex.REGEX_EMAIL_ID),
"cc": [
regex.find_first(
text = cc_mail.strip(),
pattern = regex.REGEX_EMAIL_ID
) for cc_mail in cc_mails.split(",")
] if (cc_mails := message_headers.get("Cc")) else None,
"bcc": [
regex.find_first(
text = bcc_mail.strip(),
pattern = regex.REGEX_EMAIL_ID
) for bcc_mail in bcc_mails.split(",")
] if (bcc_mails := message_headers.get("Bcc")) else None,
"subject": message_headers["Subject"]
}
# Done here:
return message
async def get_message(
self,
message_id: str,
return_raw = False,
consider_timezone = date_time.TIMEZONE_UTC,
raise_exception: bool = False
) -> Dict[str, Any] | None:
) -> Dict[str, Any] | str | None:
"""
To get one message of this user. The message will be identified by its id.
To get one message of this user. The message will be identified by its id. Note that, if you choose to return
the raw message, the message body will be compliant with RFC 5322 and RFC 2045 (among others).
DOCUMENTATION:
1. https://developers.google.com/gmail/api/reference/rest/v1/users.messages/list
1. https://developers.google.com/gmail/api/reference/rest/v1/users.messages/get
2. https://developers.google.com/gmail/api/reference/rest/v1/users.messages#Message
2. https://developers.google.com/gmail/api/reference/rest/v1/users.messages#Message.MessagePart
2. https://developers.google.com/gmail/api/reference/rest/v1/Format
:param message_id: The id that Google assigned to the message.
:param return_raw: Whether you want the raw message or the formatted message.
:param consider_timezone: The timestamp given by Google doesn't have timezone information. Use this parameter to
control what timezone the timestamp is interpreted as.
:param raise_exception: If set to True, any exceptions that occur will be propagated, else they wil be
suppressed.
:return: The list of labels.
:return: The message either parsed as a JSON, or as a raw text body. If the API call fails, the response will be
a null value.
"""
# Start by assuming failure:
@@ -750,21 +717,25 @@ class AsyncGMailClient:
if not self._debug_only_errors: self._printer("Getting One Message.", self.__user_email)
api_response = await self.__http_client.get(
url = f"https://gmail.googleapis.com/gmail/v1/users/{self.__user_email}/messages/{message_id}",
headers = {"Authorization": f"Bearer {self.__credentials.token}"}
headers = {"Authorization": f"Bearer {self.__credentials.token}"},
params = {"format": "raw"}
)
# If the API call failed:
if api_response.status_code not in [200]: return message
# Return the raw message if asked:
# Extract the raw message and respond based on the request:
api_json = api_response.json()
if return_raw: return api_json
# Else we extract and format the response:
message = self.format_message(
raw_message = api_json,
consider_timezone = consider_timezone
)
message = base64.urlsafe_b64decode(api_json["raw"])
if return_raw: message = message.decode()
else:
message = mail_parser.parse(message)
message["labels"] = api_json["labelIds"]
message["messageId"] = api_json["id"]
message["threadId"] = api_json["threadId"]
message["historyId"] = api_json["historyId"]
message["snippet"] = api_json["snippet"]
message["sizeEstimate"] = api_json["sizeEstimate"]
# In case something goes wrong along the way:
except Exception as exception:
@@ -775,6 +746,97 @@ class AsyncGMailClient:
# Done here:
return message
async def modify_messages(
self,
message_ids: List[str] | str,
add_label_ids: List[str] | str = None,
remove_label_ids: List[str] | str = None,
raise_exception: bool = False
) -> bool:
"""
To add or remove labels from messages.
DOCUMENTATION:
1. https://developers.google.com/gmail/api/reference/rest/v1/users.messages/batchModify
:param message_ids: One or more message ids (assigned by Google).
:param add_label_ids: One or more label ids (not the display name of the label).
:param remove_label_ids: One or more label ids (not the display name of the label).
:param raise_exception: If set to True, any exceptions that occur will be propagated, else they wil be
suppressed.
:return: True if the operation succeeded, else False.
"""
# Start by assuming failure:
success = False
try:
# Standard token-refresh check:
await self.__ensure_token()
# Build the needed JSON:
body_json = {"ids": message_ids if isinstance(message_ids, list) else [message_ids]}
if add_label_ids: body_json["addLabelIds"] = add_label_ids if isinstance(add_label_ids, list) else [add_label_ids]
if remove_label_ids: body_json["removeLabelIds"] = remove_label_ids if isinstance(remove_label_ids, list) else [remove_label_ids]
# Make the API call:
if not self._debug_only_errors: self._printer("Modifying Message(s).", self.__user_email)
api_response = await self.__http_client.post(
url = f"https://gmail.googleapis.com/gmail/v1/users/{self.__user_email}/messages/batchModify",
headers = {"Authorization": f"Bearer {self.__credentials.token}"},
json = body_json
)
# Check if our request was successful:
if api_response.status_code in [200, 204]: success = True
# In case something goes wrong along the way:
except Exception as exception:
if raise_exception: raise
self._printer(exception)
success = False
# Done here:
return success
async def delete_messages(
self,
message_ids: List[str] | str,
raise_exception: bool = False
) -> bool:
# Start by assuming failure:
success = False
try:
# Standard token-refresh check:
await self.__ensure_token()
# Make the API call:
if not self._debug_only_errors: self._printer("Deleting Message(s).", self.__user_email)
api_response = await self.__http_client.post(
url = f"https://gmail.googleapis.com/gmail/v1/users/{self.__user_email}/messages/batchDelete",
headers = {"Authorization": f"Bearer {self.__credentials.token}"},
json = {"ids": message_ids if isinstance(message_ids, list) else [message_ids]}
)
print("HTTP CODE:", api_response.status_code)
try: print("HTTP JSON:", json.to_string(api_response.json()))
except Exception as e: print("HTTP JSON:", e)
# Check if our request was successful:
if api_response.status_code in [200, 204]: success = True
# In case something goes wrong along the way:
except Exception as exception:
if raise_exception: raise
self._printer(exception)
success = False
# Done here:
return success
# *****************************************************************************************************************
# ***** ****
@@ -854,13 +916,21 @@ if __name__ == "__main__":
# print(json.to_string(await my_gmail.delete_label(label_id = "Label_6")))
# print(json.to_string(await my_gmail.list_messages(count = 10)))
print(json.to_string(
await my_gmail.get_message(
message_id = "1935e92672e67f22",
return_raw = True
),
default = str)
)
# print(json.to_string(
# await my_gmail.get_message(
# message_id = "19367930033154ca",
# return_raw = False
# ),
# default = str)
# )
# print(json.to_string(await my_gmail.modify_messages(
# message_ids = ["19367930033154ca"],
# add_label_ids = ["Label_5"],
# remove_label_ids = ["Label_3"]
# )))
print(json.to_string(await my_gmail.delete_messages(message_ids = "19367930033154ca")))
asyncio.run(main())
+251
View File
@@ -0,0 +1,251 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
Monday, 25th Nov., 2024
OBJECTIVE:
To manage e-mails in a GMail account.
REFERENCES:
1. Quickstart: https://developers.google.com/gmail/api/quickstart/python
2. Labels: https://developers.google.com/gmail/api/guides/labels
3. Messages: https://developers.google.com/gmail/api/reference/rest/v1/users.messages
DOWNLOADS:
N/A
"""
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
# ***** ****
# *****************************************************************************************************************
# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")
# System-level activities:
import io
# My utils:
from utils_v2.string import json
from utils_v2.string import regex
from utils_v2.date_time import date_time
from utils_v2.mail import mail_parser
# My Google utils:
from utils_v2.oauth.services.goog import GoogleOAuth
from utils_v2.goog.base import AsyncGoogleBase
from utils_v2.goog.models.data.auth_tokens import GoogleAuthTokens
from utils_v2.goog.models.data.api_call import GoogleApiResponse
# Related to Google:
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
# To make API calls:
import httpx
# For asynchronous activities:
import asyncio
# To work with date and time:
import datetime
# For working with datatypes:
from typing import Dict, Literal, List, Any
# For debugging:
from icecream import IceCreamDebugger
# For computational help:
import math
# For base64 encoding:
import base64
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** FUNCTIONS ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** CLASSES ***
# ***** ****
# *****************************************************************************************************************
class AsyncGMailClient(AsyncGoogleBase):
async def get_user_profile(
self,
tokens: GoogleAuthTokens,
user_id: str = "me",
) -> GoogleApiResponse:
# Ensure that the tokens are valid:
await tokens.arefresh(
http_client = self._http_client,
client_id = self._client_id,
client_secret = self._client_secret,
force_refresh = False
)
# Make the API call:
if not self._debug_only_errors: self._printer("Getting User Profile.")
api_response = await self.get(
url = f"https://gmail.googleapis.com/gmail/v1/users/{user_id}/profile",
headers = {"Authorization": f"Bearer {tokens.accessToken}"}
)
# If the call was successful:
if api_response.httpCode in [200]:
api_response.data = await api_response.get_json()
# Done here:
return api_response
# ┓ ┓ ┓
# ┃ ┏┓┣┓┏┓┃┏
# ┗┛┗┻┗┛┗ ┗┛
async def list_labels(
self,
tokens: GoogleAuthTokens,
user_id: str = "me"
) -> GoogleApiResponse:
"""
To get the list of labels of this user.
DOCUMENTATION:
1. https://developers.google.com/gmail/api/reference/rest/v1/users.labels/list
2. https://developers.google.com/gmail/api/reference/rest/v1/users.labels#Label
:param tokens: The object that holds the access token to the service.
:param user_id: The id of the user for which this service must be run. Typically, the e-mail id itself.
:return: A structured response where the list of labels will be in the 'data' variable.
"""
# Ensure that the tokens are valid:
await tokens.arefresh(
http_client = self._http_client,
client_id = self._client_id,
client_secret = self._client_secret,
force_refresh = False
)
# Make the API call:
if not self._debug_only_errors: self._printer("Listing All Labels.", user_id)
api_response = await self.get(
url = f"https://gmail.googleapis.com/gmail/v1/users/{user_id}/labels",
headers = {"Authorization": f"Bearer {tokens.accessToken}"}
)
# If the call was successful:
if api_response.httpCode in [200]:
api_json = await api_response.get_json()
api_response.data = {label.pop("name"): label for label in api_json.get("labels", [])}
# Done here:
return api_response
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
import dateparser
from utils_v2.goog.models.data.auth_tokens import GoogleAuthTokens
# Create an HTTP client:
test_client = httpx.AsyncClient(
limits = httpx.Limits(
max_connections = 100, # ............ Maximum number of connections allowed in the pool.
max_keepalive_connections = 50, # ... Maximum number of connections that can be kept alive.
),
timeout = httpx.Timeout(
connect = 2.5, # ... Shorter connection timeout.
read = 2.5, # ...... Like what EasyEcom gives.
write = 10.0, # .... Time to wait for sending data.
pool = 120.0 # ..... Time to wait for a free connection from the pool.
)
)
secrets_file = r"../../../creds/google_tcaoff_test_oauth_20241125.json"
secrets_dict = json.from_file(secrets_file)
tokens = {
"accessToken": "ya29.a0AeDClZAYoo85BXRId_n-hwo_amKshzi46c33GaJcsZZvGB7A7OGU2RFYcWBM_BleNBfAFUSJP2NHAvmd7Nsp_U5Kg68hXSy0iO99PNTm3pvKrJSzbkA-rXsVLsCnBIfPUMyNt2nOOVJmGwm17DNN0jAELkm1fPNTju7SZzmuaCgYKAZwSARMSFQHGX2Mis8TZui2rZT1gKySVds-N0w0175",
"refreshToken": "1//0gnqzjMf9YT19CgYIARAAGBASNgF-L9Ir3rcY37nGrV45XyOUBRllEH7Txui7T1JbwevlmDoNw7PuMu149cCWQSwsScuKaZusUQ",
"expiresIn": 3539,
"expiresAt": dateparser.parse("2024-11-25 10:40:40.833699+00:00"),
"scopes": [
"https://www.googleapis.com/auth/gmail.labels",
"https://www.googleapis.com/auth/gmail.modify"
]
}
async def main():
my_gmail = AsyncGMailClient(
service_name = "gmail",
client_id = secrets_dict["web"]["client_id"],
client_secret = secrets_dict["web"]["client_secret"],
http_client = test_client,
debug = True,
debug_prefix = "GMail (M) | ",
debug_only_errors = False
)
test_tokens = GoogleAuthTokens(**tokens)
response = await my_gmail.get_user_profile(tokens = test_tokens)
print("RESPONSE:", response)
asyncio.run(main())