141 lines
3.6 KiB
Python
141 lines
3.6 KiB
Python
from datetime import datetime, timezone
|
|
|
|
from bson import ObjectId
|
|
|
|
from app.services.message_ids import (
|
|
build_references_header,
|
|
canonical_message_id,
|
|
internet_message_id_of_message,
|
|
)
|
|
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 = internet_message_id_of_message(parent)
|
|
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"],
|
|
}
|