Merge commit '6d1f731ff76e21de2ab3c328bad52b6fea435dd5' as 'utils_v2'
This commit is contained in:
@@ -0,0 +1,962 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Monday, 25th Nov., 2024
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To manage e-mails in a GMail account.
|
||||
|
||||
REFERENCES:
|
||||
|
||||
1. GMail Quickstart: https://developers.google.com/gmail/api/quickstart/python
|
||||
2. GMail Labels: https://developers.google.com/gmail/api/guides/labels
|
||||
3. GMail Messages: https://developers.google.com/gmail/api/reference/rest/v1/users.messages
|
||||
4. People Profile: https://developers.google.com/people/api/rest/v1/people/get
|
||||
|
||||
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.goog.controllers.base import AsyncGoogleBase
|
||||
from utils_v2.goog.models.auth_tokens import GoogleAuthTokens
|
||||
from utils_v2.goog.models.api_call import GoogleApiResponse
|
||||
from utils_v2.goog.controllers.gmail.gmail_message import GMailMessage
|
||||
|
||||
# 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
|
||||
import inspect
|
||||
|
||||
# For computational help:
|
||||
import math
|
||||
|
||||
# For base64 encoding:
|
||||
import base64
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# Google Scopes:
|
||||
SCOPES_GMAIL_MAIL_MANAGEMENT = [
|
||||
r"https://www.googleapis.com/auth/gmail.modify",
|
||||
r"https://www.googleapis.com/auth/gmail.labels",
|
||||
# r"profile",
|
||||
r"https://www.googleapis.com/auth/userinfo.profile"
|
||||
]
|
||||
SCOPES_GMAIL_FULL = [
|
||||
r"https://mail.google.com/",
|
||||
# r"profile",
|
||||
r"https://www.googleapis.com/auth/userinfo.profile"
|
||||
]
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** CLASSES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
class AsyncGMailClient(AsyncGoogleBase):
|
||||
|
||||
async def get_user_profile(
|
||||
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/getProfile
|
||||
2. https://developers.google.com/people/api/rest/v1/people/get
|
||||
3. https://developers.google.com/people/api/rest/v1/people#Person
|
||||
: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 or "me".
|
||||
: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 GMail API call:
|
||||
if not self._debug_only_errors: self._printer("Getting User Profile.")
|
||||
gmail_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 gmail_api_response.httpCode in [200]:
|
||||
gmail_api_response.success = True
|
||||
gmail_api_response.data = await gmail_api_response.get_json()
|
||||
gmail_api_response.data["displayName"] = None
|
||||
gmail_api_response.data["displayPictureUrl"] = None
|
||||
|
||||
# Make the People API call:
|
||||
if not self._debug_only_errors: self._printer("Getting User Profile.")
|
||||
people_api_response = await self.get(
|
||||
url = f"https://people.googleapis.com/v1/people/me?personFields=names,photos,birthdays,phoneNumbers,genders,emailAddresses,addresses",
|
||||
headers = {"Authorization": f"Bearer {tokens.accessToken}"}
|
||||
)
|
||||
|
||||
# If the call was successful:
|
||||
if people_api_response.httpCode in [200]:
|
||||
people_api_response.success = True
|
||||
people_api_response.data = await people_api_response.get_json()
|
||||
for item in people_api_response.data.get("names", []):
|
||||
if item["metadata"]["primary"]:
|
||||
gmail_api_response.data["displayName"] = item.get("displayName")
|
||||
for item in people_api_response.data.get("photos", []):
|
||||
if item["metadata"]["primary"]:
|
||||
gmail_api_response.data["displayPictureUrl"] = item.get("url")
|
||||
|
||||
# Done here:
|
||||
return gmail_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 or "me".
|
||||
: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_response.success = True
|
||||
api_json = await api_response.get_json()
|
||||
api_response.data = {label["name"]: label for label in api_json.get("labels", [])}
|
||||
|
||||
# Done here:
|
||||
return api_response
|
||||
|
||||
async def get_label(
|
||||
self,
|
||||
tokens: GoogleAuthTokens,
|
||||
label_id: str,
|
||||
user_id: str = "me"
|
||||
) -> GoogleApiResponse:
|
||||
|
||||
"""
|
||||
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 tokens: The object that holds the access token to the service.
|
||||
:param label_id: The id that Google assigned to the label.
|
||||
:param user_id: The id of the user for which this service must be run. Typically, the e-mail id itself or "me".
|
||||
: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("Getting One Label.", user_id)
|
||||
api_response = await self.get(
|
||||
url = f"https://gmail.googleapis.com/gmail/v1/users/{user_id}/labels/{label_id}",
|
||||
headers = {"Authorization": f"Bearer {tokens.accessToken}"}
|
||||
)
|
||||
|
||||
# If the call was successful:
|
||||
if api_response.httpCode in [200]:
|
||||
api_response.success = True
|
||||
api_response.data = await api_response.get_json()
|
||||
|
||||
# Done here:
|
||||
return api_response
|
||||
|
||||
async def create_label(
|
||||
self,
|
||||
tokens: GoogleAuthTokens,
|
||||
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",
|
||||
user_id: str = "me"
|
||||
) -> GoogleApiResponse:
|
||||
|
||||
"""
|
||||
Create one label for the user. Doesn't apply it to any mail, just creates it.
|
||||
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 tokens: The object that holds the access token to the service.
|
||||
: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 user_id: The id of the user for which this service must be run. Typically, the e-mail id itself or "me".
|
||||
: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("Creating One Label.", user_id)
|
||||
api_response = await self.post(
|
||||
url = f"https://gmail.googleapis.com/gmail/v1/users/{user_id}/labels",
|
||||
headers = {"Authorization": f"Bearer {tokens.accessToken}"},
|
||||
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 call was successful:
|
||||
if api_response.httpCode in [200]:
|
||||
api_response.success = True
|
||||
api_response.data = await api_response.get_json()
|
||||
|
||||
# Done here:
|
||||
return api_response
|
||||
|
||||
async def update_label(
|
||||
self,
|
||||
tokens: GoogleAuthTokens,
|
||||
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,
|
||||
user_id: str = "me"
|
||||
) -> GoogleApiResponse:
|
||||
|
||||
"""
|
||||
Updates one label for the user.
|
||||
DOCUMENTATION:
|
||||
1. https://developers.google.com/gmail/api/reference/rest/v1/users.labels/update
|
||||
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 tokens: The object that holds the access token to the service.
|
||||
: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 user_id: The id of the user for which this service must be run. Typically, the e-mail id itself or "me".
|
||||
: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
|
||||
)
|
||||
|
||||
# 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()
|
||||
}
|
||||
|
||||
# Make the API call:
|
||||
if not self._debug_only_errors: self._printer("Updating One Label.", user_id)
|
||||
api_response = await self.put(
|
||||
url = f"https://gmail.googleapis.com/gmail/v1/users/{user_id}/labels/{label_id}",
|
||||
headers = {"Authorization": f"Bearer {tokens.accessToken}"},
|
||||
json = json_body
|
||||
)
|
||||
|
||||
# If the call was successful:
|
||||
if api_response.httpCode in [200]:
|
||||
api_response.success = True
|
||||
api_response.data = await api_response.get_json()
|
||||
|
||||
# Done here:
|
||||
return api_response
|
||||
|
||||
async def delete_label(
|
||||
self,
|
||||
tokens: GoogleAuthTokens,
|
||||
label_id: str,
|
||||
user_id: str = "me"
|
||||
) -> GoogleApiResponse:
|
||||
|
||||
"""
|
||||
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/delete
|
||||
:param tokens: The object that holds the access token to the service.
|
||||
:param label_id: The id that Google assigned to the label.
|
||||
:param user_id: The id of the user for which this service must be run. Typically, the e-mail id itself or "me".
|
||||
: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("Deleting One Label.", user_id)
|
||||
api_response = await self.delete(
|
||||
url = f"https://gmail.googleapis.com/gmail/v1/users/{user_id}/labels/{label_id}",
|
||||
headers = {"Authorization": f"Bearer {tokens.accessToken}"}
|
||||
)
|
||||
|
||||
# If the call was successful:
|
||||
if api_response.httpCode in [200, 204]:
|
||||
api_response.success = True
|
||||
|
||||
# Done here:
|
||||
return api_response
|
||||
|
||||
# ┳┳┓
|
||||
# ┃┃┃┏┓┏┏┏┓┏┓┏┓┏
|
||||
# ┛ ┗┗ ┛┛┗┻┗┫┗ ┛
|
||||
# ┛
|
||||
|
||||
async def __list_messages_on_page(
|
||||
self,
|
||||
tokens: GoogleAuthTokens,
|
||||
max_count: int = 100,
|
||||
query: str = None,
|
||||
label_ids: List[str] | str = None,
|
||||
include_spam_and_trash: bool = False,
|
||||
next_page_token: str = None,
|
||||
user_id: str = "me"
|
||||
) -> GoogleApiResponse:
|
||||
|
||||
"""
|
||||
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 tokens: The object that holds the access token to the service.
|
||||
:param max_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 user_id: The id of the user for which this service must be run. Typically, the e-mail id itself or "me".
|
||||
: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
|
||||
)
|
||||
|
||||
# Build the needed params:
|
||||
params_json = {
|
||||
"maxResults": max_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.", user_id, max_count, next_page_token)
|
||||
api_response = await self.get(
|
||||
url = f"https://gmail.googleapis.com/gmail/v1/users/{user_id}/messages",
|
||||
headers = {"Authorization": f"Bearer {tokens.accessToken}"},
|
||||
params = params_json
|
||||
)
|
||||
|
||||
# If the call was successful:
|
||||
if api_response.httpCode in [200]:
|
||||
api_response.success = True
|
||||
api_json = await api_response.get_json()
|
||||
api_response.data = {
|
||||
"messages": {m["id"]: m for m in api_json.get("messages", [])},
|
||||
"nextPageToken": api_json.get("nextPageToken"),
|
||||
"resultSizeEstimate": api_json["resultSizeEstimate"],
|
||||
}
|
||||
|
||||
# Done here:
|
||||
return api_response
|
||||
|
||||
async def list_messages(
|
||||
self,
|
||||
tokens: GoogleAuthTokens,
|
||||
max_count: int = 100,
|
||||
query: str = None,
|
||||
label_ids: List[str] | str = None,
|
||||
include_spam_and_trash: bool = False,
|
||||
next_page_token: str = None,
|
||||
user_id: str = "me"
|
||||
) -> GoogleApiResponse:
|
||||
|
||||
"""
|
||||
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 tokens: The object that holds the access token to the service.
|
||||
:param max_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 user_id: The id of the user for which this service must be run. Typically, the e-mail id itself or "me".
|
||||
: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
|
||||
)
|
||||
|
||||
# We create a variable that will hold the results.
|
||||
# We must supply the URL, Method and a few other params here due to the custom looping functionality:
|
||||
all_messages = GoogleApiResponse(
|
||||
serviceName = self._service_name,
|
||||
action = inspect.stack()[0].function,
|
||||
url = f"https://gmail.googleapis.com/gmail/v1/users/{user_id}/messages",
|
||||
method = "GET",
|
||||
data = {
|
||||
"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(max_count / max_per_call))
|
||||
last_iteration_count = max_count - int((max_per_call * (iterations_needed - 1)))
|
||||
|
||||
# Run the loop those many times:
|
||||
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 = max_count
|
||||
|
||||
# Retrieve the messages for this page:
|
||||
iteration_response = await self.__list_messages_on_page(
|
||||
tokens = tokens,
|
||||
max_count = iteration_count,
|
||||
query = query,
|
||||
label_ids = label_ids,
|
||||
include_spam_and_trash = include_spam_and_trash,
|
||||
next_page_token = next_page_token
|
||||
)
|
||||
|
||||
# Check if no data was received:
|
||||
if (
|
||||
iteration_response.data is None or
|
||||
not iteration_response.data.get("messages")
|
||||
): break
|
||||
|
||||
# Now that we know that messages were received:
|
||||
for k, v in iteration_response.data["messages"].items(): all_messages.data["messages"][k] = v
|
||||
results_size_estimate += iteration_response.data["resultSizeEstimate"]
|
||||
|
||||
# Also copy the API call params:
|
||||
all_messages.httpCode = iteration_response.httpCode
|
||||
all_messages.message = iteration_response.message
|
||||
all_messages.success = iteration_response.success
|
||||
|
||||
# If there is no next page after this, we break out of the loop:
|
||||
next_page_token = iteration_response.data["nextPageToken"]
|
||||
if next_page_token is None: break
|
||||
|
||||
# Format the final response:
|
||||
all_messages.data["nextPageToken"] = next_page_token
|
||||
all_messages.data["resultSizeEstimate"] = results_size_estimate
|
||||
|
||||
# Done here:
|
||||
return all_messages
|
||||
|
||||
async def get_message(
|
||||
self,
|
||||
tokens: GoogleAuthTokens,
|
||||
message_id: str,
|
||||
return_raw: bool = False,
|
||||
user_id: str = "me"
|
||||
) -> GoogleApiResponse:
|
||||
|
||||
"""
|
||||
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/get
|
||||
2. https://developers.google.com/gmail/api/reference/rest/v1/users.messages#Message
|
||||
2. https://developers.google.com/gmail/api/reference/rest/v1/Format
|
||||
:param tokens: The object that holds the access token to the service.
|
||||
: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 user_id: The id of the user for which this service must be run. Typically, the e-mail id itself or "me".
|
||||
: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("Getting One Message.", user_id)
|
||||
api_response = await self.get(
|
||||
url = f"https://gmail.googleapis.com/gmail/v1/users/{user_id}/messages/{message_id}",
|
||||
headers = {"Authorization": f"Bearer {tokens.accessToken}"},
|
||||
params = {"format": "raw"}
|
||||
)
|
||||
|
||||
# If the call was successful:
|
||||
if api_response.httpCode in [200]:
|
||||
api_response.success = True
|
||||
api_json = await api_response.get_json()
|
||||
raw_message = base64.urlsafe_b64decode(api_json["raw"]).decode()
|
||||
if return_raw: api_response.data = raw_message
|
||||
else:
|
||||
parsed_message = mail_parser.parse(raw_message)
|
||||
parsed_message["labels"] = api_json["labelIds"]
|
||||
parsed_message["messageId"] = api_json["id"]
|
||||
parsed_message["threadId"] = api_json["threadId"]
|
||||
parsed_message["historyId"] = api_json["historyId"]
|
||||
parsed_message["snippet"] = api_json["snippet"]
|
||||
parsed_message["sizeEstimate"] = api_json["sizeEstimate"]
|
||||
api_response.data = parsed_message
|
||||
|
||||
# Done here:
|
||||
return api_response
|
||||
|
||||
async def modify_messages(
|
||||
self,
|
||||
tokens: GoogleAuthTokens,
|
||||
message_ids: List[str] | str,
|
||||
add_label_ids: List[str] | str = None,
|
||||
remove_label_ids: List[str] | str = None,
|
||||
user_id: str = "me"
|
||||
) -> GoogleApiResponse:
|
||||
|
||||
"""
|
||||
To add or remove labels from one or more messages.
|
||||
DOCUMENTATION:
|
||||
1. https://developers.google.com/gmail/api/reference/rest/v1/users.messages/batchModify
|
||||
:param tokens: The object that holds the access token to the service.
|
||||
: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 user_id: The id of the user for which this service must be run. Typically, the e-mail id itself or "me".
|
||||
: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
|
||||
)
|
||||
|
||||
# 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).", user_id)
|
||||
api_response = await self.post(
|
||||
url = f"https://gmail.googleapis.com/gmail/v1/users/{user_id}/messages/batchModify",
|
||||
headers = {"Authorization": f"Bearer {tokens.accessToken}"},
|
||||
json = body_json
|
||||
)
|
||||
|
||||
# If the call was successful:
|
||||
if api_response.httpCode in [200, 204]:
|
||||
api_response.success = True
|
||||
|
||||
# Done here:
|
||||
return api_response
|
||||
|
||||
async def delete_messages(
|
||||
self,
|
||||
tokens: GoogleAuthTokens,
|
||||
message_ids: List[str] | str,
|
||||
user_id: str = "me"
|
||||
) -> GoogleApiResponse:
|
||||
|
||||
"""
|
||||
To PERMANENTLY delete one or more messages.
|
||||
DOCUMENTATION:
|
||||
1. https://developers.google.com/gmail/api/reference/rest/v1/users.messages/batchDelete
|
||||
:param tokens: The object that holds the access token to the service.
|
||||
:param message_ids: One or more message ids (assigned by Google).
|
||||
:param user_id: The id of the user for which this service must be run. Typically, the e-mail id itself or "me".
|
||||
: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("Deleting Message(s).", user_id)
|
||||
api_response = await self.post(
|
||||
url = f"https://gmail.googleapis.com/gmail/v1/users/{user_id}/messages/batchDelete",
|
||||
headers = {"Authorization": f"Bearer {tokens.accessToken}"},
|
||||
json = {"ids": message_ids if isinstance(message_ids, list) else [message_ids]}
|
||||
)
|
||||
|
||||
# If the call was successful:
|
||||
if api_response.httpCode in [200, 204]:
|
||||
api_response.success = True
|
||||
|
||||
# Done here:
|
||||
return api_response
|
||||
|
||||
async def trash_message(
|
||||
self,
|
||||
tokens: GoogleAuthTokens,
|
||||
message_id: str,
|
||||
user_id: str = "me"
|
||||
) -> GoogleApiResponse:
|
||||
|
||||
"""
|
||||
To move one message to trash.
|
||||
DOCUMENTATION:
|
||||
1. https://developers.google.com/gmail/api/reference/rest/v1/users.messages/trash
|
||||
2. https://developers.google.com/gmail/api/reference/rest/v1/users.messages#Message
|
||||
:param tokens: The object that holds the access token to the service.
|
||||
:param message_id: The id of the message (assigned by Google).
|
||||
:param user_id: The id of the user for which this service must be run. Typically, the e-mail id itself or "me".
|
||||
: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("Trashing One Message.", user_id)
|
||||
api_response = await self.post(
|
||||
url = f"https://gmail.googleapis.com/gmail/v1/users/{user_id}/messages/{message_id}/trash",
|
||||
headers = {"Authorization": f"Bearer {tokens.accessToken}"},
|
||||
)
|
||||
|
||||
# If the call was successful:
|
||||
if api_response.httpCode in [200, 204]:
|
||||
api_response.success = True
|
||||
api_response.data = await api_response.get_json()
|
||||
|
||||
# Done here:
|
||||
return api_response
|
||||
|
||||
async def untrash_message(
|
||||
self,
|
||||
tokens: GoogleAuthTokens,
|
||||
message_id: str,
|
||||
user_id: str = "me"
|
||||
) -> GoogleApiResponse:
|
||||
|
||||
"""
|
||||
To move one message to trash.
|
||||
DOCUMENTATION:
|
||||
1. https://developers.google.com/gmail/api/reference/rest/v1/users.messages/trash
|
||||
2. https://developers.google.com/gmail/api/reference/rest/v1/users.messages#Message
|
||||
:param tokens: The object that holds the access token to the service.
|
||||
:param message_id: The id of the message (assigned by Google).
|
||||
:param user_id: The id of the user for which this service must be run. Typically, the e-mail id itself or "me".
|
||||
: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("Un-Trashing One Message.", user_id)
|
||||
api_response = await self.post(
|
||||
url = f"https://gmail.googleapis.com/gmail/v1/users/{user_id}/messages/{message_id}/untrash",
|
||||
headers = {"Authorization": f"Bearer {tokens.accessToken}"},
|
||||
)
|
||||
|
||||
# If the call was successful:
|
||||
if api_response.httpCode in [200]:
|
||||
api_response.success = True
|
||||
api_response.data = await api_response.get_json()
|
||||
|
||||
# Done here:
|
||||
return api_response
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
tokens: GoogleAuthTokens,
|
||||
message: GMailMessage,
|
||||
thread_id: str = None,
|
||||
user_id: str = "me"
|
||||
) -> GoogleApiResponse:
|
||||
|
||||
"""
|
||||
To send one message.
|
||||
NOTE: If you want to apply a custom label to your outgoing mails, this API endpoint doesn't allow you to do that
|
||||
in one go. Instead, you should note down the 'id' field from a successful response and use the 'modify_messages'
|
||||
method of this class to immediately apply that label to the mail in a separate call.
|
||||
DOCUMENTATION:
|
||||
1. https://developers.google.com/gmail/api/reference/rest/v1/users.messages/send
|
||||
2. https://developers.google.com/gmail/api/reference/rest/v1/users.messages#Message
|
||||
:param tokens: The object that holds the access token to the service.
|
||||
:param message: The object that has the content of the message to be sent.
|
||||
:param thread_id: Replies to an existing mail if the correct thread-id is specified. If not specified, a new
|
||||
mail with a new thread-id is created.
|
||||
:param user_id: The id of the user for which this service must be run. Typically, the e-mail id itself or "me".
|
||||
: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
|
||||
)
|
||||
|
||||
# Construct the JSON body:
|
||||
json_body = {"raw": message.get_raw_message(as_base64 = True)}
|
||||
if thread_id: json_body["threadId"] = thread_id
|
||||
|
||||
# Make the API call:
|
||||
if not self._debug_only_errors: self._printer("Sending One Message.", user_id)
|
||||
api_response = await self.post(
|
||||
url = f"https://gmail.googleapis.com/gmail/v1/users/{user_id}/messages/send",
|
||||
headers = {
|
||||
"Authorization": f"Bearer {tokens.accessToken}",
|
||||
"Content-Type": "message/rfc822"
|
||||
},
|
||||
json = json_body
|
||||
)
|
||||
|
||||
# If the call was successful:
|
||||
if api_response.httpCode in [200]:
|
||||
api_response.success = True
|
||||
api_response.data = await api_response.get_json()
|
||||
|
||||
# Done here:
|
||||
return api_response
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MAIN PROGRAM ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
# 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.
|
||||
)
|
||||
)
|
||||
|
||||
# Read the secrets that give you access to the app:
|
||||
secrets_file = r"../../../creds/goog/app/google_tcaoff_test_oauth_20241125.json"
|
||||
secrets_dict = json.from_file(secrets_file)
|
||||
|
||||
async def main():
|
||||
|
||||
# Create an instance of the client:
|
||||
my_gmail = AsyncGMailClient(
|
||||
service_name = "gmail",
|
||||
oauth_json = secrets_dict,
|
||||
http_client = test_client,
|
||||
redirect_url = r"https://api.thecaoffice.com/converse/mail/callback/gmail",
|
||||
debug = True,
|
||||
debug_prefix = "GMail (M) | ",
|
||||
debug_only_errors = False
|
||||
)
|
||||
|
||||
# Request Auth:
|
||||
print("AUTH URL:", await my_gmail.get_authorization_url(
|
||||
scopes = SCOPES_GMAIL_MAIL_MANAGEMENT,
|
||||
state = "Bhopli",
|
||||
approval_prompt = "force"
|
||||
))
|
||||
|
||||
# Get tokens from callback:
|
||||
test_tokens = await my_gmail.get_authorization_tokens(
|
||||
scopes = SCOPES_GMAIL_MAIL_MANAGEMENT,
|
||||
redirect_url = input("Paste the redirect URL here: ")
|
||||
)
|
||||
print("TOKENS:", test_tokens)
|
||||
|
||||
# Test some feature:
|
||||
response = await my_gmail.get_user_profile(tokens = test_tokens)
|
||||
print("SUCCESS:", response.success)
|
||||
print("SUMMARY:", response.to_markdown())
|
||||
print("\n\n---\n\n")
|
||||
print("DATA:", json.to_string(response.data, default = str))
|
||||
if not response.success:
|
||||
print("\n\n---\n\n")
|
||||
print("FULL RESPONSE JSON:", json.to_string(await response.get_json()))
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,336 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Tuesday, 16th Jul, 2024
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To be able to send out mails from code.
|
||||
|
||||
REFERENCES:
|
||||
|
||||
N/A
|
||||
|
||||
DOWNLOADS:
|
||||
|
||||
N/A
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** IMPORT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# To make sibling directories accessible for imports:
|
||||
import sys
|
||||
sys.path.append(".")
|
||||
sys.path.append("..")
|
||||
|
||||
# For working with mails:
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.image import MIMEImage
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.base import MIMEBase
|
||||
from email import encoders
|
||||
|
||||
# For random strings:
|
||||
import string
|
||||
import random
|
||||
|
||||
# For system-level activities:
|
||||
import os
|
||||
|
||||
# For working with files in RAM:
|
||||
import io
|
||||
|
||||
# To work with Base64 encoding:
|
||||
import base64
|
||||
|
||||
# To work with datatypes:
|
||||
from typing import List
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** CLASSES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
class GMailMessage:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
from_email: str,
|
||||
to_email: str | List[str],
|
||||
subject: str,
|
||||
cc_emails: str | List[str] = None,
|
||||
bcc_emails: str | List[str] = None
|
||||
):
|
||||
|
||||
"""
|
||||
Create an instance of the message that you would like to send.
|
||||
:param from_email: The EMail ID of the sender.
|
||||
:param to_email: The EMail ID of the recipient.
|
||||
:param subject: The subject of the mail.
|
||||
:param cc_emails: A list of recipients to add to the CC section.
|
||||
:param bcc_emails: A list of recipients to add to the BCC section.
|
||||
"""
|
||||
|
||||
# Create the instance of the message:
|
||||
self.message = MIMEMultipart()
|
||||
self.message["From"] = from_email
|
||||
self.message["To"] = ",".join(to_email if isinstance(to_email, list) else [to_email])
|
||||
self.message["Subject"] = subject
|
||||
if cc_emails: self.message["CC"] = ",".join(cc_emails if isinstance(cc_emails, list) else [cc_emails])
|
||||
if bcc_emails: self.message["BCC"] = ",".join(bcc_emails if isinstance(bcc_emails, list) else [bcc_emails])
|
||||
|
||||
# Note down the values for accessing later:
|
||||
self.__from = from_email
|
||||
self.__to = to_email
|
||||
self.__cc = cc_emails,
|
||||
self.__bcc = bcc_emails
|
||||
self.__subject = subject
|
||||
|
||||
# ┏┓ •
|
||||
# ┃┃┏┓┏┓┏┓┏┓┏┓╋┓┏┓┏
|
||||
# ┣┛┛ ┗┛┣┛┗ ┛ ┗┗┗ ┛
|
||||
# ┛
|
||||
|
||||
@property
|
||||
def from_mail(self):
|
||||
return self.__from
|
||||
|
||||
@property
|
||||
def to_mail(self):
|
||||
return self.__to
|
||||
|
||||
@property
|
||||
def cc_mails(self):
|
||||
return self.__cc
|
||||
|
||||
@property
|
||||
def bcc_mails(self):
|
||||
return self.__bcc
|
||||
|
||||
@property
|
||||
def subject(self):
|
||||
return self.__subject
|
||||
|
||||
# ┏┓ ┓ ┓ ┏┓
|
||||
# ┣┫┏┫┏┫ ┃ ┏┓┏┓╋┏┓┏┓╋
|
||||
# ┛┗┗┻┗┻ ┗┛┗┛┛┗┗┗ ┛┗┗
|
||||
|
||||
def add_text(self, text):
|
||||
|
||||
"""
|
||||
Add plain-text to the mail body.
|
||||
:param text: The text to add to the mail body.
|
||||
:return: None.
|
||||
"""
|
||||
|
||||
self.message.attach(MIMEText(text, "plain"))
|
||||
|
||||
def add_html(self, html_text):
|
||||
|
||||
"""
|
||||
Add HTML text to the mail body.
|
||||
:param html_text: The HTML text to add to the mail body.
|
||||
:return: None.
|
||||
"""
|
||||
|
||||
self.message.attach(MIMEText(html_text, "html"))
|
||||
|
||||
def add_inline_image(
|
||||
self,
|
||||
image_file: str | io.BytesIO,
|
||||
file_name: str = None,
|
||||
content_id: str = None
|
||||
):
|
||||
|
||||
"""
|
||||
Add an inline image to the body of the mail.
|
||||
NOTE: This is NOT the same as sending an image as an attachment.
|
||||
:param image_file: The image data to attach to the mail body.
|
||||
:param file_name: The name of the file. This is the same name by which it will be downloaded. You need not
|
||||
specify this if the input file is specified as a path. Needed when you give the input file as a buffer.
|
||||
:param content_id: Inline images are inserted via HTML bocks. This field identifies the image resource. If not
|
||||
specified, I will generate a random string. You may write a custom value here if you know what you are
|
||||
doing. For most use cases, please ignore this field.
|
||||
:return: None.
|
||||
"""
|
||||
|
||||
# Read the image as bytes:
|
||||
image_bytes = None
|
||||
if type(image_file) is str:
|
||||
file_name = file_name or os.path.split(image_file)[-1]
|
||||
with open(image_file, "rb") as opened_image_file:
|
||||
image_bytes = opened_image_file.read()
|
||||
if type(image_file) is io.BytesIO:
|
||||
image_file.seek(0)
|
||||
image_bytes = image_file.getvalue()
|
||||
|
||||
# Declare the part to be attached to the multipart message:
|
||||
if image_bytes is not None:
|
||||
|
||||
# Create the HTML block if the image pointer is blank:
|
||||
if content_id is None:
|
||||
content_id = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8))
|
||||
self.add_html(f"""
|
||||
<html>
|
||||
<body>
|
||||
<p><img src="cid:{content_id}"></p>
|
||||
</body>
|
||||
</html>
|
||||
""")
|
||||
|
||||
# Then add the image:
|
||||
image_part = MIMEImage(image_bytes)
|
||||
image_part.add_header(
|
||||
"Content-ID", f"<{content_id}>"
|
||||
)
|
||||
image_part.add_header(
|
||||
"Content-Disposition",
|
||||
f"inline; filename=\"{file_name}\"",
|
||||
)
|
||||
self.message.attach(image_part)
|
||||
|
||||
def add_attachment(
|
||||
self,
|
||||
attachment_file: str | io.BytesIO,
|
||||
file_name: str = None
|
||||
):
|
||||
|
||||
"""
|
||||
Add a file as an attachment to the mail. This file, even if possible, will not be rendered on the screen in-line
|
||||
with the body. It will be made available as a download.
|
||||
:param attachment_file: The file that you would like to attach.
|
||||
:param file_name: The name of the file. This is the same name by which it will be downloaded. You need not
|
||||
specify this if the input file is specified as a path. Needed when you give the input file as a buffer.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# Declare the part to be attached to the multipart message:
|
||||
part = MIMEBase("application", "octet-stream")
|
||||
|
||||
# If the attachment is a file stored in the local disk:
|
||||
if isinstance(attachment_file, str):
|
||||
file_name = file_name or os.path.split(attachment_file)[-1]
|
||||
with open(attachment_file, "rb") as attachment:
|
||||
part.set_payload(attachment.read())
|
||||
|
||||
# If the file is held in RAM:
|
||||
elif isinstance(attachment_file, io.BytesIO):
|
||||
attachment_file.seek(0)
|
||||
part.set_payload(attachment_file.read())
|
||||
|
||||
# Encode and attach the file:
|
||||
encoders.encode_base64(part)
|
||||
part.add_header(
|
||||
"Content-Disposition",
|
||||
f"attachment; filename=\"{file_name}\"",
|
||||
)
|
||||
self.message.attach(part)
|
||||
|
||||
# ┏┓
|
||||
# ┣ ┏┓┏┓┏┳┓┏┓╋
|
||||
# ┻ ┗┛┛ ┛┗┗┗┻┗
|
||||
|
||||
def get_raw_message(
|
||||
self,
|
||||
as_base64: bool = True
|
||||
):
|
||||
|
||||
"""
|
||||
Get the raw string dump from the current contents of the message. This text will be compliant with RFC 5322 and
|
||||
RFC 2045 (among others).
|
||||
:param as_base64: If set to True, the response will be a URL-safe B64 output, else it'll be a raw string.
|
||||
:return: The standardized raw text dump. either as a raw string or as a Base64 (url-safe) string.
|
||||
"""
|
||||
|
||||
if not as_base64: return self.message
|
||||
else: return base64.urlsafe_b64encode(self.message.as_bytes()).decode()
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MAIN PROGRAM ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
my_mail = GMailMessage(
|
||||
from_email = "sender@gmail.com",
|
||||
to_email = "recipient@gmail.com",
|
||||
subject = "Bhopli is the best!",
|
||||
cc_emails = None,
|
||||
bcc_emails = None
|
||||
)
|
||||
my_mail.add_html(
|
||||
"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Sample HTML String</title>
|
||||
<style>
|
||||
.heading {
|
||||
color: #ff9025;
|
||||
}
|
||||
.sub-heading {
|
||||
color: #000000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="heading">Hello, Bhopli!</h1>
|
||||
<h2 class="sub-heading">Bhopli is the best, most well-behaved cat in the known universe.</h2>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
)
|
||||
my_mail.add_text("This is how you should pet her 👇")
|
||||
my_mail.add_inline_image(r"../../../../data/images/cat_petting.png")
|
||||
my_mail.add_attachment(r"../../../../data/pdf/sample_label.pdf")
|
||||
print(my_mail.get_raw_message(as_base64 = False))
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"_id": ObjectId("..."), // MongoDB auto-generated ID
|
||||
"message_id": "unique-message-id", // Unique identifier for the email
|
||||
"thread_id": "thread-id", // Optional: Group of related messages
|
||||
"subject": "Subject of the email",
|
||||
"from": {
|
||||
"name": "Sender Name", // Name of the sender
|
||||
"email": "sender@example.com" // Sender's email address
|
||||
},
|
||||
"to": [ // List of recipients
|
||||
{
|
||||
"name": "Recipient Name",
|
||||
"email": "recipient@example.com"
|
||||
}
|
||||
],
|
||||
"cc": [ // List of CC recipients (optional)
|
||||
{
|
||||
"name": "CC Name",
|
||||
"email": "cc@example.com"
|
||||
}
|
||||
],
|
||||
"bcc": [ // List of BCC recipients (optional)
|
||||
{
|
||||
"name": "BCC Name",
|
||||
"email": "bcc@example.com"
|
||||
}
|
||||
],
|
||||
"date": ISODate("2024-11-26T12:00:00Z"), // Date the email was sent
|
||||
"headers": { // Raw headers from the email
|
||||
"X-Priority": "3",
|
||||
"Content-Type": "multipart/alternative; boundary=\"boundary\"",
|
||||
"X-Mailer": "Mailer XYZ"
|
||||
},
|
||||
"body": { // The body of the email, with parts if multipart
|
||||
"text": "Plain text body content", // Plain text part (if any)
|
||||
"html": "<p>HTML body content</p>", // HTML part (if any)
|
||||
"parts": [ // List of parts for multipart emails
|
||||
{
|
||||
"content_type": "text/plain",
|
||||
"content_transfer_encoding": "base64",
|
||||
"content": "base64-encoded-content-here"
|
||||
},
|
||||
{
|
||||
"content_type": "text/html",
|
||||
"content_transfer_encoding": "base64",
|
||||
"content": "base64-encoded-html-content-here"
|
||||
}
|
||||
]
|
||||
},
|
||||
"attachments": [ // Attachments in the email
|
||||
{
|
||||
"filename": "file1.pdf",
|
||||
"content_type": "application/pdf",
|
||||
"content_transfer_encoding": "base64",
|
||||
"content": "base64-encoded-file-content"
|
||||
},
|
||||
{
|
||||
"filename": "image1.jpg",
|
||||
"content_type": "image/jpeg",
|
||||
"content_transfer_encoding": "base64",
|
||||
"content": "base64-encoded-image-content"
|
||||
}
|
||||
],
|
||||
"flags": { // Optional flags for internal tracking
|
||||
"read": false,
|
||||
"spam": false
|
||||
},
|
||||
"received_timestamp": ISODate("2024-11-26T12:01:00Z") // Time the email was received (optional)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"messages": {
|
||||
"193669615aa33694": {
|
||||
"threadId": "193669615aa33694"
|
||||
},
|
||||
"19365d0239aa1aaf": {
|
||||
"threadId": "19365d0239aa1aaf"
|
||||
},
|
||||
"193641e1379da64d": {
|
||||
"threadId": "193641e1379da64d"
|
||||
},
|
||||
"193640e74d93dc9b": {
|
||||
"threadId": "193640e74d93dc9b"
|
||||
},
|
||||
"19363d6a7f50225a": {
|
||||
"threadId": "19363d6a7f50225a"
|
||||
},
|
||||
"19363ba8e2582133": {
|
||||
"threadId": "19363ba8e2582133"
|
||||
},
|
||||
"19362f9983e58ea6": {
|
||||
"threadId": "19362f9983e58ea6"
|
||||
},
|
||||
"19360aabcb976cfc": {
|
||||
"threadId": "19360aabcb976cfc"
|
||||
},
|
||||
"193604ad97c9fe62": {
|
||||
"threadId": "193604ad97c9fe62"
|
||||
},
|
||||
"1935e92672e67f22": {
|
||||
"threadId": "1935e92672e67f22"
|
||||
}
|
||||
},
|
||||
"nextPageToken": "11954226706764006151",
|
||||
"resultSizeEstimate": 402
|
||||
}
|
||||
+148
File diff suppressed because one or more lines are too long
+258
@@ -0,0 +1,258 @@
|
||||
{
|
||||
"id": "19367930033154ca",
|
||||
"threadId": "19367930033154ca",
|
||||
"labelIds": [
|
||||
"UNREAD",
|
||||
"IMPORTANT",
|
||||
"CATEGORY_PERSONAL",
|
||||
"INBOX"
|
||||
],
|
||||
"snippet": "Sample content. Inline graphics done. Sample monospaced How about some Headers? Hello, World! These are quotes. How about some emojis? \ud83d\ude1a\ud83d\ude05\ud83d\ude05\ud83e\udd70\ud83d\ude18\ud83d\ude01\ud83d\ude01 There's also an attachment!",
|
||||
"payload": {
|
||||
"partId": "",
|
||||
"mimeType": "multipart/mixed",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Delivered-To",
|
||||
"value": "pskhushal@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Received",
|
||||
"value": "by 2002:a5d:4205:0:b0:382:44a8:2102 with SMTP id n5csp1327351wrq; Tue, 26 Nov 2024 00:25:08 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "X-Received",
|
||||
"value": "by 2002:a05:690c:6111:b0:6e3:fd6:6ccb with SMTP id 00721157ae682-6eee08c3459mr156525787b3.13.1732609507838; Tue, 26 Nov 2024 00:25:07 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "ARC-Seal",
|
||||
"value": "i=1; a=rsa-sha256; t=1732609507; cv=none; d=google.com; s=arc-20240605; b=VDostCh8Kr1QGeQoXgLjYyrkymtwKEVHTyPaNeSjy30Q0B4GTTIBIPbKgkL6UazG4T s92UnE+Br4HJUSsqbtOdKTV1Pwt9lI3EzASDMX3OVGccdViTyUgGjgNuagV2fUVQugYi BJWxLrWZXVVtKkLNqRDOEQgiPi6t869FTB4877V8dKxQs99Q8IBncekOq3f6+SmsFlIl U3l6EUgaV/7MFzNRofsmXFGEG0TLto+Xgx5bMQ8B0OpjSFXR2Va/1xF5BmrzJTLgL6N5 zct3VIRLMxkf+8v3oniTG7yaaCQMPLxOq1pKXhP/MfKzgvdo5r14/kAzsj05rQ/L2/rT 2OrQ=="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Message-Signature",
|
||||
"value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=cc:to:subject:message-id:date:from:mime-version:dkim-signature; bh=9NSLykdU3XzuxItzVTTQJjRRBR6yhJqdgYRarHa+VdY=; fh=KtxgdfxhT3MZUXDfdB/Mxp08o9TbOvoJMvz1V2wYTKk=; b=Xqtl2jfteUY2I+wdSwdllaqPKg1QP8hUyJpP/644zwGrOBdLbjCO8Po5C9KJfxUh7i H7UMWfEWkulB52WFTDU/54UY8lnX7iGZEMtT/4twKUGToWPPWeG/wmh/I4W9ZEmA/qXG OiRrBlJI3NJowsrnfoqXrpV/W5mz1SNuJBkj5k71+3vrPVlEmGwGmz8/MCG+RF2yV4nz QEVg613QkuThPEEsgYdHbfWDlF8jnGbAS+Lyt8ZdixVvO4/Gq5swuGUHihfeV+Fxt5Kv L2Wbqiq7UFJF9YJEtezJOdkdrTxs8dz6bTUZTrpWtRYxoBuH76HB+f9yrhOIRFTxor+9 18dA==; dara=google.com"
|
||||
},
|
||||
{
|
||||
"name": "ARC-Authentication-Results",
|
||||
"value": "i=1; mx.google.com; dkim=pass header.i=@gmail.com header.s=20230601 header.b=\"Adx/Ch2H\"; spf=pass (google.com: domain of orangebhopli@gmail.com designates 209.85.220.41 as permitted sender) smtp.mailfrom=orangebhopli@gmail.com; dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com; dara=pass header.i=@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Return-Path",
|
||||
"value": "<orangebhopli@gmail.com>"
|
||||
},
|
||||
{
|
||||
"name": "Received",
|
||||
"value": "from mail-sor-f41.google.com (mail-sor-f41.google.com. [209.85.220.41]) by mx.google.com with SMTPS id 00721157ae682-6eee01062easor78223267b3.12.2024.11.26.00.25.07 (Google Transport Security); Tue, 26 Nov 2024 00:25:07 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "Received-SPF",
|
||||
"value": "pass (google.com: domain of orangebhopli@gmail.com designates 209.85.220.41 as permitted sender) client-ip=209.85.220.41;"
|
||||
},
|
||||
{
|
||||
"name": "Authentication-Results",
|
||||
"value": "mx.google.com; dkim=pass header.i=@gmail.com header.s=20230601 header.b=\"Adx/Ch2H\"; spf=pass (google.com: domain of orangebhopli@gmail.com designates 209.85.220.41 as permitted sender) smtp.mailfrom=orangebhopli@gmail.com; dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com; dara=pass header.i=@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "DKIM-Signature",
|
||||
"value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1732609507; x=1733214307; dara=google.com; h=cc:to:subject:message-id:date:from:mime-version:from:to:cc:subject :date:message-id:reply-to; bh=9NSLykdU3XzuxItzVTTQJjRRBR6yhJqdgYRarHa+VdY=; b=Adx/Ch2H1Vw5fzCEcIPf/EuHGviFMr1/Gw93WwruoabampXxraQ5hkcJNx0yi+zM2/ 9wlmVqcDwiWM1dFrkc1hPNMyyH4h7hCXjGsDbK8hkUE/Fk1AR51fhjliB9AZFkps+PUR 21CYTDMzWWlHjKlBpnM1axU9suvv04NqDZ8Hr7LAXysd9eF1ypoTWjwWMJ5QGKpoQo6W qf3m5yuqSXLAtbTKZN6K3qUO+S9ZRjUahw0eke7ieQ2uKR8dFwaoS899KhVBY5bZpLzr gEjYkNbzZGN4bp/LIJcHmgueZ5J0L6V+zaYMIqNRp3wOwAe41nZSAf6PvLxixLZoTvyH TmmA=="
|
||||
},
|
||||
{
|
||||
"name": "X-Google-DKIM-Signature",
|
||||
"value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1732609507; x=1733214307; h=cc:to:subject:message-id:date:from:mime-version:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=9NSLykdU3XzuxItzVTTQJjRRBR6yhJqdgYRarHa+VdY=; b=F5yE7IslZpO2UHCSg9jCYE645+q7dW9WO9S0DIb4oRcTlz/qgI1fwnnB0vlWvgXQxJ Af8/WUW6AJm/8mJ//SNXSDwE28OZiZqR+i/SYc0yontGepFawQAZg9xFRLspp7O/2UAL EHrkssz9noe2wPPdcPGYanXfoiSLtX/3XnrGCg1P5Y4jdy7p6624f87Xx+Mno1t8q2xh Odh2QrURKad50BqY+UB20+um9ommh4J0BOsD9WiVCfu5isvTCfwZYAtjYUJ48UVWLh3b kAPAaSic4g1zQzlOychUnqFB95VrrODUNUACfKBuIR0YbUdsgjMzn9p4RQ1LAgr9NfvY 62kw=="
|
||||
},
|
||||
{
|
||||
"name": "X-Forwarded-Encrypted",
|
||||
"value": "i=1; AJvYcCVe+1Kjt1hhMEjYG2UyrzV/DvH4xn376Ya7lsLKkTIVIAJGmG8xKSkvvJAmXIAXRyYuWSNpPVXGyjSKXTgsLlU=@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "X-Gm-Message-State",
|
||||
"value": "AOJu0Ywb3oN+4RLkTx99fCaTMOlGwSTJR7AWBy7IvxJyBYNwLV6Of7c2 vtYqtaRl1CcUgUXmGnXqjfQsSX6DT4LLNPGCsrrzk4VXfV26FJA1iaDFm/ey7pRaAucm5dGPHRO 9oG7H+96wM0pzmY3Tlb9LUEY3NIdSR9H2"
|
||||
},
|
||||
{
|
||||
"name": "X-Gm-Gg",
|
||||
"value": "ASbGncuwgQwpQZrb7exEtts26PEAwkklAu9DD/4RvvBSiHYGDUD7+909oeyrMtfk66q IuA94Z8WcFFe3TabyhyXIjGTCEF5+Qg=="
|
||||
},
|
||||
{
|
||||
"name": "X-Google-Smtp-Source",
|
||||
"value": "AGHT+IFy7wXSaJCDrrXclKRzxMFtyv1HrgyWSfgyomoDnvBS4NPu9KBdzHjvcekKjw9uKzJAj5R2iOl+mZuCt5sjIws="
|
||||
},
|
||||
{
|
||||
"name": "X-Received",
|
||||
"value": "by 2002:a05:690c:6701:b0:6ee:b5a6:a67a with SMTP id 00721157ae682-6eee0a402acmr176495297b3.28.1732609506221; Tue, 26 Nov 2024 00:25:06 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "MIME-Version",
|
||||
"value": "1.0"
|
||||
},
|
||||
{
|
||||
"name": "From",
|
||||
"value": "TheBhopli <orangebhopli@gmail.com>"
|
||||
},
|
||||
{
|
||||
"name": "Date",
|
||||
"value": "Tue, 26 Nov 2024 13:54:54 +0530"
|
||||
},
|
||||
{
|
||||
"name": "Message-ID",
|
||||
"value": "<CAA-t6761d=GwN3to+252S34rA=gjv+Ao+u5WVbz+Gj+5oiwb1g@mail.gmail.com>"
|
||||
},
|
||||
{
|
||||
"name": "Subject",
|
||||
"value": "Test Mail for GMail API."
|
||||
},
|
||||
{
|
||||
"name": "To",
|
||||
"value": "\"pskhushal@gmail.com\" <pskhushal@gmail.com>"
|
||||
},
|
||||
{
|
||||
"name": "Cc",
|
||||
"value": "\"khushal@easyfi.net.in\" <khushal@easyfi.net.in>, \"bhushan.thakkar@gmail.com\" <bhushan.thakkar@gmail.com>"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/mixed; boundary=\"00000000000027d46c0627cc96ea\""
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"size": 0
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"partId": "0",
|
||||
"mimeType": "multipart/related",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/related; boundary=\"00000000000027d46c0627cc96e9\""
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"size": 0
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"partId": "0.0",
|
||||
"mimeType": "multipart/alternative",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/alternative; boundary=\"00000000000027d46c0627cc96e8\""
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"size": 0
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"partId": "0.0.0",
|
||||
"mimeType": "text/plain",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/plain; charset=\"UTF-8\""
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "quoted-printable"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"size": 236,
|
||||
"data": "U2FtcGxlIGNvbnRlbnQuDQoNCltpbWFnZTogY2hhdC5wbmddDQoNCklubGluZSBncmFwaGljcyBkb25lLg0KDQpTYW1wbGUgbW9ub3NwYWNlZA0KDQoqSG93IGFib3V0IHNvbWUgSGVhZGVycz8qDQoNCkhlbGxvLCBXb3JsZCEgVGhlc2UgYXJlIHF1b3Rlcy4NCg0KDQpIb3cgYWJvdXQgc29tZSBlbW9qaXM_IPCfmJrwn5iF8J-YhfCfpbDwn5iY8J-YgfCfmIENClRoZXJlJ3MgYWxzbyBhbiAqYXR0YWNobWVudCohDQo="
|
||||
}
|
||||
},
|
||||
{
|
||||
"partId": "0.0.1",
|
||||
"mimeType": "text/html",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/html; charset=\"UTF-8\""
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "quoted-printable"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"size": 675,
|
||||
"data": "PGRpdiBkaXI9Imx0ciI-U2FtcGxlIGNvbnRlbnQuPGRpdj48YnI-PGRpdj48aW1nIHNyYz0iY2lkOmlpX20zeTZ0dTg1MCIgYWx0PSJjaGF0LnBuZyIgd2lkdGg9IjIyMiIgaGVpZ2h0PSIyMjIiIHN0eWxlPSJtYXJnaW4tcmlnaHQ6IDBweDsiPjwvZGl2PjxkaXY-PGJyPjwvZGl2PjxkaXY-SW5saW5lIGdyYXBoaWNzIGRvbmUuPC9kaXY-PGRpdj48YnI-PC9kaXY-PGRpdj48Zm9udCBmYWNlPSJtb25vc3BhY2UiPlNhbXBsZSBtb25vc3BhY2VkPC9mb250PjwvZGl2PjxkaXY-PGJyPjwvZGl2PjxkaXY-PGI-PGZvbnQgc2l6ZT0iNiI-SG93IGFib3V0IHNvbWUgSGVhZGVycz88L2ZvbnQ-PC9iPjwvZGl2PjxkaXY-PGJyPjwvZGl2PjxibG9ja3F1b3RlIGNsYXNzPSJnbWFpbF9xdW90ZSIgc3R5bGU9Im1hcmdpbjowcHggMHB4IDBweCAwLjhleDtib3JkZXItbGVmdDoxcHggc29saWQgcmdiKDIwNCwyMDQsMjA0KTtwYWRkaW5nLWxlZnQ6MWV4Ij5IZWxsbywgV29ybGQhIFRoZXNlIGFyZSBxdW90ZXMuPC9ibG9ja3F1b3RlPjxkaXY-PGJyPjwvZGl2PjxkaXY-SG93IGFib3V0IHNvbWUgZW1vamlzP8Kg8J-YmvCfmIXwn5iF8J-lsPCfmJjwn5iB8J-YgTxicj48L2Rpdj48L2Rpdj48ZGl2PlRoZXJlJiMzOTtzIGFsc28gYW4gPGk-PHU-YXR0YWNobWVudDwvdT48L2k-ITwvZGl2PjwvZGl2Pg0K"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"partId": "0.1",
|
||||
"mimeType": "image/png",
|
||||
"filename": "chat.png",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "image/png; name=\"chat.png\""
|
||||
},
|
||||
{
|
||||
"name": "Content-Disposition",
|
||||
"value": "inline; filename=\"chat.png\""
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "base64"
|
||||
},
|
||||
{
|
||||
"name": "Content-ID",
|
||||
"value": "<ii_m3y6tu850>"
|
||||
},
|
||||
{
|
||||
"name": "X-Attachment-Id",
|
||||
"value": "ii_m3y6tu850"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"attachmentId": "ANGjdJ88SNHhnjq5-Alk71WYYaL72CUhJn6_geE_GP7JZKZdcSIR56LPGVklvFO_IuMFqeDkDT2U4r8SXTSMb-uBnNrio-NeXiW2p8vPWnV2yq-YaZxG0xBD6WILt5Qwg_43lv9_TGnjSvWn5QrGzusOttPluVRIWtv0-dksvYXrHDbS5MC0IYX4qpv8dbvGto5rgH_iIsIMav2aXbm5_o_mmxlxO9GavC8G9ZWD9BbuLlKKMJQAXgGXlfOlWA1Ukr5Cr0UO5LZbeTtVX3dZMYs3QfMwlGkVLT7Gam6aZfW8JduWhObFP9ghg2haF4tooXcUFNOPX7e2Axto-OmYAXLaPZRh-xpSzEObOeQv8gAHihoXlkA5Y_A2m45-emjdSiwff2Ui0VRZs96_DMGO",
|
||||
"size": 17466
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"partId": "1",
|
||||
"mimeType": "image/png",
|
||||
"filename": "chat.png",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "image/png; name=\"chat.png\""
|
||||
},
|
||||
{
|
||||
"name": "Content-Disposition",
|
||||
"value": "attachment; filename=\"chat.png\""
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "base64"
|
||||
},
|
||||
{
|
||||
"name": "Content-ID",
|
||||
"value": "<f_m3y6xx8e1>"
|
||||
},
|
||||
{
|
||||
"name": "X-Attachment-Id",
|
||||
"value": "f_m3y6xx8e1"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"attachmentId": "ANGjdJ-XMpv18VYpIKT1zc45Su99KXSI3r3YaVquSLfKE3DqgQ7XjO7p4iHRHvc47lIr70rLBk2H8lLlMifTCFxFLMy0q_uFgsCHASCDeq-opjPkeDxC3BaxACeyeNWaZYANXgUCFYqCygTLkIoeggJDBxmzPn99baxvoJqcRLXROO1mSWGB4K0PLJcP35r8Y2D9lVRjSAJupzf7HLYLzimY1Q-X6Ge2qNCNnree-rS47FNXQgvFfVdV_x7jcvLE7JJl5cBBIyus-TtTulMk0RrFbcn5zKH7AniW6IF7v-mLOUzpDVye9tIiVqc6W4EdLeP3wmIaMVy4shsAdHkKtbjwLxicycke8Vi5hAV50s0pCllN26rSHakAA9zswG9q5z3cbS5LNMdEUpgWt6ON",
|
||||
"size": 17466
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"sizeEstimate": 55270,
|
||||
"historyId": "528587",
|
||||
"internalDate": "1732609494000"
|
||||
}
|
||||
+175
File diff suppressed because one or more lines are too long
+179
File diff suppressed because one or more lines are too long
+183
File diff suppressed because one or more lines are too long
+183
File diff suppressed because one or more lines are too long
+210
@@ -0,0 +1,210 @@
|
||||
{
|
||||
"id": "19362f9983e58ea6",
|
||||
"threadId": "19362f9983e58ea6",
|
||||
"labelIds": [
|
||||
"CATEGORY_PROMOTIONS",
|
||||
"UNREAD",
|
||||
"INBOX"
|
||||
],
|
||||
"snippet": "INDIAN FILM & TELEVISION DIRECTORS' ASSOCIATION G- 8/9/10, Crescent Towers, Near Morya House, Andheri (W), Mumbai - 400 053 Tel: 022 46088096 Mob: 9892885346/ 7021494476 www.directorsiftda.com",
|
||||
"payload": {
|
||||
"partId": "",
|
||||
"mimeType": "multipart/related",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Delivered-To",
|
||||
"value": "pskhushal@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Received",
|
||||
"value": "by 2002:a5d:4205:0:b0:382:44a8:2102 with SMTP id n5csp832066wrq; Mon, 25 Nov 2024 02:59:05 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "X-Forwarded-Encrypted",
|
||||
"value": "i=2; AJvYcCWTJ16JRatSfV54WvhqvY7z3mW9LUmKJsBYYVJp5gas68X0OXWAugWTJyyEQE0ESerQAquXl7ZIugA=@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "X-Received",
|
||||
"value": "by 2002:a17:906:3188:b0:a99:7bc0:bca9 with SMTP id a640c23a62f3a-aa50990b300mr1091084466b.3.1732532345051; Mon, 25 Nov 2024 02:59:05 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "ARC-Seal",
|
||||
"value": "i=1; a=rsa-sha256; t=1732532345; cv=none; d=google.com; s=arc-20240605; b=cGPTlt9iJZomIZrEX1YxFZt7CeiQG3g/9XJ/DHe6g6ghGuBopYyGtWRIq/b0u5ezsK 3JyasTQwmCnqAmvhDEgUZQM/f2Bk8PshykVmLfSUzhP0vUS2wfg90N1jmaMTefDGFvlF lSjmHBlgpl4Vhn4BKGBnLuN3ttrMXvyZ6Vi4xGil9yDgW+KjwrL3fN2jdlFsOvhxODZR EE7q5SC0waIenSAllTd+0CcG4TrH7Q0ZrQYR0yVH98JZ4UJN6v5xqhwBmTMtlPx3mWw2 d8H5HXwevTXeYzYf/F7iKAlaaWu2IPM7k/e3HAatqQk/pKHfh6cmYqN1G/MzENERvOsk HmUQ=="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Message-Signature",
|
||||
"value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=to:subject:message-id:date:from:mime-version:dkim-signature; bh=52sadrERSC8TqD31OwCOGWYBr43KUD3rPyq2rw8jz2Y=; fh=3tsGDR7RWsLBUvHMNbctop2/zoZmFmKl/B1BkNJ2jsE=; b=N9JHVDUsSOQO9HPax1y++VDKEbumii3FfXCPi2ec+NDD0lV4w+48dqXsrPO9k8XO03 FgGp+qnSgZps8TogBykk8a/bFPO4VJ0AZgt5b7v4i08Y+auiO+ywcP37yeZjB52iGVSU U0TcBLTtqQSBtlirHN4lG5t4CvNTgDhKDwr9UjvOqtLgValnrsySlSpoQQ+CRhaxC3Pq bEVn4CKh8dGlJWVvOA037UDZFBQtR6ygY7IbiXBEIFZsC49aH4lZbpyzA+Xh3H3X0Xtg LrM/0hxRNj33PLOvbm5gdS7F32djBbaoIXRl057QZV0JQbPa1fvEeDOwJCv8H6rECfHK m0UQ==; dara=google.com"
|
||||
},
|
||||
{
|
||||
"name": "ARC-Authentication-Results",
|
||||
"value": "i=1; mx.google.com; dkim=pass header.i=@directorsiftda-com.20230601.gappssmtp.com header.s=20230601 header.b=h3FDhhUH; spf=fail (google.com: domain of contact@directorsiftda.com does not designate 209.85.220.41 as permitted sender) smtp.mailfrom=contact@directorsiftda.com; dara=pass header.i=@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Return-Path",
|
||||
"value": "<contact@directorsiftda.com>"
|
||||
},
|
||||
{
|
||||
"name": "Received",
|
||||
"value": "from mail-sor-f41.google.com (mail-sor-f41.google.com. [209.85.220.41]) by mx.google.com with SMTPS id a640c23a62f3a-aa560bdd42csor237566b.0.2024.11.25.02.59.04 for <pskhushal@gmail.com> (Google Transport Security); Mon, 25 Nov 2024 02:59:04 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "Received-SPF",
|
||||
"value": "fail (google.com: domain of contact@directorsiftda.com does not designate 209.85.220.41 as permitted sender) client-ip=209.85.220.41;"
|
||||
},
|
||||
{
|
||||
"name": "Authentication-Results",
|
||||
"value": "mx.google.com; dkim=pass header.i=@directorsiftda-com.20230601.gappssmtp.com header.s=20230601 header.b=h3FDhhUH; spf=fail (google.com: domain of contact@directorsiftda.com does not designate 209.85.220.41 as permitted sender) smtp.mailfrom=contact@directorsiftda.com; dara=pass header.i=@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "DKIM-Signature",
|
||||
"value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=directorsiftda-com.20230601.gappssmtp.com; s=20230601; t=1732532344; x=1733137144; dara=google.com; h=to:subject:message-id:date:from:mime-version:from:to:cc:subject :date:message-id:reply-to; bh=52sadrERSC8TqD31OwCOGWYBr43KUD3rPyq2rw8jz2Y=; b=h3FDhhUHelnzSe5+as+rP+HnwiQlFmbqO/XDImaXOHroIQfkSRt1MAwH2o8MiNS07l fzAKjxqRmO+p+fy9FyRuQ7d0/TDALivdP4HW1CpU7nHUqqp8BamVDC06zS1RI2PCVo7s qp4cxTIJsFTVaMauaTWQCIj9xstz9lGMa7DuhyJ8deSJU6zIAn0rFOhLISFZfZkVtZEy Q4J4MBtf2DZVvaxqUNr/DqkaownkPsTWaxfXTE8Ztq41a9kb+2IfX+6kk6M80DF4pm8I 5Wju6mVd6xpms0UXJPtRPfRBFKEjEApYq6dypd37ETr+tkUynPrv10xfuSUvrZdzU73L 1uEQ=="
|
||||
},
|
||||
{
|
||||
"name": "X-Google-DKIM-Signature",
|
||||
"value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1732532344; x=1733137144; h=to:subject:message-id:date:from:mime-version:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=52sadrERSC8TqD31OwCOGWYBr43KUD3rPyq2rw8jz2Y=; b=tQU+3IcgiGI90Bknb75WOMN2IpL9BIunrIP5KwfkJMvrRTWq9GzjUl0t6gUocRNiPn 1fJ2VgBd/nAvzUpg/2lcypTBbfp7019CxuW5aqungvCD5iWFzMUcOoudN0O4kMx0KrMX DFThj9pD951evE/jteODFBQt0886+8Ofo8KIWaCsSiJ7uerf968pWkkJm6lkHsMCFf97 wgiA4kN8X8i9d+ZFs2yo5MxZykY1Ll2YYmMozgIS6jsXLb0pveLtA1L0SFxbqRU+/Njf n6ibGo42xOWOM92qDEbJnDXNtlnOEVOAKA3sgazkG8pDpC9yf2RqMPWex9xCNAbbVNQE KTog=="
|
||||
},
|
||||
{
|
||||
"name": "X-Forwarded-Encrypted",
|
||||
"value": "i=1; AJvYcCWtfq6Mi9XsVN+r9oOb64yoh/F6ROqg/9C0UMWDuXU/fz1A74BwcwwZQy0hyIykg2dwtgxUFBLIz0w=@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "X-Gm-Message-State",
|
||||
"value": "AOJu0YxUy7pf/My+et+YkWa4bqQ26C2PeklIXRKdME1VkRVdK9A8dtmN p34rPukc6VEwoEvOLsIh42JHO+nLEOYgcdIMTfklpdIp+2SaamAG+j/zSQFpfdFhWov4PO2vbqx aOztICe4xCfaWEiv4jKQ3arZkHmUWwrT6xb1sOQ=="
|
||||
},
|
||||
{
|
||||
"name": "X-Gm-Gg",
|
||||
"value": "ASbGncsXwRmXGJJAyTvAKY+pMS2y7/Mf8mmUI9AmHaycDBvZnbQkc20j125cXkjxmOG o3M/KYTcTZDKyHLOi5xRqMfOv1Qs8lwFelQ=="
|
||||
},
|
||||
{
|
||||
"name": "X-Google-Smtp-Source",
|
||||
"value": "AGHT+IFopa4irNtf4tkqOS0N6Tc13BHGQQ6fNDsmqoW9klWPS/JD7cYNG9eMDQkP9k7DPZCzWaTq4bPYSLxo6IJY2OU="
|
||||
},
|
||||
{
|
||||
"name": "X-Received",
|
||||
"value": "by 2002:a17:906:1daa:b0:aa5:3853:5532 with SMTP id a640c23a62f3a-aa53853589dmr630025266b.43.1732532343546; Mon, 25 Nov 2024 02:59:03 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "MIME-Version",
|
||||
"value": "1.0"
|
||||
},
|
||||
{
|
||||
"name": "From",
|
||||
"value": "Iftda India <contact@directorsiftda.com>"
|
||||
},
|
||||
{
|
||||
"name": "Date",
|
||||
"value": "Mon, 25 Nov 2024 16:28:50 +0530"
|
||||
},
|
||||
{
|
||||
"name": "Message-ID",
|
||||
"value": "<CAAPQQGQPeA-z38EPhBSyMPmhyuWzc2uRyibVMg6uLd5V8hr5VQ@mail.gmail.com>"
|
||||
},
|
||||
{
|
||||
"name": "Subject",
|
||||
"value": "Sad Demise of Mr. Jalaj Dhir"
|
||||
},
|
||||
{
|
||||
"name": "To",
|
||||
"value": "undisclosed-recipients:;"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/related; boundary=\"000000000000e67bea0627ba9e63\""
|
||||
},
|
||||
{
|
||||
"name": "Bcc",
|
||||
"value": "pskhushal@gmail.com"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"size": 0
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"partId": "0",
|
||||
"mimeType": "multipart/alternative",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/alternative; boundary=\"000000000000e67be90627ba9e62\""
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"size": 0
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"partId": "0.0",
|
||||
"mimeType": "text/plain",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/plain; charset=\"UTF-8\""
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"size": 672,
|
||||
"data": "W2ltYWdlOiBjb25kb2xlbmNlcyBNci4gSkFMQUogREhJUi5qcGddDQoNCg0KKklORElBTioqIEZJTE0gJiBURUxFVklTSU9OIERJUkVDVE9SUycgQVNTT0NJQVRJT04qDQpHLSA4LzkvMTAsIENyZXNjZW50IFRvd2VycywNCk5lYXIgTW9yeWEgSG91c2UsDQpBbmRoZXJpIChXKSwgTXVtYmFpIC0gNDAwIDA1Mw0KVGVsOiAwMjIgNDYwODgwOTYNCk1vYjogOTg5Mjg4NTM0Ni8gNzAyMTQ5NDQ3Ng0Kd3d3LmRpcmVjdG9yc2lmdGRhLmNvbSB8IEZhY2Vib29rL2RpcmVjdG9yc2lmdGRhDQoNCipESVNDTEFJTUVSOiogVGhpcyBlLW1haWwgbWF5IGJlIHByaXZpbGVnZWQgYW5kL29yIGNvbmZpZGVudGlhbCwgYW5kIHRoZQ0Kc2VuZGVyIGRvZXMgbm90IHdhaXZlIGFueSByZWxhdGVkIHJpZ2h0cyBhbmQgb2JsaWdhdGlvbnMuIEFueSBkaXN0cmlidXRpb24sDQp1c2Ugb3IgY29weWluZyBvZiB0aGlzIGUtbWFpbCBvciB0aGUgaW5mb3JtYXRpb24gaXQgY29udGFpbnMgYnkgb3RoZXIgdGhhbg0KYW4gaW50ZW5kZWQgcmVjaXBpZW50KHMpIGlzIHVuYXV0aG9yaXplZC4gSWYgeW91IHJlY2VpdmVkIHRoaXMgZS1tYWlsIGluDQplcnJvciwgcGxlYXNlIGFkdmlzZSB1cyAoYnkgcmV0dXJuIGUtbWFpbCBvciBvdGhlcndpc2UpIGltbWVkaWF0ZWx5IGFuZA0KZGVsZXRlIHRoaXMgZS1tYWlsLg0K"
|
||||
}
|
||||
},
|
||||
{
|
||||
"partId": "0.1",
|
||||
"mimeType": "text/html",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/html; charset=\"UTF-8\""
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "quoted-printable"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"size": 2187,
|
||||
"data": "PGRpdiBkaXI9Imx0ciI-PGRpdj48ZGl2IGNsYXNzPSJnbWFpbF9kZWZhdWx0IiBzdHlsZT0iZm9udC1mYW1pbHk6JnF1b3Q7dHJlYnVjaGV0IG1zJnF1b3Q7LHNhbnMtc2VyaWY7Y29sb3I6cmdiKDY4LDY4LDY4KSI-PC9kaXY-PGltZyBzcmM9ImNpZDppaV9tM3d4MGo5YzAiIGFsdD0iY29uZG9sZW5jZXMgTXIuIEpBTEFKIERISVIuanBnIiB3aWR0aD0iNDcyIiBoZWlnaHQ9IjMzNCI-PGJyPjxiciBjbGVhcj0iYWxsIj48L2Rpdj48ZGl2PjxkaXYgZGlyPSJsdHIiIGNsYXNzPSJnbWFpbF9zaWduYXR1cmUiIGRhdGEtc21hcnRtYWlsPSJnbWFpbF9zaWduYXR1cmUiPjxkaXYgZGlyPSJsdHIiPjxkaXY-PGRpdiBkaXI9Imx0ciI-PGRpdj48ZGl2IGRpcj0ibHRyIj48ZGl2IGRpcj0ibHRyIj48ZGl2IGRpcj0ibHRyIj48ZGl2IGRpcj0ibHRyIj48ZGl2IGRpcj0ibHRyIj48ZGl2IGRpcj0ibHRyIj48ZGl2IGRpcj0ibHRyIj48ZGl2IGRpcj0ibHRyIj48ZGl2PjxzcGFuPjxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoNjgsNjgsNjgpIj48c3Bhbj48Yj48Zm9udCBzaXplPSI0Ij48YnI-PC9mb250PjwvYj48L3NwYW4-PC9zcGFuPjwvc3Bhbj48ZGl2Pjxmb250IGZhY2U9InRhaG9tYSwgc2Fucy1zZXJpZiI-PGZvbnQ-PGI-SU5ESUFOPC9iPjwvZm9udD48Yj7CoEZJTE0gJmFtcDsgVEVMRVZJU0lPTiBESVJFQ1RPUlMmIzM5OyBBU1NPQ0lBVElPTjwvYj48L2ZvbnQ-PC9kaXY-PGRpdj48Zm9udCBmYWNlPSJ0YWhvbWEsIHNhbnMtc2VyaWYiPkctIDgvOS8xMCwgQ3Jlc2NlbnQgVG93ZXJzLMKgPGJyPjwvZm9udD48L2Rpdj48ZGl2IGRpcj0ibHRyIj48ZGl2Pjxmb250IGZhY2U9InRhaG9tYSwgc2Fucy1zZXJpZiI-TmVhciBNb3J5YSBIb3VzZSw8L2ZvbnQ-PC9kaXY-PGRpdj48Zm9udCBmYWNlPSJ0YWhvbWEsIHNhbnMtc2VyaWYiPkFuZGhlcmkgKFcpLCBNdW1iYWkgLSA0MDAgMDUzPC9mb250PjwvZGl2PjxkaXY-PHNwYW4gc3R5bGU9InRleHQtYWxpZ246Y2VudGVyIj5UZWw6wqA8L3NwYW4-PHNwYW4gc3R5bGU9InRleHQtYWxpZ246Y2VudGVyIj4wMjIgNDYwODgwOTY8L3NwYW4-PGZvbnQgZmFjZT0idGFob21hLCBzYW5zLXNlcmlmIj48YnI-PC9mb250PjwvZGl2PjxkaXY-PGZvbnQgZmFjZT0idGFob21hLCBzYW5zLXNlcmlmIj5Nb2I6IDxzcGFuIHN0eWxlPSJ0ZXh0LWFsaWduOmNlbnRlciI-OTg5Mjg4NTM0Ni8gNzAyMTQ5NDQ3Njwvc3Bhbj48L2ZvbnQ-PC9kaXY-PC9kaXY-PC9kaXY-PGRpdj48ZGl2IGRpcj0ibHRyIj48ZGl2PjxzcGFuIHN0eWxlPSJjb2xvcjpyZ2IoNjgsNjgsNjgpIj48Zm9udCBmYWNlPSJ0YWhvbWEsIHNhbnMtc2VyaWYiPjxmb250IHNpemU9IjIiPnd3dy5kaXJlY3RvcnNpZnRkYTwvZm9udD4uY29tIHwgRmFjZWJvb2svZGlyZWN0b3JzaWZ0ZGE8L2ZvbnQ-PGJyPjxicj48c3BhbiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI-PGI-PGk-PHU-PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZTo4cHQiIGxhbmc9IkVOLVVTIj5ESVNDTEFJTUVSOjwvc3Bhbj48L3U-PC9pPjwvYj48c3BhbiBzdHlsZT0iZm9udC1zaXplOjhwdCI-PC9zcGFuPjxzcGFuIHN0eWxlPSJmb250LXNpemU6OHB0O2JhY2tncm91bmQ6d2hpdGUgbm9uZSByZXBlYXQgc2Nyb2xsIDAlIDAlIj4gVGhpcw0KIGUtbWFpbCBtYXkgYmUgcHJpdmlsZWdlZCBhbmQvb3IgY29uZmlkZW50aWFsLCBhbmQgdGhlIHNlbmRlciBkb2VzIG5vdCANCndhaXZlIGFueSByZWxhdGVkIHJpZ2h0cyBhbmQgb2JsaWdhdGlvbnMuIEFueSBkaXN0cmlidXRpb24sIHVzZSBvciANCmNvcHlpbmcgb2YgdGhpcyBlLW1haWwgb3IgdGhlIGluZm9ybWF0aW9uIGl0IGNvbnRhaW5zIGJ5IG90aGVyIHRoYW4gYW4gDQppbnRlbmRlZCByZWNpcGllbnQocykgaXMgdW5hdXRob3JpemVkLiBJZiB5b3UgcmVjZWl2ZWQgdGhpcyBlLW1haWwgaW4gDQplcnJvciwgcGxlYXNlIGFkdmlzZSB1cyAoYnkgcmV0dXJuIGUtbWFpbCBvciBvdGhlcndpc2UpIGltbWVkaWF0ZWx5IGFuZCANCmRlbGV0ZSB0aGlzIGUtbWFpbC48L3NwYW4-PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZTo4cHQiPjwvc3Bhbj48L3NwYW4-PC9zcGFuPjwvZGl2PjwvZGl2PjwvZGl2PjwvZGl2PjwvZGl2PjwvZGl2PjwvZGl2PjwvZGl2PjwvZGl2PjwvZGl2PjwvZGl2PjwvZGl2PjwvZGl2PjwvZGl2PjwvZGl2PjwvZGl2PjwvZGl2PjwvZGl2Pg0K"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"partId": "1",
|
||||
"mimeType": "image/jpeg",
|
||||
"filename": "condolences Mr. JALAJ DHIR.jpg",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "image/jpeg; name=\"condolences Mr. JALAJ DHIR.jpg\""
|
||||
},
|
||||
{
|
||||
"name": "Content-Disposition",
|
||||
"value": "inline; filename=\"condolences Mr. JALAJ DHIR.jpg\""
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "base64"
|
||||
},
|
||||
{
|
||||
"name": "Content-ID",
|
||||
"value": "<ii_m3wx0j9c0>"
|
||||
},
|
||||
{
|
||||
"name": "X-Attachment-Id",
|
||||
"value": "ii_m3wx0j9c0"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"attachmentId": "ANGjdJ8zCv8y-pC8ecFP7IVoYsW4sTkayvcSepjUwiCDIrSUTJPBua7Ack8YvSuFPYcM-np_PeWRTl_wq5dYpyz3k6Hc5e-tp4RZjT6t2ITBDpkqJhPn17eR36sVN41ojkJ63Wzd9HD60LDk3o4GiL8C6iyMlsHMhadvOXKYK5zITlU9mvtdMoye6fofiiLwRsoNuT9dEpBbk9r2gh-vmuYpbu0waybh_EKefEejDvuOl7cIFU1Y3Dv8kxWYQwtZ1kx1LhHF24TzisZjvBaF6cno1T6f9aTdNd3x5T0_YsgJNilfU3xdWbCYilbVACU8axpIN5mC9CwhD45ogliA9kLadSpig8KmW9MNJJxr1vMnOAI4G-zr1SAhkzm6ZXcHllk9WpncS8RRRaLilLxo",
|
||||
"size": 1736748
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"sizeEstimate": 2385712,
|
||||
"historyId": "527104",
|
||||
"internalDate": "1732532330000"
|
||||
}
|
||||
+175
File diff suppressed because one or more lines are too long
+183
File diff suppressed because one or more lines are too long
+183
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user