import logging import re from dataclasses import dataclass from datetime import datetime, timezone from bson import ObjectId from app.models import MailDomain from app.services.handoff_email_templates import external_reply_subject from app.services.mail_sender import deliver_email from app.services.message_ids import normalize_message_id from app.services.thread_service import ( attach_message_to_thread, message_event_at, threading_fields_from_parent, ) logger = logging.getLogger(__name__) MESSAGE_SCOPE_EXTERNAL = "EXTERNAL" @dataclass(frozen=True) class ExternalReplyThreading: in_reply_to: str | None references: list[str] parent_internal_message_id: ObjectId replying_to_internet_message_id: str | None @dataclass(frozen=True) class ExternalReplyPlan: subject: str body_text: str body_html: str | None threading: ExternalReplyThreading recipients: list[str] def internet_message_id_of(message: dict) -> str | None: return normalize_message_id( message.get("internet_message_id") or message.get("message_id") ) def build_external_reply_plan( *, parent_external_message: dict, body_text: str, body_html: str | None = None, subject_override: str | None = None, recipients: list[str], ) -> ExternalReplyPlan: threading_fields = threading_fields_from_parent(parent_external_message) parent_internet_id = internet_message_id_of(parent_external_message) if subject_override: subject = subject_override else: parent_subject = parent_external_message.get("subject", "") subject = ( parent_subject if re.match(r"^re:\s*", parent_subject, flags=re.IGNORECASE) else external_reply_subject(parent_subject) ) return ExternalReplyPlan( subject=subject, body_text=body_text, body_html=body_html, threading=ExternalReplyThreading( in_reply_to=threading_fields.get("in_reply_to"), references=threading_fields.get("references", []), parent_internal_message_id=parent_external_message["_id"], replying_to_internet_message_id=parent_internet_id, ), recipients=recipients, ) def log_smtp_message_sending( *, generated_message_id: str | None, in_reply_to: str | None, references: list[str] | None, sender: str, recipients: list[str], subject: str, external_thread_id: ObjectId | str | None = None, replying_to_internal_message_id: ObjectId | str | None = None, replying_to_internet_message_id: str | None = None, ) -> None: logger.info( "smtp_message_sending generated_message_id=%s in_reply_to=%s references=%s " "from=%s to=%s subject=%s external_thread_id=%s " "replying_to_internal_message_id=%s replying_to_internet_message_id=%s", generated_message_id, in_reply_to, " ".join(references or []), sender, ",".join(recipients), subject, external_thread_id, replying_to_internal_message_id, replying_to_internet_message_id, ) async def send_external_reply( database, *, account_id: str, sender: str, sender_name: str, external_thread_id: ObjectId, plan: ExternalReplyPlan, domain: MailDomain, resolution_method: str = "in_reply_to", ) -> dict: log_smtp_message_sending( generated_message_id=None, in_reply_to=plan.threading.in_reply_to, references=plan.threading.references, sender=sender, recipients=plan.recipients, subject=plan.subject, external_thread_id=external_thread_id, replying_to_internal_message_id=plan.threading.parent_internal_message_id, replying_to_internet_message_id=plan.threading.replying_to_internet_message_id, ) internet_message_id = await deliver_email( sender=sender, recipients=plan.recipients, cc=[], bcc=[], subject=plan.subject, body_text=plan.body_text, body_html=plan.body_html, domain=domain, in_reply_to=plan.threading.in_reply_to, references=plan.threading.references, ) normalized_id = normalize_message_id(internet_message_id) log_smtp_message_sending( generated_message_id=normalized_id, in_reply_to=plan.threading.in_reply_to, references=plan.threading.references, sender=sender, recipients=plan.recipients, subject=plan.subject, external_thread_id=external_thread_id, replying_to_internal_message_id=plan.threading.parent_internal_message_id, replying_to_internet_message_id=plan.threading.replying_to_internet_message_id, ) now = datetime.now(timezone.utc) document = { "direction": "outbound", "message_scope": MESSAGE_SCOPE_EXTERNAL, "account_id": account_id, "sender": {"name": sender_name, "address": sender}, "to": [{"name": value.split("@")[0], "address": value} for value in plan.recipients], "subject": plan.subject, "snippet": re.sub(r"\s+", " ", plan.body_text).strip()[:240], "body_text": plan.body_text, "body_html": plan.body_html, "attachments": [], "created_at": now, "sent_at": now, "status": "accepted", "message_id": normalized_id, "internet_message_id": normalized_id, "in_reply_to": plan.threading.in_reply_to, "references": plan.threading.references, "parent_message_id": plan.threading.parent_internal_message_id, "thread_relation": "reply", "thread_resolution_method": resolution_method, } result = await database.messages.insert_one(document) document["_id"] = result.inserted_id await attach_message_to_thread( database, account_id=account_id, thread_id=external_thread_id, message_id=result.inserted_id, subject=plan.subject, event_at=now, ) await database.mailbox_entries.insert_one( { "account_id": account_id, "message_id": result.inserted_id, "thread_id": external_thread_id, "mailbox": "sent", "message_scope": MESSAGE_SCOPE_EXTERNAL, "direction": "outbound", "sender": document["sender"], "recipients": document["to"], "subject": plan.subject, "snippet": document["snippet"], "attachments": [], "status": "accepted", "is_read": True, "is_starred": False, "event_at": now, "created_at": now, "trashed_at": None, "deletion_state": None, } ) return document