(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())