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"], }