(20250120) Mail sending API ready.

This commit is contained in:
2025-01-20 14:14:02 +05:30
parent 1f8e13e1d5
commit f6636afe59
8 changed files with 442 additions and 73 deletions
+179 -51
View File
@@ -21,8 +21,7 @@
N/A
"""
import io
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
@@ -36,8 +35,8 @@ sys.path.append(".")
sys.path.append("..")
# For making data behaviour_models:
from pydantic import BaseModel, Field, field_validator, PastDatetime, EmailStr
from typing import Optional, Literal, List
from pydantic import BaseModel, Field, field_validator, PastDatetime, EmailStr, model_validator
from typing import Union, Literal, List
# My utils:
from utils_v2.string import json
@@ -47,6 +46,9 @@ from utils_v2.date_time import date_time
# To work with date and time:
import datetime
# To work with Base64 data:
import base64
# To work with MongoDB:
from bson.objectid import ObjectId
@@ -103,15 +105,10 @@ class MailSendRequestHeaders(BaseModel):
# ---------------------------------------------------------------------------------------------------------------------
class MailSendInlineFiles(BaseModel):
class MailSendPlainText(BaseModel):
key: str = Field(
description = "the key in the form data under which the file has been sent",
frozen = True
)
cid: str = Field(
description = "the content id to assign to the file",
content: str = Field(
description = "The string to add to the mail as plain text.",
frozen = True
)
@@ -127,46 +124,189 @@ class MailSendInlineFiles(BaseModel):
# ---------------------------------------------------------------------------------------------------------------------
class MailSendRequestData(BaseModel):
class MailSendHTMLText(BaseModel):
tokenKey: ObjectId = Field(
description = "the account identifier (Mongo ObjectId)",
content: str = Field(
description = "The HTML string to add to the mail.",
frozen = True
)
html: str = Field(
description = "a valid html string that will become the mail's body",
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫
# ┛
class Config:
extra = "forbid"
# ---------------------------------------------------------------------------------------------------------------------
class MailSendAttachment(BaseModel):
content: str | io.BytesIO = Field(
description = "The Base64 string to add to the mail as a file.",
frozen = True
)
fileName: str = Field(
description = "The name of the file that will be downloaded when the recipient tries to access the content.",
frozen = True
)
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫
# ┛
class Config:
extra = "forbid"
arbitrary_types_allowed = True
# ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
@field_validator("content", mode = "before")
def parse_base64_file(cls, value):
if isinstance(value, str):
base64_parts = value.split(",", 1)
if len(base64_parts) == 1: header, base64_string = None, base64_parts[0]
else: header, base64_string = base64_parts[0], base64_parts[1]
value = io.BytesIO(base64.b64decode(base64_string))
return value
# ---------------------------------------------------------------------------------------------------------------------
class MailSendInlineImage(BaseModel):
content: str | io.BytesIO = Field(
description = "The image content to add to the mail as an inline image file.",
frozen = True
)
fileName: str = Field(
description = "The name of the file that will be downloaded when the recipient tries to access the content.",
frozen = True
)
cid: str | None = Field(
description = "A custom Content-Id to assign to the inline attachment.",
frozen = True,
default = None
)
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫
# ┛
class Config:
extra = "forbid"
arbitrary_types_allowed = True
# ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
@field_validator("content", mode = "before")
def parse_base64_file(cls, value):
if isinstance(value, str):
base64_parts = value.split(",", 1)
if len(base64_parts) == 1: header, base64_string = None, base64_parts[0]
else: header, base64_string = base64_parts[0], base64_parts[1]
value = io.BytesIO(base64.b64decode(base64_string))
return value
# ---------------------------------------------------------------------------------------------------------------------
class MailSendPart(BaseModel):
type: Literal["plain", "html", "attachment", "inline"] = Field(
description = "The kind of part this is.",
frozen = True
)
part: dict | Union[
MailSendPlainText, MailSendHTMLText, # ...... Textual content.
MailSendAttachment, MailSendInlineImage # ... Media content.
] = Field(
description = "One of the structured types of data that can be put in the mail.",
frozen = True
)
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫
# ┛
class Config:
extra = "forbid"
# ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
@model_validator(mode = "before")
def ensure_harmony(cls, values):
kind_map = {
"plain": MailSendPlainText,
"html": MailSendHTMLText,
"attachment": MailSendAttachment,
"inline": MailSendInlineImage,
}
part_dict = values["part"] if isinstance(values["part"], dict) else values["part"].model_dump()
values["part"] = kind_map[values["type"]](**part_dict)
return values
# ---------------------------------------------------------------------------------------------------------------------
class MailSendRequestData(BaseModel):
tokenKey: ObjectId = Field(
description = "The identifier (Mongo ObjectId) of the account from which the mail has to be sent.",
frozen = True
)
to: List[EmailStr] = Field(
description = "the recipient of your mail",
# default = None,
# validate_default = True,
description = "The list of e-mail addresses to send the mail to.",
frozen = True
)
cc: List[EmailStr] = Field(
description = "the list of ids to add as cc",
description = "The list of e-mail addresses to add as CC.",
default = None,
validate_default = True,
frozen = True
)
bcc: List[EmailStr] = Field(
description = "the list of ids to add as bcc",
description = "The list of e-mail addresses to add as BCC.",
default = None,
validate_default = True,
frozen = True
)
inlineFiles: List[MailSendInlineFiles] = Field(
description = (
"when sending files, this will be a list of keys whose "
"associated files will be treated as inline files"
),
default = None,
validate_default = True,
subject: str = Field(
description = "The subject of the mail.",
frozen = True
)
clientThreadId: str | int | None = Field(
description = "The id of the mail-chain if you would like to reply in one.",
frozen = True,
default = None
)
body: List[MailSendPart] = Field(
description = "The actual payload to send as the mail.",
frozen = True
)
@@ -189,32 +329,20 @@ class MailSendRequestData(BaseModel):
except: pass
return value
@field_validator("inlineFiles", mode = "before")
def parse_json(cls, value):
# If we get a null value or an empty string:
if value is None: return []
if isinstance(value, str):
if not value.strip(): return []
# If we get a properly populated string:
try: value = json.from_string(value)
except: pass
# Done here:
return value
@field_validator("to", "cc", "bcc", mode = "before")
def parse_recipients(cls, value):
# If we get a null value or an empty string:
if value is None: return []
if isinstance(value, str):
if not value.strip(): return []
# Ensure that we are working with some kind of list:
if value is None: value = []
if isinstance(value, str): value = [value]
# If we get a properly populated string:
try: value = json.from_string(value)
except: value = [value.strip()]
# # Ensure that all values of the list look like valid mails:
# for index, email_id in enumerate(value):
# if not regex.match(
# text = email_id,
# pattern = regex.REGEX_START + regex.REGEX_EMAIL_ID + regex.REGEX_END,
# case_sensitive = False,
# ): raise ValueError(f"'{email_id}' does not seem to be a valid e-mail id.")
# Done here:
return value