handoff fixes
This commit is contained in:
parent
6c52cd19cb
commit
89ddebdb60
15 changed files with 991 additions and 271 deletions
210
backend/app/services/external_reply_service.py
Normal file
210
backend/app/services/external_reply_service.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue