handoff fixes

This commit is contained in:
debu-2 2026-10-06 02:44:54 +05:30
parent 6c52cd19cb
commit 89ddebdb60
15 changed files with 991 additions and 271 deletions

View file

@ -0,0 +1,210 @@
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