308 lines
11 KiB
Python
308 lines
11 KiB
Python
"""
|
|
|
|
AUTHOR:
|
|
|
|
Khushal P Soonderji
|
|
|
|
DATE:
|
|
|
|
Monday, 20th Jan., 2025.
|
|
|
|
OBJECTIVE:
|
|
|
|
To send mails.
|
|
|
|
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.logging.context import AsyncLoggerContext
|
|
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.controllers.gmail.gmail_message import GmailMessage
|
|
from utils_v2.goog.models.auth_tokens import GoogleAuthTokens
|
|
|
|
# Common:
|
|
from shared import constants
|
|
|
|
# Data Models:
|
|
from models.api.message.mail.send import MailSendRequestHeaders, MailSendRequestData
|
|
from models.message.mail.send import MailSendOneResult
|
|
from models.core.user import CoreUserInfoModel
|
|
from models.core.auth_token import CoreAuthTokenModel
|
|
|
|
# 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_send_bp = Blueprint("mail_send", __name__)
|
|
|
|
|
|
# *****************************************************************************************************************
|
|
# ***** ****
|
|
# *** VARIABLES ***
|
|
# ***** ****
|
|
# *****************************************************************************************************************
|
|
|
|
|
|
# --- Nothing Yet
|
|
|
|
|
|
# *****************************************************************************************************************
|
|
# ***** ****
|
|
# *** FUNCTIONS ***
|
|
# ***** ****
|
|
# *****************************************************************************************************************
|
|
|
|
|
|
@mail_send_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
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------------------------------
|
|
|
|
|
|
@AsyncLoggerContext.log_it(
|
|
api_version = "1.0.0",
|
|
project = constants.PROJECT_NAME,
|
|
log_type = constants.MODULE_NAME,
|
|
operation = "mailGmailMsgCreate",
|
|
log_input = 2,
|
|
log_output = 1,
|
|
sensitive_keys = ["sessionToken", "X-Session-Token"]
|
|
)
|
|
async def create_gmail_mail_message(
|
|
from_email: str,
|
|
inbound_data: MailSendRequestData
|
|
) -> GmailMessage:
|
|
|
|
"""
|
|
To create a mail message object of to be sent via Gmail.
|
|
:param from_email: The e-mail id of the sender.
|
|
:param inbound_data: The data that came in with the request.
|
|
:return: The mail message object that can be sent via Gmail.
|
|
"""
|
|
|
|
# Create the instance of the message:
|
|
mail_message = GmailMessage(
|
|
from_email = from_email,
|
|
to_email = inbound_data.to,
|
|
subject = inbound_data.subject,
|
|
cc_emails = inbound_data.cc,
|
|
bcc_emails = inbound_data.bcc
|
|
)
|
|
|
|
# Add all the parts one-by-one:
|
|
for part in inbound_data.body:
|
|
if part.type == "plain": mail_message.add_text(part.part.content)
|
|
elif part.type == "html": mail_message.add_html(part.part.content)
|
|
elif part.type == "inline": mail_message.add_inline_image(part.part.content, part.part.fileName, part.part.cid)
|
|
elif part.type == "attachment": mail_message.add_attachment(part.part.content, part.part.fileName)
|
|
|
|
# Done here:
|
|
return mail_message
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------------------------------
|
|
|
|
|
|
@mail_send_bp.route("", methods = ["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 = "mailSendApi",
|
|
log_input = 1,
|
|
log_output = True,
|
|
sensitive_keys = ["sessionToken", "X-Session-Token", "tokenKey", "tokenId"]
|
|
)
|
|
@log_chain_to_mongo(attr_name = "logs_mongo")
|
|
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
|
|
@validate_input(
|
|
header_validator = lambda x: MailSendRequestHeaders(**x).model_dump(),
|
|
data_validator = lambda x: MailSendRequestData(**x)
|
|
)
|
|
@handle_cancelled_request()
|
|
async def send_one_mail(
|
|
inbound_headers: dict | MailSendRequestHeaders = None,
|
|
inbound_data: dict | MailSendRequestData = None,
|
|
inbound_files: dict = None,
|
|
**kwargs
|
|
):
|
|
|
|
"""
|
|
Use this endpoint to send one mail message.
|
|
: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.
|
|
"""
|
|
|
|
# Start by assuming failure:
|
|
client_controller = None
|
|
client_connector = None
|
|
mail_message = None
|
|
send_result = MailSendOneResult()
|
|
|
|
# ┏┓ ┓ ┏┓┓ ┓
|
|
# ┣┫┓┏╋┣┓ ┃ ┣┓┏┓┏┃┏
|
|
# ┛┗┗┻┗┛┗ ┗┛┛┗┗ ┗┛┗
|
|
|
|
# If the session token is invalid/expired:
|
|
if (
|
|
kwargs.get("session_info") is None and
|
|
inbound_headers["Remote-IP"] not in current_app.whitelisted_ips
|
|
): return constants.API_RESPONSE_UNAUTHORIZED
|
|
|
|
# ┏┓ ┓ • ┏┓┓ ┓
|
|
# ┃┃┓┏┏┏┓┏┓┏┓┏┣┓┓┏┓ ┃ ┣┓┏┓┏┃┏
|
|
# ┗┛┗┻┛┛┗┗ ┛ ┛┛┗┗┣┛ ┗┛┛┗┗ ┗┛┗
|
|
# ┛
|
|
|
|
# Get the token based on the key:
|
|
auth_token = await current_app.mail_controller.get_token_from_key(
|
|
mongo_data_conn = current_app.data_mongo,
|
|
token_key = inbound_data.tokenKey,
|
|
must_be_active = True
|
|
)
|
|
if not auth_token: return constants.API_RESPONSE_NO_AUTH_TOKEN
|
|
|
|
# Get the user's info:
|
|
if inbound_headers["Remote-IP"] not in current_app.whitelisted_ips:
|
|
user_info = CoreUserInfoModel(**kwargs["session_info"])
|
|
else: user_info = auth_token.user
|
|
|
|
# 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 = user_info,
|
|
token_ids = [auth_token.authTokenId]
|
|
): return constants.API_RESPONSE_UNAUTHORIZED
|
|
|
|
# ┏┓ ┓ ┳┳┓ •┓
|
|
# ┗┓┏┓┏┓┏┫ ┃┃┃┏┓┓┃
|
|
# ┗┛┗ ┛┗┗┻ ┛ ┗┗┻┗┗
|
|
|
|
# Figure out the client connector:
|
|
match auth_token.client:
|
|
case "gmail":
|
|
client_controller = current_app.gmail_controller
|
|
client_connector = current_app.gmail_client
|
|
mail_message = await create_gmail_mail_message(
|
|
from_email = auth_token.clientUserId["email"],
|
|
inbound_data = inbound_data
|
|
)
|
|
case _:
|
|
client_controller = None
|
|
client_connector = None
|
|
mail_message = None
|
|
send_result.message = f"Invalid/unimplemented client '{auth_token.client}'"
|
|
|
|
# If the controller and connector were matched:
|
|
if client_controller is not None and client_connector is not None:
|
|
send_result = await client_controller.send_mail(
|
|
sql_conn = current_app.sql_writer,
|
|
mongo_data_conn = current_app.data_mongo,
|
|
mail_client = client_connector,
|
|
mail_message = mail_message,
|
|
auth_token = auth_token,
|
|
client_thread_id = inbound_data.clientThreadId,
|
|
user_info = user_info,
|
|
llm = current_app.llm,
|
|
session_token = inbound_headers["X-Session-Token"]
|
|
)
|
|
|
|
# ┳┓
|
|
# ┣┫┏┓┏┏┓┏┓┏┓┏┏┓
|
|
# ┛┗┗ ┛┣┛┗┛┛┗┛┗
|
|
# ┛
|
|
|
|
# Done here:
|
|
return ResponseModel(
|
|
status_code = StatusCodes.OK if send_result.success else StatusCodes.FAILED,
|
|
http_code = HttpCodes.SUCCESS if send_result.success else HttpCodes.INTERNAL_SERVER_ERROR,
|
|
message = send_result.message
|
|
)
|
|
|
|
|
|
# *****************************************************************************************************************
|
|
# ***** ****
|
|
# *** MAIN PROGRAM ***
|
|
# ***** ****
|
|
# *****************************************************************************************************************
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
pass
|