210 lines
6.7 KiB
Python
210 lines
6.7 KiB
Python
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
|