(20250116) Started upgrading the mail module (to eventually work on cron).

This commit is contained in:
2025-01-16 16:39:23 +05:30
parent efd7dda9f0
commit ed1e86c5e9
71 changed files with 2423 additions and 464 deletions
+223
View File
@@ -0,0 +1,223 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
Tuesday, 3rd Dec., 2024
OBJECTIVE:
To get one full mail for any given user for any given account.
REFERENCES:
N/A
DOWNLOADS:
N/A
NOTES:
N/A
"""
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
# ***** ****
# *****************************************************************************************************************
# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")
# For using Quart:
from quart import Blueprint, current_app, g, request
# My utils:
from utils_v2.string import json
from utils_v2.database.async_mongo_v2 import AsyncMongo
from utils_v2.api.codes import StatusCodes, HttpCodes
from utils_v2.api.response import ResponseModel
from utils_v2.api.async_quart import (
make_ordered_json,
set_api_version,
read_input,
get_session_info,
log_request_to_mongo,
log_chain_to_mongo,
should_not_be_under_maintenance,
only_whitelisted_ips,
limit_rate,
validate_input,
handle_cancelled_request
)
# GMail-related utils:
from utils_v2.goog.controllers.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
from utils_v2.goog.models.auth_tokens import GoogleAuthTokens
# Common:
from shared import constants
# Data Models:
from models.api.message.mail.get import MailGetRequestHeaders, MailGetRequestData
from models.core.user import CoreUserInfoModel
# To work with datatypes:
from typing import Literal
# For asynchronous activities:
import asyncio
# To work with date and time:
import datetime
# Helpers:
from api.helpers.user import token_check
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# Related to Quart:
mail_get_bp = Blueprint("mail_get", __name__)
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** FUNCTIONS ***
# ***** ****
# *****************************************************************************************************************
@mail_get_bp.record_once
def init(blueprint_setup_state):
# This gets called when the blueprint is registered.
# Consider this to be a one-time setup for the whole blueprint:
pass
# ---------------------------------------------------------------------------------------------------------------------
@mail_get_bp.route("", methods = ["GET"])
@set_api_version(api_version = "1.0.0")
@read_input(sanitize_headers = False, sanitize_data = False)
@get_session_info(key = "X-Session-Token", session_coro = "get_session")
@log_request_to_mongo(
attr_name = "logs_mongo",
project = constants.PROJECT_NAME,
log_type = constants.MODULE_NAME,
operation = "mailGetApi",
log_input = True,
log_output = True,
sensitive_keys = ["sessionToken", "X-Session-Token"]
)
@log_chain_to_mongo(attr_name = "logs_mongo")
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
@validate_input(
header_validator = lambda x: MailGetRequestHeaders(**x).model_dump(),
data_validator = lambda x: MailGetRequestData(**x)
)
@handle_cancelled_request()
async def get_one_mail(
inbound_headers: dict | MailGetRequestHeaders = None,
inbound_data: dict | MailGetRequestData = None,
inbound_files: dict = None,
**kwargs
):
"""
Use this endpoint when the user wants to fetch one mail's full payload.
:param inbound_headers: auto-extracted by the decorators.
:param inbound_data: auto-extracted by the decorators.
:param inbound_files: auto-extracted by the decorators.
:param kwargs: Any number of extra inputs supplied by the decorators.
:return: A standard response structure.
"""
# ┏┓ ┓ ┏┓┓ ┓
# ┣┫┓┏╋┣┓ ┃ ┣┓┏┓┏┃┏
# ┛┗┗┻┗┛┗ ┗┛┛┗┗ ┗┛┗
# If the session token is invalid/expired:
if kwargs.get("session_info") is None:
return ResponseModel(
status_code = StatusCodes.FAILED,
http_code = HttpCodes.UNAUTHORIZED,
message = "Invalid session."
)
# ┏┓ ┓ ┳┳┓ •┓
# ┣ ┏┓╋┏┣┓ ┃┃┃┏┓┓┃
# ┻ ┗ ┗┗┛┗ ┛ ┗┗┻┗┗
# Get the mail:
message = await current_app.mail_controller.get_one_mail(
mongo_conn = current_app.data_mongo,
message_id = inbound_data.messageId
)
# ┏┓ ┓ • ┏┓┓ ┓
# ┃┃┓┏┏┏┓┏┓┏┓┏┣┓┓┏┓ ┃ ┣┓┏┓┏┃┏
# ┗┛┗┻┛┛┗┗ ┛ ┛┛┗┗┣┛ ┗┛┛┗┗ ┗┛┗
# ┛
# We check if the token that was used to fetch the mail is owned by this user:
if not await token_check.is_authorized(
mongo_data_conn = current_app.data_mongo,
user_info = CoreUserInfoModel(**kwargs["session_info"]),
token_ids = [message.tokenId]
): return ResponseModel(
status_code = StatusCodes.FAILED,
http_code = HttpCodes.UNAUTHORIZED,
message = "The message does not belong to this user."
)
# ┳┓
# ┣┫┏┓┏┏┓┏┓┏┓┏┏┓
# ┛┗┗ ┛┣┛┗┛┛┗┛┗
# ┛
# Done here:
return ResponseModel(
status_code = StatusCodes.OK if message else StatusCodes.FAILED,
http_code = HttpCodes.SUCCESS if message else HttpCodes.NOT_FOUND,
data = message.full
)
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
pass
@@ -0,0 +1,221 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
Tuesday, 3rd Dec., 2024
OBJECTIVE:
To list e-mails by their token identifier. Remember that the 'token identifier' is the '_id' of the document in
MongoDB that holds the tokens to authorize the e-mail id whose mails are being accessed.
REFERENCES:
N/A
DOWNLOADS:
N/A
NOTES:
N/A
"""
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
# ***** ****
# *****************************************************************************************************************
# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")
# For using Quart:
from quart import Blueprint, current_app, g, request
# My utils:
from utils_v2.string import json
from utils_v2.database.async_mongo_v2 import AsyncMongo
from utils_v2.api.codes import StatusCodes, HttpCodes
from utils_v2.api.response import ResponseModel
from utils_v2.api.async_quart import (
make_ordered_json,
set_api_version,
read_input,
get_session_info,
log_request_to_mongo,
log_chain_to_mongo,
should_not_be_under_maintenance,
only_whitelisted_ips,
limit_rate,
validate_input,
handle_cancelled_request
)
# GMail-related utils:
from utils_v2.goog.controllers.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
from utils_v2.goog.models.auth_tokens import GoogleAuthTokens
# Common:
from shared import constants
# Data Models:
from models.api.message.mail.list import MailListRequestHeaders, MailListRequestData
# To work with datatypes:
from typing import Literal
# For asynchronous activities:
import asyncio
# To work with date and time:
import datetime
# Helpers:
from api.helpers.user import token_check
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# Related to Quart:
mail_list_bp = Blueprint("mail_list", __name__)
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** FUNCTIONS ***
# ***** ****
# *****************************************************************************************************************
@mail_list_bp.record_once
def init(blueprint_setup_state):
# This gets called when the blueprint is registered.
# Consider this to be a one-time setup for the whole blueprint:
pass
# ---------------------------------------------------------------------------------------------------------------------
@mail_list_bp.route("/list", methods = ["GET"])
@mail_list_bp.route("/list/id/token", methods = ["GET", "POST"])
@mail_list_bp.route("/list/tags", methods = ["GET", "POST"])
@set_api_version(api_version = "1.0.0")
@read_input(sanitize_headers = False, sanitize_data = False)
@get_session_info(key = "X-Session-Token", session_coro = "get_session")
@log_request_to_mongo(
attr_name = "logs_mongo",
project = constants.PROJECT_NAME,
log_type = constants.MODULE_NAME,
operation = "mailListApi",
log_input = True,
log_output = 1,
sensitive_keys = ["sessionToken", "X-Session-Token"]
)
@log_chain_to_mongo(attr_name = "logs_mongo")
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
@validate_input(
header_validator = lambda x: MailListRequestHeaders(**x).model_dump(),
data_validator = lambda x: MailListRequestData(**x)
)
@handle_cancelled_request()
async def list_mails(
inbound_headers: dict | MailListRequestHeaders = None,
inbound_data: dict | MailListRequestData = None,
inbound_files: dict = None,
**kwargs
):
"""
Use this endpoint when the user wants to fetch the list of mails. The shortlisting here will be done by way of the
token identifier. Any no. of 'tokenId' objects will be given by the client.
:param inbound_headers: auto-extracted by the decorators.
:param inbound_data: auto-extracted by the decorators.
:param inbound_files: auto-extracted by the decorators.
:param kwargs: Any number of extra inputs supplied by the decorators.
:return: A standard response structure.
"""
# ┏┓ ┓ ┏┓┓ ┓
# ┣┫┓┏╋┣┓ ┃ ┣┓┏┓┏┃┏
# ┛┗┗┻┗┛┗ ┗┛┛┗┗ ┗┛┗
# If the session token is invalid/expired:
if kwargs.get("session_info") is None:
return ResponseModel(
status_code = StatusCodes.FAILED,
http_code = HttpCodes.UNAUTHORIZED,
message = "invalid session"
)
# ┏┓ ┓• ┳┳┓ •┓
# ┣ ┏┓┃┓┏╋ ┃┃┃┏┓┓┃┏
# ┗┛┛┗┗┗┛┗ ┛ ┗┗┻┗┗┛
# Get the token ids from the token keys:
auth_tokens = await current_app.mail_controller.get_tokens_from_keys(
mongo_conn = current_app.data_mongo,
token_keys = inbound_data.tokenKeys
)
token_ids = [t.authTokenId for t in auth_tokens]
# Build the additional filter:
additional_filter = {}
if inbound_data.tags: additional_filter["tags"] = {"$in": inbound_data.tags}
additional_filter = additional_filter or None
# Get the mails:
mails_list = await current_app.mail_controller.list_mails(
mongo_conn = current_app.data_mongo,
token_ids = token_ids,
limit = inbound_data.count,
skip = inbound_data.fromCount,
additional_filter = additional_filter
)
# Done here:
return ResponseModel(
status_code = StatusCodes.OK if mails_list else StatusCodes.FAILED,
http_code = HttpCodes.SUCCESS if mails_list else HttpCodes.NOT_FOUND,
data = [mail.preview for mail in mails_list],
message = f"{len(mails_list) if mails_list else 0} mail(s) found"
)
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
pass