867 lines
33 KiB
Python
867 lines
33 KiB
Python
"""
|
|
|
|
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.oauth.services.goog import GoogleOAuth
|
|
|
|
# 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
|
|
|
|
|
|
# *****************************************************************************************************************
|
|
# ***** ****
|
|
# *** MACROS / ONE-TIME INIT ***
|
|
# ***** ****
|
|
# *****************************************************************************************************************
|
|
|
|
|
|
# --- Nothing Yet
|
|
|
|
|
|
# *****************************************************************************************************************
|
|
# ***** ****
|
|
# *** VARIABLES ***
|
|
# ***** ****
|
|
# *****************************************************************************************************************
|
|
|
|
|
|
# --- Nothing Yet
|
|
|
|
|
|
# *****************************************************************************************************************
|
|
# ***** ****
|
|
# *** FUNCTIONS ***
|
|
# ***** ****
|
|
# *****************************************************************************************************************
|
|
|
|
|
|
# --- Nothing Yet
|
|
|
|
|
|
# *****************************************************************************************************************
|
|
# ***** ****
|
|
# *** CLASSES ***
|
|
# ***** ****
|
|
# *****************************************************************************************************************
|
|
|
|
|
|
class AsyncGMailClient:
|
|
|
|
def __init__(
|
|
self,
|
|
credentials: Credentials,
|
|
http_client: httpx.AsyncClient,
|
|
debug = True,
|
|
debug_prefix = "GMail | ",
|
|
debug_only_errors = True
|
|
):
|
|
|
|
# Prepare the debugging utility:
|
|
self._debug_prefix = debug_prefix
|
|
self._printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
|
|
if not debug: self._printer.disable()
|
|
self._debug_only_errors = debug_only_errors
|
|
|
|
# Accept the input configuration:
|
|
self.__http_client = http_client
|
|
self.__service = None
|
|
self.__user_email = None
|
|
self.__credentials = credentials
|
|
|
|
def enable_debug(self):
|
|
self._printer.enable()
|
|
|
|
def disable_debug(self):
|
|
self._printer.disable()
|
|
|
|
def debug_only_errors(self):
|
|
self._debug_only_errors = True
|
|
|
|
def debug_everything(self):
|
|
self._debug_only_errors = False
|
|
|
|
# ┏┓ ┏┓ ┏┓ ┏•
|
|
# ┗┓┏┓╋┓┏┏┓ ┣╋ ┃ ┏┓┏┓╋┓┏┓
|
|
# ┗┛┗ ┗┗┻┣┛ ┗┻ ┗┛┗┛┛┗┛┗┗┫
|
|
# ┛ ┛
|
|
|
|
async def initialize(self):
|
|
|
|
"""
|
|
Call this once when the instance is created.
|
|
:return: True if successful, else False.
|
|
"""
|
|
|
|
return await self.__build_service()
|
|
|
|
async def __build_service(self) -> bool:
|
|
|
|
"""
|
|
Build a service object that can be used to perform various activities.
|
|
:return: True if successful, else False.
|
|
"""
|
|
|
|
try:
|
|
|
|
# Build the service object:
|
|
self.__service = build(
|
|
serviceName = "gmail",
|
|
version = "v1",
|
|
credentials = self.__credentials,
|
|
cache_discovery = False
|
|
)
|
|
|
|
# Note down the e-mail address of the user:
|
|
profile = self.__service.users().getProfile(userId = "me").execute()
|
|
self.__user_email = profile.get("emailAddress")
|
|
|
|
# Done here:
|
|
return True
|
|
|
|
# In case something goes wrong:
|
|
except Exception as exception:
|
|
self._printer(exception)
|
|
return False
|
|
|
|
async def set_credentials(
|
|
self,
|
|
credentials: Credentials
|
|
) -> bool:
|
|
|
|
"""
|
|
To update the credentials. Needed for the times when the access token gets refreshed.
|
|
:param credentials: Google's custom 'Credentials' object that describes the OAuth-based access details.
|
|
:return: True if successful, else False.
|
|
"""
|
|
|
|
self.__credentials = credentials
|
|
return await self.__build_service()
|
|
|
|
async def __ensure_token(self) -> None:
|
|
|
|
"""
|
|
Checks if the access token has expired and refreshes if needed.
|
|
:return: None.
|
|
"""
|
|
|
|
if await GoogleOAuth.credentials_have_expired(self.__credentials):
|
|
if not self._debug_only_errors: self._printer("Refreshing Token.", self.__user_email)
|
|
self.__credentials.refresh(Request())
|
|
|
|
# ┏┓ •
|
|
# ┃┃┏┓┏┓┏┓┏┓┏┓╋┓┏┓┏
|
|
# ┣┛┛ ┗┛┣┛┗ ┛ ┗┗┗ ┛
|
|
# ┛
|
|
|
|
@property
|
|
async def user_email(self):
|
|
return self.__user_email
|
|
|
|
@property
|
|
async def user_profile(self):
|
|
return self.__service.users().getProfile(userId = "me").execute()
|
|
|
|
# ┓ ┓ ┓
|
|
# ┃ ┏┓┣┓┏┓┃┏
|
|
# ┗┛┗┻┗┛┗ ┗┛
|
|
|
|
async def list_labels(
|
|
self,
|
|
raise_exception: bool = False
|
|
) -> Dict[str, dict] | None:
|
|
|
|
"""
|
|
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 raise_exception: If set to True, any exceptions that occur will be propagated, else they wil be
|
|
suppressed.
|
|
:return: The list of labels.
|
|
"""
|
|
|
|
# Start by assuming failure:
|
|
labels = None
|
|
|
|
try:
|
|
|
|
# Standard token-refresh check:
|
|
await self.__ensure_token()
|
|
|
|
# Make the API call:
|
|
if not self._debug_only_errors: self._printer("Listing All Labels.", self.__user_email)
|
|
api_response = await self.__http_client.get(
|
|
url = f"https://gmail.googleapis.com/gmail/v1/users/{self.__user_email}/labels",
|
|
headers = {"Authorization": f"Bearer {self.__credentials.token}"}
|
|
)
|
|
|
|
# If the API call failed:
|
|
if api_response.status_code not in [200]: return labels
|
|
|
|
# Else we format the response:
|
|
labels = {label.pop("name"): label for label in api_response.json().get("labels", [])}
|
|
|
|
# In case something goes wrong along the way:
|
|
except Exception as exception:
|
|
if raise_exception: raise
|
|
self._printer(exception)
|
|
labels = None
|
|
|
|
# Done here:
|
|
return labels
|
|
|
|
async def get_label(
|
|
self,
|
|
label_id: str,
|
|
raise_exception: bool = False
|
|
) -> dict | None:
|
|
|
|
"""
|
|
To get one label of this user. the label will be identified by its id.
|
|
DOCUMENTATION:
|
|
1. https://developers.google.com/gmail/api/reference/rest/v1/users.labels/get
|
|
2. https://developers.google.com/gmail/api/reference/rest/v1/users.labels#Label
|
|
:param label_id: The id that Google assigned to the label.
|
|
:param raise_exception: If set to True, any exceptions that occur will be propagated, else they wil be
|
|
suppressed.
|
|
:return: The list of labels.
|
|
"""
|
|
|
|
# Start by assuming failure:
|
|
label = None
|
|
|
|
try:
|
|
|
|
# Standard token-refresh check:
|
|
await self.__ensure_token()
|
|
|
|
# Make the API call:
|
|
if not self._debug_only_errors: self._printer("Getting One Label.", self.__user_email)
|
|
api_response = await self.__http_client.get(
|
|
url = f"https://gmail.googleapis.com/gmail/v1/users/{self.__user_email}/labels/{label_id}",
|
|
headers = {"Authorization": f"Bearer {self.__credentials.token}"}
|
|
)
|
|
|
|
# If the API call failed:
|
|
if api_response.status_code not in [200]: return label
|
|
|
|
# Else we extract the response:
|
|
label = api_response.json()
|
|
|
|
# In case something goes wrong along the way:
|
|
except Exception as exception:
|
|
if raise_exception: raise
|
|
self._printer(exception)
|
|
label = None
|
|
|
|
# Done here:
|
|
return label
|
|
|
|
async def create_label(
|
|
self,
|
|
label_name: str,
|
|
label_visibility: Literal["labelShow", "labelShowIfUnread", "labelHide"] = "labelShow",
|
|
message_visibility: Literal["show", "hide"] = "show",
|
|
label_text_color: str = "#434343",
|
|
label_background_color: str = "#000000",
|
|
raise_exception: bool = False
|
|
) -> bool:
|
|
|
|
"""
|
|
Create one label for the user.
|
|
DOCUMENTATION:
|
|
1. https://developers.google.com/gmail/api/reference/rest/v1/users.labels/create
|
|
2. https://developers.google.com/gmail/api/reference/rest/v1/users.labels#Label
|
|
:param label_name: The display name of the label.
|
|
:param label_visibility: Whether, or not, you would like to show the label in the web UI.
|
|
:param message_visibility: Whether, or not, you would like to show messages with this label in the web UI.
|
|
:param label_text_color: The colour of the text of the label.
|
|
:param label_background_color: The colour of the background/tag 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 label was created, else False.
|
|
"""
|
|
|
|
# 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("Creating One Label.", self.__user_email)
|
|
api_response = await self.__http_client.post(
|
|
url = f"https://gmail.googleapis.com/gmail/v1/users/{self.__user_email}/labels",
|
|
headers = {
|
|
"Authorization": f"Bearer {self.__credentials.token}"
|
|
},
|
|
json = {
|
|
"name": label_name,
|
|
"messageListVisibility": "show" if message_visibility else "hide",
|
|
"labelListVisibility": "labelShow" if label_visibility else "labelHide",
|
|
"color": {
|
|
"textColor": label_text_color.lower(),
|
|
"backgroundColor": label_background_color.lower()
|
|
}
|
|
}
|
|
)
|
|
|
|
# If the API call failed:
|
|
if api_response.status_code not in [200]: return success
|
|
|
|
# Else we note down the success:
|
|
if api_response.json().get("id") is not None: 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 update_label(
|
|
self,
|
|
label_id: str,
|
|
label_name: str = None,
|
|
label_visibility: Literal["labelShow", "labelShowIfUnread", "labelHide"] = None,
|
|
message_visibility: Literal["show", "hide"] = None,
|
|
label_text_color: str = None,
|
|
label_background_color: str = None,
|
|
raise_exception: bool = False
|
|
) -> bool:
|
|
|
|
"""
|
|
Updates one label for the user.
|
|
DOCUMENTATION:
|
|
1. https://developers.google.com/gmail/api/reference/rest/v1/users.labels/create
|
|
2. https://developers.google.com/gmail/api/reference/rest/v1/users.labels#Label
|
|
NOTE: Both or none of the colours must be updated. For this reason, a simple default will be chosen for the
|
|
other if only one is provided.
|
|
:param label_id: The id that Google assigned to the label.
|
|
:param label_name: The display name of the label.
|
|
:param label_visibility: Whether, or not, you would like to show the label in the web UI.
|
|
:param message_visibility: Whether, or not, you would like to show messages with this label in the web UI.
|
|
:param label_text_color: The colour of the text of the label.
|
|
:param label_background_color: The colour of the background/tag 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 label was created, else False.
|
|
"""
|
|
|
|
# Start by assuming failure:
|
|
success = False
|
|
|
|
try:
|
|
|
|
# Standard token-refresh check:
|
|
await self.__ensure_token()
|
|
|
|
# Format the JSON body:
|
|
json_body = {}
|
|
if label_name: json_body["name"] = label_name
|
|
if label_visibility: json_body["labelListVisibility"] = label_visibility
|
|
if message_visibility: json_body["messageListVisibility"] = message_visibility
|
|
if label_text_color or label_background_color:
|
|
json_body["color"] = {
|
|
"textColor": (label_text_color or "#434343").lower(),
|
|
"backgroundColor": (label_background_color or "#000000").lower()
|
|
}
|
|
|
|
# If no value was given to update:
|
|
if not json_body: return success
|
|
|
|
# Make the API call:
|
|
if not self._debug_only_errors: self._printer("Updating One Label.", self.__user_email)
|
|
api_response = await self.__http_client.put(
|
|
url = f"https://gmail.googleapis.com/gmail/v1/users/{self.__user_email}/labels/{label_id}",
|
|
headers = {"Authorization": f"Bearer {self.__credentials.token}"},
|
|
json = json_body
|
|
)
|
|
|
|
# If the API call failed:
|
|
if api_response.status_code not in [200]: return success
|
|
|
|
# Else we note down the success:
|
|
if api_response.json().get("id") is not None: 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_label(
|
|
self,
|
|
label_id: str,
|
|
raise_exception: bool = False
|
|
) -> bool:
|
|
|
|
"""
|
|
To delete one label of this user. the label will be identified by its id.
|
|
DOCUMENTATION:
|
|
1. https://developers.google.com/gmail/api/reference/rest/v1/users.labels/get
|
|
2. https://developers.google.com/gmail/api/reference/rest/v1/users.labels#Label
|
|
:param label_id: The id that Google assigned to the label.
|
|
:param raise_exception: If set to True, any exceptions that occur will be propagated, else they wil be
|
|
suppressed.
|
|
:return: The list of labels.
|
|
"""
|
|
|
|
# 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 One Label.", self.__user_email)
|
|
api_response = await self.__http_client.delete(
|
|
url = f"https://gmail.googleapis.com/gmail/v1/users/{self.__user_email}/labels/{label_id}",
|
|
headers = {"Authorization": f"Bearer {self.__credentials.token}"}
|
|
)
|
|
|
|
# If the API call failed:
|
|
if api_response.status_code not in [200, 204]: return success
|
|
|
|
# Else we extract the response:
|
|
else: 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 __list_messages_on_page(
|
|
self,
|
|
count: int = 100,
|
|
query: str = None,
|
|
label_ids: List[str] | str = None,
|
|
include_spam_and_trash: bool = False,
|
|
next_page_token: str = None,
|
|
raise_exception: bool = False
|
|
) -> Dict[str, Any] | None:
|
|
|
|
"""
|
|
To enlist messages on one page. Google allows at most 500 results on one page. This method respects that
|
|
pagination limit and returns only what Google gives. This method should be used internally by the class and the
|
|
class should expose another method that calls this one in loop to get any arbitrary no. of messages as the user
|
|
desires.
|
|
DOCUMENTATION:
|
|
1. https://developers.google.com/gmail/api/reference/rest/v1/users.messages/list
|
|
2. https://developers.google.com/gmail/api/reference/rest/v1/users.messages#Message
|
|
:param count: The no. of messages to fetch.
|
|
:param query: Any query filter that is supported by GMail.
|
|
:param label_ids: The list of labels' ids that the mails must have on them.
|
|
:param include_spam_and_trash: Whether, or not, you would like to include mails categorized as spam and trash.
|
|
:param next_page_token: The token to fetch the next set of results.
|
|
:param raise_exception: If set to True, any exceptions that occur will be propagated, else they wil be
|
|
suppressed.
|
|
:return: The messages that matched the given conditions if the call was successful, else None.
|
|
"""
|
|
|
|
# Start by assuming failure:
|
|
page_messages = None
|
|
|
|
try:
|
|
|
|
# Standard token-refresh check:
|
|
await self.__ensure_token()
|
|
|
|
# Build the needed params:
|
|
params_json = {
|
|
"maxResults": count,
|
|
"includeSpamTrash": include_spam_and_trash
|
|
}
|
|
if query: params_json["q"] = query
|
|
if next_page_token: params_json["pageToken"] = next_page_token
|
|
if label_ids: params_json["labelIds"] = label_ids if isinstance(label_ids, list) else [label_ids]
|
|
|
|
# Make the API call:
|
|
if not self._debug_only_errors: self._printer(
|
|
"Listing Messages for Page.",
|
|
self.__user_email,
|
|
count,
|
|
next_page_token
|
|
)
|
|
api_response = await self.__http_client.get(
|
|
url = f"https://gmail.googleapis.com/gmail/v1/users/{self.__user_email}/messages",
|
|
headers = {"Authorization": f"Bearer {self.__credentials.token}"},
|
|
params = params_json
|
|
)
|
|
|
|
# If the API call failed:
|
|
if api_response.status_code not in [200]: return page_messages
|
|
|
|
# Else we format the response:
|
|
api_json = api_response.json()
|
|
page_messages = {
|
|
"messages": {m.pop("id"): m for m in api_json.get("messages", [])},
|
|
"nextPageToken": api_json.get("nextPageToken"),
|
|
"resultSizeEstimate": api_json["resultSizeEstimate"],
|
|
}
|
|
|
|
# In case something goes wrong along the way:
|
|
except Exception as exception:
|
|
if raise_exception: raise
|
|
self._printer(exception)
|
|
page_messages = None
|
|
|
|
# Done here:
|
|
return page_messages
|
|
|
|
async def list_messages(
|
|
self,
|
|
count: int = 100,
|
|
query: str = None,
|
|
label_ids: List[str] | str = None,
|
|
include_spam_and_trash: bool = False,
|
|
raise_exception: bool = False
|
|
) -> Dict[str, Any]:
|
|
|
|
"""
|
|
To enlist mail messages from a user's account.
|
|
DOCUMENTATION:
|
|
1. https://developers.google.com/gmail/api/reference/rest/v1/users.messages/list
|
|
2. https://developers.google.com/gmail/api/reference/rest/v1/users.messages#Message
|
|
:param count: The no. of messages to fetch.
|
|
:param query: Any query filter that is supported by GMail.
|
|
:param label_ids: The list of labels' ids that the mails must have on them.
|
|
:param include_spam_and_trash: Whether, or not, you would like to include mails categorized as spam and trash.
|
|
:param raise_exception: If set to True, any exceptions that occur will be propagated, else they wil be
|
|
suppressed.
|
|
:return: The messages that matched the given conditions if the call was successful, else None.
|
|
"""
|
|
|
|
# Start by assuming failure:
|
|
messages = None
|
|
|
|
try:
|
|
|
|
# Standard token-refresh check:
|
|
await self.__ensure_token()
|
|
|
|
# We convert the messages to a dict:
|
|
messages = {
|
|
"messages": {},
|
|
"nextPageToken": None,
|
|
"resultSizeEstimate": 0
|
|
}
|
|
|
|
# Let's figure out how many times we'll have to loop through the process to retrieve the target no. of
|
|
# messages. Google allows you to fetch info about at most 500 messages in one go.
|
|
max_per_call = 500 # ... because Google allows at most 500 entries in one call.
|
|
iterations_needed = int(math.ceil(count / max_per_call))
|
|
last_iteration_count = count - int((max_per_call * (iterations_needed - 1)))
|
|
|
|
# Run the loop those many times:
|
|
next_page_token = None
|
|
results_size_estimate = 0
|
|
for iteration_no in range(iterations_needed):
|
|
|
|
# Figure out the count for this page:
|
|
if iterations_needed > 1:
|
|
if iteration_no < (iterations_needed - 1): iteration_count = max_per_call
|
|
else: iteration_count = last_iteration_count
|
|
else: iteration_count = count
|
|
|
|
# Retrieve the messages for this page:
|
|
iteration_messages = await self.__list_messages_on_page(
|
|
count = iteration_count,
|
|
query = query,
|
|
label_ids = label_ids,
|
|
include_spam_and_trash = include_spam_and_trash,
|
|
next_page_token = next_page_token,
|
|
raise_exception = True
|
|
)
|
|
|
|
# Check if no data was received:
|
|
if iteration_messages is None: break
|
|
if not iteration_messages.get("messages"): break
|
|
|
|
# Now that we know that messages were received:
|
|
for k, v in iteration_messages["messages"].items(): messages["messages"][k] = v
|
|
results_size_estimate += iteration_messages["resultSizeEstimate"]
|
|
|
|
# If there is no next page after this, we break out of the loop:
|
|
next_page_token = iteration_messages["nextPageToken"]
|
|
if next_page_token is None: break
|
|
|
|
# Format the final response:
|
|
messages["nextPageToken"] = next_page_token
|
|
messages["resultSizeEstimate"] = results_size_estimate
|
|
|
|
# In case something goes wrong along the way:
|
|
except Exception as exception:
|
|
if raise_exception: raise
|
|
self._printer(exception)
|
|
messages = None
|
|
|
|
# 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:
|
|
|
|
"""
|
|
To get one message of this user. The message will be identified by its id.
|
|
DOCUMENTATION:
|
|
1. https://developers.google.com/gmail/api/reference/rest/v1/users.messages/list
|
|
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
|
|
: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.
|
|
"""
|
|
|
|
# Start by assuming failure:
|
|
message = None
|
|
|
|
try:
|
|
|
|
# Standard token-refresh check:
|
|
await self.__ensure_token()
|
|
|
|
# Make the API call:
|
|
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}"}
|
|
)
|
|
|
|
# If the API call failed:
|
|
if api_response.status_code not in [200]: return message
|
|
|
|
# Return the raw message if asked:
|
|
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
|
|
)
|
|
|
|
# In case something goes wrong along the way:
|
|
except Exception as exception:
|
|
if raise_exception: raise
|
|
self._printer(exception)
|
|
message = None
|
|
|
|
# Done here:
|
|
return message
|
|
|
|
|
|
# *****************************************************************************************************************
|
|
# ***** ****
|
|
# *** MAIN PROGRAM ***
|
|
# ***** ****
|
|
# *****************************************************************************************************************
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
import dateparser
|
|
|
|
# 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)
|
|
|
|
my_oauth = GoogleOAuth(
|
|
config = secrets_dict,
|
|
redirect_url = r"https://api.thecaoffice.com/converse/mail/callback/gmail",
|
|
debug = True,
|
|
debug_prefix = "OAuth (Goog) | ",
|
|
)
|
|
|
|
tokens = {
|
|
"access_token": "ya29.a0AeDClZAYoo85BXRId_n-hwo_amKshzi46c33GaJcsZZvGB7A7OGU2RFYcWBM_BleNBfAFUSJP2NHAvmd7Nsp_U5Kg68hXSy0iO99PNTm3pvKrJSzbkA-rXsVLsCnBIfPUMyNt2nOOVJmGwm17DNN0jAELkm1fPNTju7SZzmuaCgYKAZwSARMSFQHGX2Mis8TZui2rZT1gKySVds-N0w0175",
|
|
"refresh_token": "1//0gnqzjMf9YT19CgYIARAAGBASNgF-L9Ir3rcY37nGrV45XyOUBRllEH7Txui7T1JbwevlmDoNw7PuMu149cCWQSwsScuKaZusUQ",
|
|
"expires_in": 3539,
|
|
"expires_at": 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(
|
|
http_client = test_client,
|
|
credentials = await my_oauth.credentials_from_tokens(tokens = tokens),
|
|
debug_only_errors = False
|
|
)
|
|
await my_gmail.initialize()
|
|
print(await my_gmail.user_email)
|
|
print(await my_gmail.user_profile)
|
|
|
|
# print(json.to_string(await my_gmail.list_labels()))
|
|
|
|
# print(json.to_string(await my_gmail.get_label(label_id = "Label_3")))
|
|
|
|
# print(json.to_string(await my_gmail.update_label(
|
|
# label_id = "Label_3",
|
|
# label_name = "Updated Label 123",
|
|
# label_background_color = "#cc3a21"
|
|
# )))
|
|
#
|
|
# print(json.to_string(await my_gmail.get_label(label_id = "Label_3")))
|
|
|
|
# print(json.to_string(await my_gmail.create_label(
|
|
# label_name = "Bye - Bye !",
|
|
# label_text_color = "#fbc8d9",
|
|
# label_background_color = "#7a2e0b"
|
|
# )))
|
|
|
|
# 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)
|
|
)
|
|
|
|
|
|
asyncio.run(main())
|