agentic.email/backend/app/services/thread_service.py
2026-10-06 01:51:23 +05:30

137 lines
3.6 KiB
Python

from datetime import datetime, timezone
from bson import ObjectId
from app.services.message_ids import build_references_header, normalize_message_id
from app.services.subject_normalize import normalize_subject
from app.services.thread_resolver import ThreadResolution
def message_event_at(message: dict) -> datetime:
return (
message.get("received_at")
or message.get("sent_at")
or message.get("created_at")
or datetime.now(timezone.utc)
)
async def create_thread(
database,
*,
account_id: str,
subject: str,
message_id: ObjectId,
event_at: datetime,
thread_type: str = "EXTERNAL",
) -> ObjectId:
now = datetime.now(timezone.utc)
document = {
"account_id": account_id,
"thread_type": thread_type,
"normalized_subject": normalize_subject(subject),
"created_at": now,
"updated_at": now,
"last_message_at": event_at,
"latest_message_id": message_id,
"message_count": 1,
}
result = await database.email_threads.insert_one(document)
return result.inserted_id
async def message_already_in_thread(
database,
*,
account_id: str,
thread_id: ObjectId,
message_id: ObjectId,
) -> bool:
existing = await database.mailbox_entries.find_one(
{
"account_id": account_id,
"thread_id": thread_id,
"message_id": message_id,
},
{"_id": 1},
)
return existing is not None
async def attach_message_to_thread(
database,
*,
account_id: str,
thread_id: ObjectId,
message_id: ObjectId,
subject: str,
event_at: datetime,
) -> None:
if await message_already_in_thread(
database,
account_id=account_id,
thread_id=thread_id,
message_id=message_id,
):
return
now = datetime.now(timezone.utc)
await database.email_threads.update_one(
{"_id": thread_id, "account_id": account_id},
{
"$set": {
"updated_at": now,
"last_message_at": event_at,
"latest_message_id": message_id,
"normalized_subject": normalize_subject(subject),
},
"$inc": {"message_count": 1},
},
)
async def ensure_thread_for_message(
database,
*,
account_id: str,
message: dict,
resolution: ThreadResolution,
) -> ObjectId:
message_id = message["_id"]
event_at = message_event_at(message)
subject = message.get("subject", "")
if resolution.thread_id is not None:
await attach_message_to_thread(
database,
account_id=account_id,
thread_id=resolution.thread_id,
message_id=message_id,
subject=subject,
event_at=event_at,
)
return resolution.thread_id
return await create_thread(
database,
account_id=account_id,
subject=subject,
message_id=message_id,
event_at=event_at,
)
def threading_fields_from_parent(parent: dict) -> dict:
parent_internet_id = normalize_message_id(parent.get("message_id"))
parent_references = parent.get("references") or []
if isinstance(parent_references, str):
parent_references = [parent_references]
references = []
if parent_references:
references = list(parent_references)
if parent_internet_id:
references = build_references_header(references, parent_internet_id).split()
return {
"in_reply_to": parent_internet_id,
"references": references,
"parent_message_id": parent["_id"],
}