human hand off

This commit is contained in:
debu-2 2026-10-06 07:12:55 +05:30
parent d0fee5c8f7
commit 7af776196a
34 changed files with 2941 additions and 195 deletions

View file

@ -23,6 +23,7 @@ from app.routers import (
)
from app.services.thread_service import create_thread, message_event_at
from app.services.domain_provisioning import ensure_platform_domain
from app.services.handoff_delivery import handoff_delivery_worker
from app.services.inbound_smtp import InboundSMTPServer
settings = get_settings()
@ -208,6 +209,11 @@ async def lifespan(_: FastAPI):
sparse=True,
unique=True,
)
await mongo_db.messages.create_index(
[("handoff_delivery_key", 1)],
sparse=True,
unique=True,
)
await mongo_db.mailbox_entries.create_index(
[("account_id", 1), ("thread_id", 1), ("event_at", -1), ("_id", -1)]
)
@ -217,18 +223,54 @@ async def lifespan(_: FastAPI):
await mongo_db.human_handoff_rules.create_index(
[("account_id", 1), ("updated_at", -1)]
)
account_ids = await mongo_db.human_handoff_rules.distinct("account_id")
for account_id in account_ids:
existing_rules = (
await mongo_db.human_handoff_rules.find({"account_id": account_id})
.sort([("priority", 1), ("created_at", 1), ("_id", 1)])
.to_list(length=1000)
)
for index, rule in enumerate(existing_rules):
if rule.get("priority") != index:
await mongo_db.human_handoff_rules.update_one(
{"_id": rule["_id"]},
{"$set": {"priority": index}},
)
await mongo_db.human_handoff_rules.create_index(
[("account_id", 1), ("priority", 1)],
unique=True,
)
await mongo_db.human_handoff_rules.create_index(
[("account_id", 1), ("name_key", 1)],
unique=True,
sparse=True,
)
await mongo_db.human_handoffs.create_index(
[("account_id", 1), ("updated_at", -1)]
)
await mongo_db.human_handoffs.create_index(
[("account_id", 1), ("external_thread_id", 1)]
[("account_id", 1), ("external_thread_id", 1)],
unique=True,
)
await mongo_db.human_handoffs.create_index(
[("account_id", 1), ("handoff_thread_id", 1)]
[("account_id", 1), ("handoff_thread_id", 1)],
unique=True,
)
await mongo_db.handoff_message_links.create_index(
[("handoff_id", 1), ("created_at", -1)]
)
await mongo_db.handoff_message_links.create_index(
[("handoff_id", 1), ("source_message_id", 1), ("relationship", 1)],
unique=True,
sparse=True,
)
await mongo_db.handoff_delivery_jobs.create_index(
[("idempotency_key", 1)],
unique=True,
)
await mongo_db.handoff_delivery_jobs.create_index(
[("status", 1), ("next_attempt_at", 1), ("created_at", 1)]
)
await mongo_db.mailbox_entries.create_index(
[("account_id", 1), ("message_scope", 1), ("mailbox", 1), ("event_at", -1)]
)
@ -239,9 +281,11 @@ async def lifespan(_: FastAPI):
await backfill_missing_thread_ids(mongo_db)
if settings.inbound_smtp_enabled:
await inbound_server.start()
await handoff_delivery_worker.start()
try:
yield
finally:
await handoff_delivery_worker.stop()
await inbound_server.stop()
await engine.dispose()
await mongo_client.close()

View file

@ -1,23 +1,36 @@
import re
from datetime import datetime, timezone
from bson import ObjectId
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import EmailStr, Field
from pymongo.errors import DuplicateKeyError
from app.database import get_mongo_database
from app.models import Account
from app.routers.auth import get_current_account
from app.schemas import (
HandoffRuleOrderUpdate,
HandoffRuleCreate,
HandoffRuleRead,
HandoffRuleUpdate,
HandoffTestRequest,
HandoffTestResult,
HandoffConversationRead,
HandoffConversationSummary,
HandoffLinkedExternal,
HandoffMessageRead,
)
from app.services.handoff_evaluator import EMAIL_TYPE_META, HANDOFF_LABELS, evaluate_handoff
from app.services.handoff_delivery import (
STATUS_AMBIGUOUS,
STATUS_FAILED,
STATUS_PENDING,
)
router = APIRouter(prefix="/agent/handoff", tags=["agent-handoff"])
HANDOFF_MAILBOXES = {"handoff_inbox", "handoff_outbox"}
def _now() -> datetime:
return datetime.now(timezone.utc)
@ -31,8 +44,10 @@ def _owner_name_from_email(email: str) -> str:
def _serialize(document: dict) -> HandoffRuleRead:
return HandoffRuleRead(
id=str(document["_id"]),
name=document.get("name") or document.get("owner_name") or "Human handoff",
owner_email=document["owner_email"],
owner_name=document.get("owner_name") or _owner_name_from_email(document["owner_email"]),
priority=document.get("priority", 0),
labels=document.get("labels", []),
email_types=document.get("email_types", []),
instructions=document.get("instructions", ""),
@ -51,10 +66,231 @@ async def list_handoff_rules(
):
cursor = database.human_handoff_rules.find(
{"account_id": str(account.id)}
).sort("updated_at", -1)
).sort([("priority", 1), ("created_at", 1), ("_id", 1)])
return [_serialize(document) async for document in cursor]
def _handoff_status_label(value: str) -> str:
return {
"ACTIVE": "Active",
"WAITING_FOR_HUMAN": "Waiting for human",
"HUMAN_REPLIED": "Human replied",
"CLOSED": "Closed",
}.get(value, value.replace("_", " ").title())
@router.get("/conversations", response_model=list[HandoffConversationSummary])
async def list_handoff_conversations(
mailbox: str = Query(default="handoff_inbox"),
database=Depends(get_mongo_database),
account: Account = Depends(get_current_account),
):
if mailbox not in HANDOFF_MAILBOXES:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid handoff mailbox.",
)
account_id = str(account.id)
cursor = database.mailbox_entries.find(
{
"account_id": account_id,
"message_scope": "HANDOFF",
"mailbox": mailbox,
"deletion_state": {"$in": [None, ""]},
}
).sort([("event_at", -1), ("_id", -1)])
seen: set[ObjectId] = set()
items: list[HandoffConversationSummary] = []
async for entry in cursor:
thread_id = entry.get("thread_id")
if not thread_id or thread_id in seen:
continue
seen.add(thread_id)
handoff = await database.human_handoffs.find_one(
{
"account_id": account_id,
"handoff_thread_id": thread_id,
}
)
message = await database.messages.find_one({"_id": entry["message_id"]})
thread = await database.email_threads.find_one({"_id": thread_id})
if not handoff or not message:
continue
items.append(
HandoffConversationSummary(
id=str(handoff["_id"]),
thread_id=str(thread_id),
mailbox=mailbox,
rule_name=handoff.get("rule_name", "Human handoff"),
external_subject=handoff.get("external_subject", ""),
external_sender=handoff.get("external_sender", ""),
human_email=handoff.get("human_email", ""),
status=handoff.get("status", "ACTIVE"),
status_label=_handoff_status_label(
handoff.get("status", "ACTIVE")
),
current_owner=handoff.get("current_owner", "HUMAN"),
snippet=message.get("new_reply_content")
or message.get("snippet", ""),
message_count=(thread or {}).get("message_count", 1),
updated_at=entry.get("event_at")
or handoff.get("updated_at")
or _now(),
)
)
return items
@router.get("/conversations/counts")
async def handoff_conversation_counts(
database=Depends(get_mongo_database),
account: Account = Depends(get_current_account),
):
account_id = str(account.id)
result = {}
for mailbox in sorted(HANDOFF_MAILBOXES):
thread_ids = await database.mailbox_entries.distinct(
"thread_id",
{
"account_id": account_id,
"message_scope": "HANDOFF",
"mailbox": mailbox,
"deletion_state": {"$in": [None, ""]},
},
)
result[mailbox] = len([value for value in thread_ids if value])
return result
@router.post("/deliveries/{job_id}/retry")
async def retry_handoff_delivery(
job_id: str,
database=Depends(get_mongo_database),
account: Account = Depends(get_current_account),
):
if not ObjectId.is_valid(job_id):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid delivery job ID.",
)
now = _now()
result = await database.handoff_delivery_jobs.find_one_and_update(
{
"_id": ObjectId(job_id),
"account_id": str(account.id),
"status": {"$in": [STATUS_FAILED, STATUS_AMBIGUOUS]},
},
{
"$set": {
"status": STATUS_PENDING,
"next_attempt_at": now,
"updated_at": now,
},
"$unset": {
"error": "",
"failed_at": "",
"ambiguous_at": "",
},
},
return_document=True,
)
if result is None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="This delivery cannot be retried.",
)
return {"id": str(result["_id"]), "status": result["status"]}
@router.get(
"/conversations/{handoff_id}",
response_model=HandoffConversationRead,
)
async def get_handoff_conversation(
handoff_id: str,
database=Depends(get_mongo_database),
account: Account = Depends(get_current_account),
):
if not ObjectId.is_valid(handoff_id):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid handoff ID.",
)
account_id = str(account.id)
handoff = await database.human_handoffs.find_one(
{"_id": ObjectId(handoff_id), "account_id": account_id}
)
if handoff is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Handoff conversation not found.",
)
cursor = database.mailbox_entries.find(
{
"account_id": account_id,
"thread_id": handoff["handoff_thread_id"],
"message_scope": "HANDOFF",
"deletion_state": {"$in": [None, ""]},
}
).sort([("event_at", 1), ("_id", 1)])
messages: list[HandoffMessageRead] = []
async for entry in cursor:
message = await database.messages.find_one({"_id": entry["message_id"]})
if not message:
continue
delivery = await database.handoff_delivery_jobs.find_one(
{
"$or": [
{"source_message_id": message["_id"]},
{"target_message_id": message["_id"]},
]
},
sort=[("created_at", -1)],
)
messages.append(
HandoffMessageRead(
id=str(message["_id"]),
role="human" if message.get("direction") == "inbound" else "agent",
subject=message.get("subject", ""),
snippet=message.get("snippet", ""),
body_text=message.get("body_text", ""),
body_html=message.get("body_html"),
new_reply_content=message.get("new_reply_content"),
raw_body=message.get("body_text")
if message.get("direction") == "inbound"
else None,
has_quoted_text=bool(message.get("has_quoted_text")),
relay_status=message.get("relay_status"),
delivery_status=(delivery or {}).get("status"),
delivery_job_id=str(delivery["_id"]) if delivery else None,
direction=message.get("direction", "outbound"),
mailbox=entry["mailbox"],
event_at=entry.get("event_at")
or message.get("received_at")
or message.get("sent_at")
or message["created_at"],
)
)
return HandoffConversationRead(
id=str(handoff["_id"]),
thread_id=str(handoff["handoff_thread_id"]),
rule_name=handoff.get("rule_name", "Human handoff"),
external_subject=handoff.get("external_subject", ""),
external_sender=handoff.get("external_sender", ""),
human_email=handoff.get("human_email", ""),
handoff_reason="Matched handoff rule",
status=handoff.get("status", "ACTIVE"),
status_label=_handoff_status_label(handoff.get("status", "ACTIVE")),
current_owner=handoff.get("current_owner", "HUMAN"),
linked_external=HandoffLinkedExternal(
thread_id=str(handoff["external_thread_id"]),
subject=handoff.get("external_subject", ""),
sender=handoff.get("external_sender", ""),
),
messages=messages,
)
@router.post("", response_model=HandoffRuleRead, status_code=status.HTTP_201_CREATED)
async def create_handoff_rule(
payload: HandoffRuleCreate,
@ -62,9 +298,30 @@ async def create_handoff_rule(
account: Account = Depends(get_current_account),
):
now = _now()
account_id = str(account.id)
name = payload.name.strip()
duplicate = await database.human_handoff_rules.find_one(
{"account_id": account_id, "name_key": name.casefold()},
{"_id": 1},
)
if duplicate:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="A handoff rule with this name already exists.",
)
priority = payload.priority
if priority is None:
latest = await database.human_handoff_rules.find_one(
{"account_id": account_id},
sort=[("priority", -1)],
)
priority = int(latest.get("priority", -1)) + 1 if latest else 0
document = {
"_id": ObjectId(),
"account_id": str(account.id),
"account_id": account_id,
"name": name,
"name_key": name.casefold(),
"priority": priority,
"owner_email": str(payload.owner_email).lower(),
"owner_name": payload.owner_name or _owner_name_from_email(str(payload.owner_email)),
"labels": payload.labels,
@ -76,10 +333,60 @@ async def create_handoff_rule(
"created_at": now,
"updated_at": now,
}
await database.human_handoff_rules.insert_one(document)
try:
await database.human_handoff_rules.insert_one(document)
except DuplicateKeyError:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="A handoff rule with this name or priority already exists.",
)
return _serialize(document)
@router.put("/reorder", response_model=list[HandoffRuleRead])
async def reorder_handoff_rules(
payload: HandoffRuleOrderUpdate,
database=Depends(get_mongo_database),
account: Account = Depends(get_current_account),
):
if len(payload.rule_ids) != len(set(payload.rule_ids)):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Rule IDs must be unique.",
)
if any(not ObjectId.is_valid(value) for value in payload.rule_ids):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid rule ID.",
)
account_id = str(account.id)
object_ids = [ObjectId(value) for value in payload.rule_ids]
count = await database.human_handoff_rules.count_documents(
{"account_id": account_id, "_id": {"$in": object_ids}}
)
if count != len(object_ids):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="One or more handoff rules were not found.",
)
now = _now()
# Shift priorities out of the unique range first so swaps cannot collide.
for offset, rule_id in enumerate(object_ids):
await database.human_handoff_rules.update_one(
{"_id": rule_id, "account_id": account_id},
{"$set": {"priority": offset + 10_000, "updated_at": now}},
)
for priority, rule_id in enumerate(object_ids):
await database.human_handoff_rules.update_one(
{"_id": rule_id, "account_id": account_id},
{"$set": {"priority": priority, "updated_at": now}},
)
cursor = database.human_handoff_rules.find(
{"account_id": account_id}
).sort([("priority", 1), ("created_at", 1), ("_id", 1)])
return [_serialize(document) async for document in cursor]
@router.patch("/{rule_id}", response_model=HandoffRuleRead)
async def update_handoff_rule(
rule_id: str,
@ -90,14 +397,36 @@ async def update_handoff_rule(
if not ObjectId.is_valid(rule_id):
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid rule ID.")
patch = payload.model_dump(exclude_unset=True)
if "name" in patch:
patch["name"] = patch["name"].strip()
patch["name_key"] = patch["name"].casefold()
duplicate = await database.human_handoff_rules.find_one(
{
"account_id": str(account.id),
"name_key": patch["name_key"],
"_id": {"$ne": ObjectId(rule_id)},
},
{"_id": 1},
)
if duplicate:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="A handoff rule with this name already exists.",
)
if "owner_email" in patch and patch["owner_email"]:
patch["owner_email"] = str(patch["owner_email"]).lower()
patch["updated_at"] = _now()
result = await database.human_handoff_rules.find_one_and_update(
{"_id": ObjectId(rule_id), "account_id": str(account.id)},
{"$set": patch},
return_document=True,
)
try:
result = await database.human_handoff_rules.find_one_and_update(
{"_id": ObjectId(rule_id), "account_id": str(account.id)},
{"$set": patch},
return_document=True,
)
except DuplicateKeyError:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="A handoff rule with this name or priority already exists.",
)
if result is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Handoff rule not found.")
return _serialize(result)
@ -152,7 +481,7 @@ async def test_handoff(
owner_email=owner_email,
**evaluation,
result_path=(
"Would match handoff rule (configuration only)"
"Would hand this email to the Human Owner"
if evaluation["would_handoff"]
else "Would not match handoff rule"
),

View file

@ -1,5 +1,6 @@
import base64
import json
import logging
import re
from datetime import date, datetime, time, timedelta, timezone
from typing import Annotated
@ -35,10 +36,13 @@ from app.services.thread_service import (
message_event_at,
)
from app.services.external_reply_service import build_external_reply_plan, log_smtp_message_sending
from app.services.handoff_mirror_policy import GENERATED_BY_AGENT, MESSAGE_ORIGIN_AGENT
from app.services.human_handoff_service import maybe_trigger_handoff
from app.services.mailbox_scope import apply_external_scope
from app.services.thread_resolver import ThreadResolution
MESSAGE_SCOPE_EXTERNAL = "EXTERNAL"
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/emails", tags=["emails"])
@ -191,6 +195,11 @@ async def create_email(
)
created_at = utc_now()
sender_name = " ".join(
part.strip()
for part in [account.first_name, account.last_name]
if part and part.strip()
)
subject = payload.subject
if parent_message:
reply_plan = build_external_reply_plan(
@ -213,10 +222,12 @@ async def create_email(
document = {
"direction": "outbound",
"message_scope": MESSAGE_SCOPE_EXTERNAL,
"message_origin": MESSAGE_ORIGIN_AGENT,
"generated_by": GENERATED_BY_AGENT,
"account_id": str(account.id),
"sender": address(
sender,
" ".join(part for part in [account.first_name, account.last_name] if part),
sender_name,
),
"to": [address(value) for value in recipients],
"cc": [address(value) for value in cc],
@ -251,6 +262,7 @@ async def create_email(
)
internet_message_id = await deliver_email(
sender=sender,
sender_name=sender_name,
recipients=recipients,
cc=cc,
bcc=bcc,
@ -324,6 +336,22 @@ async def create_email(
"deletion_state": None,
}
)
if document["status"] == "accepted":
try:
await maybe_trigger_handoff(
database,
account_id=str(account.id),
agent_email=sender,
external_message=document,
external_thread_id=thread_id,
)
except Exception:
logger.exception(
"handoff_trigger_failed account_id=%s thread_id=%s message_id=%s",
account.id,
thread_id,
document["_id"],
)
if document["status"] == "failed":
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,

View file

@ -232,8 +232,10 @@ class HandoffEmailSample(BaseModel):
class HandoffRuleCreate(BaseModel):
name: str = Field(min_length=1, max_length=120)
owner_email: EmailStr
owner_name: str | None = None
priority: int | None = Field(default=None, ge=0, le=10000)
labels: list[str] = Field(default_factory=list)
email_types: list[str] = Field(default_factory=list)
instructions: str = ""
@ -243,8 +245,10 @@ class HandoffRuleCreate(BaseModel):
class HandoffRuleUpdate(BaseModel):
name: str | None = Field(default=None, min_length=1, max_length=120)
owner_email: EmailStr | None = None
owner_name: str | None = None
priority: int | None = Field(default=None, ge=0, le=10000)
labels: list[str] | None = None
email_types: list[str] | None = None
instructions: str | None = None
@ -255,8 +259,10 @@ class HandoffRuleUpdate(BaseModel):
class HandoffRuleRead(BaseModel):
id: str
name: str
owner_email: EmailStr
owner_name: str
priority: int
labels: list[str]
email_types: list[str]
instructions: str
@ -267,6 +273,10 @@ class HandoffRuleRead(BaseModel):
updated_at: datetime
class HandoffRuleOrderUpdate(BaseModel):
rule_ids: list[str] = Field(min_length=1, max_length=100)
class HandoffTestRequest(BaseModel):
rule_id: str | None = None
owner_email: EmailStr | None = None
@ -297,6 +307,9 @@ class HandoffLinkedExternal(BaseModel):
class HandoffConversationSummary(BaseModel):
id: str
thread_id: str
mailbox: Literal["handoff_inbox", "handoff_outbox"]
rule_name: str
external_subject: str
external_sender: str
human_email: str
@ -314,15 +327,22 @@ class HandoffMessageRead(BaseModel):
subject: str
snippet: str
body_text: str
body_html: str | None = None
new_reply_content: str | None = None
raw_body: str | None = None
has_quoted_text: bool = False
relay_status: str | None = None
delivery_status: str | None = None
delivery_job_id: str | None = None
direction: Literal["inbound", "outbound"]
mailbox: Literal["handoff_inbox", "handoff_outbox"]
event_at: datetime
class HandoffConversationRead(BaseModel):
id: str
thread_id: str
rule_name: str
external_subject: str
external_sender: str
human_email: str

View file

@ -17,6 +17,7 @@ from app.services.message_ids import (
message_id_fields,
)
from app.services.mime_parser import parse_mime
from app.services.mime_body_extractor import ExtractedMimePart
from app.services.reply_extractor import build_reply_display
from app.services.raw_mime_storage import get_raw_mime_storage
from app.services.thread_service import threading_fields_from_parent
@ -351,6 +352,9 @@ async def send_external_reply(
generated_by: str | None = None,
generated_from_handoff_message_id: ObjectId | None = None,
message_origin: str | None = None,
message_id: str | None = None,
inline_parts: list[ExtractedMimePart] | None = None,
file_attachments: list[ExtractedMimePart] | None = None,
) -> dict:
if generated_from_handoff_message_id:
existing = await database.messages.find_one(
@ -386,6 +390,7 @@ async def send_external_reply(
internet_message_id = await deliver_email(
sender=sender,
sender_name=sender_name,
recipients=plan.recipients,
cc=[],
bcc=[],
@ -395,6 +400,9 @@ async def send_external_reply(
domain=domain,
in_reply_to=plan.threading.in_reply_to,
references=plan.threading.references,
message_id=message_id,
inline_parts=inline_parts,
file_attachments=file_attachments,
)
normalized_id = canonical_message_id(internet_message_id)
id_fields = message_id_fields(normalized_id)
@ -428,7 +436,16 @@ async def send_external_reply(
"has_quoted_text": reply_display["has_quoted_text"],
"body_text": plan.body_text,
"body_html": plan.body_html,
"attachments": [],
"attachments": [
{
"filename": part.filename,
"content_type": part.content_type,
"size": len(part.payload),
"content_id": part.content_id,
"disposition": part.disposition,
}
for part in [*(inline_parts or []), *(file_attachments or [])]
],
"created_at": now,
"sent_at": now,
"status": "accepted",
@ -469,7 +486,7 @@ async def send_external_reply(
"recipients": document["to"],
"subject": plan.subject,
"snippet": document["snippet"],
"attachments": [],
"attachments": document["attachments"],
"status": "accepted",
"is_read": True,
"is_starred": False,

View file

@ -0,0 +1,215 @@
import asyncio
import email.utils
import logging
from datetime import datetime, timezone
from bson import ObjectId
from app.database import mongo_db
logger = logging.getLogger(__name__)
JOB_MIRROR = "MIRROR_TO_HUMAN"
JOB_RELAY = "RELAY_TO_EXTERNAL"
STATUS_PENDING = "PENDING"
STATUS_SENDING = "SENDING"
STATUS_SENT = "SENT"
STATUS_FAILED = "FAILED"
STATUS_AMBIGUOUS = "AMBIGUOUS"
class HandoffPreflightError(RuntimeError):
"""A deterministic error that happened before SMTP submission."""
def _now() -> datetime:
return datetime.now(timezone.utc)
async def enqueue_handoff_delivery(
database,
*,
kind: str,
account_id: str,
handoff_id: ObjectId,
source_message_id: ObjectId,
agent_email: str,
) -> dict:
idempotency_key = f"{kind}:{account_id}:{handoff_id}:{source_message_id}"
now = _now()
document = {
"idempotency_key": idempotency_key,
"kind": kind,
"account_id": account_id,
"handoff_id": handoff_id,
"source_message_id": source_message_id,
"agent_email": agent_email.lower(),
"generated_message_id": email.utils.make_msgid(
domain=agent_email.rsplit("@", 1)[-1].lower()
),
"status": STATUS_PENDING,
"attempt_count": 0,
"next_attempt_at": now,
"created_at": now,
"updated_at": now,
}
await database.handoff_delivery_jobs.update_one(
{"idempotency_key": idempotency_key},
{"$setOnInsert": document},
upsert=True,
)
return await database.handoff_delivery_jobs.find_one(
{"idempotency_key": idempotency_key}
)
async def process_handoff_delivery_once(database=mongo_db) -> bool:
now = _now()
job = await database.handoff_delivery_jobs.find_one_and_update(
{
"status": STATUS_PENDING,
"next_attempt_at": {"$lte": now},
},
{
"$set": {
"status": STATUS_SENDING,
"started_at": now,
"updated_at": now,
},
"$inc": {"attempt_count": 1},
},
sort=[("created_at", 1), ("_id", 1)],
return_document=True,
)
if not job:
return False
try:
from app.services.human_handoff_service import deliver_handoff_job
target_message_id = await deliver_handoff_job(database, job)
except HandoffPreflightError as error:
logger.error(
"handoff_delivery_preflight_failed job=%s kind=%s error=%s",
job["_id"],
job["kind"],
error,
)
await database.handoff_delivery_jobs.update_one(
{"_id": job["_id"], "status": STATUS_SENDING},
{
"$set": {
"status": STATUS_FAILED,
"error": str(error),
"failed_at": _now(),
"updated_at": _now(),
}
},
)
if job["kind"] == JOB_RELAY:
await database.messages.update_one(
{"_id": job["source_message_id"]},
{
"$set": {
"relay_status": STATUS_FAILED,
"relay_error": str(error),
"relay_failed_at": _now(),
}
},
)
except Exception as error:
# SMTP exceptions can be ambiguous: the remote MTA may have accepted the
# message before the acknowledgement was lost. Never auto-retry these.
logger.exception(
"handoff_delivery_ambiguous job=%s kind=%s",
job["_id"],
job["kind"],
)
await database.handoff_delivery_jobs.update_one(
{"_id": job["_id"], "status": STATUS_SENDING},
{
"$set": {
"status": STATUS_AMBIGUOUS,
"error": str(error),
"ambiguous_at": _now(),
"updated_at": _now(),
}
},
)
if job["kind"] == JOB_RELAY:
await database.messages.update_one(
{"_id": job["source_message_id"]},
{
"$set": {
"relay_status": STATUS_AMBIGUOUS,
"relay_error": str(error),
"relay_failed_at": _now(),
}
},
)
else:
await database.handoff_delivery_jobs.update_one(
{"_id": job["_id"], "status": STATUS_SENDING},
{
"$set": {
"status": STATUS_SENT,
"target_message_id": target_message_id,
"sent_at": _now(),
"updated_at": _now(),
}
},
)
return True
class HandoffDeliveryWorker:
def __init__(self, poll_seconds: float = 1.0):
self.poll_seconds = poll_seconds
self._task: asyncio.Task | None = None
self._stop = asyncio.Event()
async def start(self) -> None:
if self._task and not self._task.done():
return
# A process that died during SMTP submission cannot know whether the
# remote MTA accepted it. Quarantine instead of risking a duplicate.
await mongo_db.handoff_delivery_jobs.update_many(
{"status": STATUS_SENDING},
{
"$set": {
"status": STATUS_AMBIGUOUS,
"error": "Worker restarted during SMTP submission.",
"ambiguous_at": _now(),
"updated_at": _now(),
}
},
)
self._stop.clear()
self._task = asyncio.create_task(self._run())
async def stop(self) -> None:
self._stop.set()
if self._task:
await self._task
self._task = None
async def _run(self) -> None:
while not self._stop.is_set():
processed = False
try:
processed = await process_handoff_delivery_once()
except Exception:
logger.exception("handoff_delivery_worker_iteration_failed")
if processed:
continue
try:
await asyncio.wait_for(
self._stop.wait(),
timeout=self.poll_seconds,
)
except TimeoutError:
pass
handoff_delivery_worker = HandoffDeliveryWorker()

View file

@ -1,13 +1,5 @@
from dataclasses import dataclass, field
from app.services.handoff_email_templates import (
build_mirror_handoff_html,
build_mirror_handoff_text,
build_mirror_update_html,
build_mirror_update_text,
format_sender_line,
)
from app.services.html_sanitizer import plain_text_to_html, sanitize_email_html
from app.services.mime_body_extractor import ExtractedMimePart, MirroredMimeContent, extract_mirror_content
from app.services.raw_mime_storage import get_raw_mime_storage
@ -21,11 +13,6 @@ class HandoffMirrorPayload:
async def load_mirror_payload(external_message: dict) -> HandoffMirrorPayload:
sender = external_message.get("sender") or {}
sender_line = format_sender_line(sender)
external_subject = external_message.get("subject", "")
agent_email = external_message.get("_agent_email", "")
body_text = (external_message.get("body_text") or external_message.get("snippet") or "").strip()
body_html = external_message.get("body_html")
inline_parts: list[ExtractedMimePart] = []
@ -44,29 +31,12 @@ async def load_mirror_payload(external_message: dict) -> HandoffMirrorPayload:
except Exception:
pass
if body_html:
original_html = sanitize_email_html(body_html)
elif body_text:
original_html = plain_text_to_html(body_text)
else:
original_html = plain_text_to_html("(No message body)")
if not body_text:
body_text = external_message.get("snippet") or "(No message body)"
return HandoffMirrorPayload(
body_text=build_mirror_handoff_text(
sender_line=sender_line,
external_subject=external_subject,
original_body_text=body_text,
agent_email=agent_email,
),
body_html=build_mirror_handoff_html(
sender_line=sender_line,
external_subject=external_subject,
original_html=original_html,
agent_email=agent_email,
),
body_text=body_text,
body_html=body_html,
inline_parts=inline_parts,
file_attachments=file_attachments,
)
@ -77,32 +47,4 @@ async def load_mirror_update_payload(
*,
agent_email: str,
) -> HandoffMirrorPayload:
external_message = {**external_message, "_agent_email": agent_email}
payload = await load_mirror_payload(external_message)
sender = external_message.get("sender") or {}
sender_line = format_sender_line(sender)
external_subject = external_message.get("subject", "")
body_text = (external_message.get("body_text") or external_message.get("snippet") or "").strip()
body_html = external_message.get("body_html")
if body_html:
original_html = sanitize_email_html(body_html)
else:
original_html = plain_text_to_html(body_text or "(No message body)")
return HandoffMirrorPayload(
body_text=build_mirror_update_text(
sender_line=sender_line,
external_subject=external_subject,
original_body_text=body_text,
agent_email=agent_email,
),
body_html=build_mirror_update_html(
sender_line=sender_line,
external_subject=external_subject,
original_html=original_html,
agent_email=agent_email,
),
inline_parts=payload.inline_parts,
file_attachments=payload.file_attachments,
)
return await load_mirror_payload(external_message)

View file

@ -19,6 +19,7 @@ GENERATED_BY_RELAY_ACK = "RELAY_ACK"
RELAY_STATUS_SENT = "SENT"
RELAY_STATUS_FAILED = "FAILED"
RELAY_STATUS_PENDING = "PENDING"
RELAY_STATUS_BLOCKED = "BLOCKED"
def should_mirror_to_human(message: dict) -> bool:
@ -27,7 +28,7 @@ def should_mirror_to_human(message: dict) -> bool:
if scope != MESSAGE_SCOPE_EXTERNAL:
return False
if message.get("direction") != "inbound":
if message.get("direction") not in ("inbound", "outbound"):
return False
generated_by = message.get("generated_by")
@ -49,6 +50,11 @@ def should_mirror_to_human(message: dict) -> bool:
if message.get("generated_from_handoff_message_id"):
return False
if message.get("direction") == "outbound":
return (
generated_by == GENERATED_BY_AGENT
or origin == MESSAGE_ORIGIN_AGENT
)
return True

View file

@ -1,8 +1,918 @@
"""Human Handoff runtime is disabled — configuration-only via agent_handoff router.
import logging
import re
from datetime import datetime, timezone
from uuid import UUID
This module retains scope constants used by normal mail filtering. All email
sending, receiving, mirroring, forwarding, and relay behavior has been removed.
"""
from bson import ObjectId
from pymongo.errors import DuplicateKeyError
from app.database import SessionFactory
from app.models import Account
from app.services.domain_provisioning import get_domain_by_name
from app.services.external_reply_service import (
ExternalReplyThreadingError,
build_external_reply_plan,
resolve_external_reply_parent,
resolve_internet_message_id,
send_external_reply,
)
from app.services.handoff_delivery import (
HandoffPreflightError,
JOB_MIRROR,
JOB_RELAY,
enqueue_handoff_delivery,
)
from app.services.handoff_evaluator import evaluate_handoff
from app.services.handoff_mirror import load_mirror_payload
from app.services.handoff_mirror_policy import (
GENERATED_BY_AGENT,
GENERATED_BY_HANDOFF_BRIDGE,
GENERATED_BY_HUMAN,
MESSAGE_ORIGIN_AGENT,
MESSAGE_ORIGIN_EXTERNAL_CUSTOMER,
MESSAGE_ORIGIN_HANDOFF_MIRROR,
MESSAGE_ORIGIN_HUMAN_OWNER,
RELAY_STATUS_BLOCKED,
RELAY_STATUS_FAILED,
RELAY_STATUS_PENDING,
RELAY_STATUS_SENT,
should_mirror_to_human,
)
from app.services.mail_sender import deliver_email
from app.services.message_ids import (
canonical_message_id,
folded_message_id,
message_id_fields,
parse_message_id_list,
)
from app.services.mime_body_extractor import extract_new_reply_mime_content
from app.services.raw_mime_storage import get_raw_mime_storage
from app.services.reply_extractor import (
extract_new_reply_content,
validate_external_reply_body,
)
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"
MESSAGE_SCOPE_HANDOFF = "HANDOFF"
THREAD_TYPE_HUMAN_HANDOFF = "HUMAN_HANDOFF"
HANDOFF_STATUS_ACTIVE = "ACTIVE"
HANDOFF_STATUS_WAITING = "WAITING_FOR_HUMAN"
HANDOFF_STATUS_HUMAN_REPLIED = "HUMAN_REPLIED"
OWNER_AGENT = "AGENT"
OWNER_HUMAN = "HUMAN"
LINK_EXTERNAL_MIRRORED = "EXTERNAL_MIRRORED_TO_HUMAN"
LINK_HUMAN_RELAYED = "HUMAN_RELAYED_TO_EXTERNAL"
def _now() -> datetime:
return datetime.now(timezone.utc)
def _snippet(value: str, limit: int = 240) -> str:
return re.sub(r"\s+", " ", value or "").strip()[:limit]
def _safe_subject_part(value: str, fallback: str) -> str:
cleaned = re.sub(r"[\r\n]+", " ", value or "").strip()
return cleaned or fallback
def build_handoff_subject(
*,
rule_name: str,
external_sender: str,
external_subject: str,
) -> str:
subject = " · ".join(
[
_safe_subject_part(rule_name, "Human"),
"handoff",
_safe_subject_part(external_sender, "unknown sender"),
_safe_subject_part(external_subject, "(No subject)"),
]
)
return subject[:998].rstrip()
def redact_human_address(
value: str | None,
*,
human_email: str,
agent_email: str,
) -> str | None:
if value is None or not human_email:
return value
return re.sub(
re.escape(human_email),
agent_email,
value,
flags=re.IGNORECASE,
)
def select_matching_handoff_rule(
rules: list[dict],
*,
email: dict,
) -> dict | None:
ordered = sorted(
rules,
key=lambda rule: (
int(rule.get("priority", 0)),
(
rule["created_at"].timestamp()
if isinstance(rule.get("created_at"), datetime)
else 0
),
str(rule.get("_id", "")),
),
)
return next(
(
rule
for rule in ordered
if evaluate_handoff(
email=email,
labels=rule.get("labels", []),
email_types=rule.get("email_types", []),
instructions=rule.get("instructions", ""),
)["would_handoff"]
),
None,
)
async def _account_and_domain(account_id: str, agent_email: str):
async with SessionFactory() as session:
account = await session.get(Account, UUID(account_id))
domain = await get_domain_by_name(session, agent_email.rsplit("@", 1)[-1])
if account is None:
raise HandoffPreflightError("Handoff account no longer exists.")
if domain is None:
raise HandoffPreflightError("Agent mail domain is not configured.")
if domain.domain_type == "company" and domain.status != "active":
raise HandoffPreflightError("Agent mail domain is not active.")
sender_name = " ".join(
part.strip()
for part in [account.first_name, account.last_name]
if part and part.strip()
)
return account, domain, sender_name
async def create_handoff_thread(
database,
*,
account_id: str,
subject: str,
) -> ObjectId:
now = _now()
result = await database.email_threads.insert_one(
{
"account_id": account_id,
"thread_type": THREAD_TYPE_HUMAN_HANDOFF,
"normalized_subject": subject.casefold(),
"created_at": now,
"updated_at": now,
"last_message_at": now,
"latest_message_id": None,
"message_count": 0,
}
)
return result.inserted_id
async def _latest_thread_message(
database,
*,
account_id: str,
thread_id: ObjectId,
scope: str,
) -> dict | None:
pipeline = [
{
"$match": {
"account_id": account_id,
"thread_id": thread_id,
"message_scope": scope,
"deletion_state": {"$in": [None, ""]},
}
},
{"$sort": {"event_at": -1, "_id": -1}},
{"$limit": 1},
{
"$lookup": {
"from": "messages",
"localField": "message_id",
"foreignField": "_id",
"as": "message",
}
},
{"$unwind": "$message"},
]
cursor = await database.mailbox_entries.aggregate(pipeline)
rows = [row async for row in cursor]
return rows[0]["message"] if rows else None
async def find_handoff_for_external_thread(
database,
*,
account_id: str,
external_thread_id: ObjectId,
) -> dict | None:
return await database.human_handoffs.find_one(
{
"account_id": account_id,
"external_thread_id": external_thread_id,
"status": {"$ne": "CLOSED"},
}
)
async def resolve_handoff_from_inbound(
database,
*,
account_id: str,
agent_email: str,
sender_address: str,
in_reply_to: str | None,
references: list[str],
) -> dict | None:
sender = sender_address.strip().lower()
if not sender or not agent_email:
return None
search_ids = parse_message_id_list(
" ".join([*([in_reply_to] if in_reply_to else []), *(references or [])])
)
for internet_id in reversed(search_ids):
folded = folded_message_id(internet_id)
if not folded:
continue
parent = await database.messages.find_one(
{
"message_scope": MESSAGE_SCOPE_HANDOFF,
"$or": [
{"message_id_folded": folded},
{"message_id": canonical_message_id(internet_id)},
],
},
{"_id": 1},
)
if parent is None:
continue
entry = await database.mailbox_entries.find_one(
{
"account_id": account_id,
"message_id": parent["_id"],
"message_scope": MESSAGE_SCOPE_HANDOFF,
},
{"thread_id": 1},
)
if entry is None:
continue
handoff = await database.human_handoffs.find_one(
{
"account_id": account_id,
"handoff_thread_id": entry["thread_id"],
"human_email": sender,
"agent_email": agent_email.lower(),
"status": {"$ne": "CLOSED"},
}
)
if handoff:
return handoff
return None
async def _record_link(
database,
*,
handoff_id: ObjectId,
source_message_id: ObjectId,
external_message_id: ObjectId | None,
handoff_message_id: ObjectId | None,
relationship: str,
) -> None:
await database.handoff_message_links.update_one(
{
"handoff_id": handoff_id,
"source_message_id": source_message_id,
"relationship": relationship,
},
{
"$setOnInsert": {
"handoff_id": handoff_id,
"source_message_id": source_message_id,
"external_message_id": external_message_id,
"handoff_message_id": handoff_message_id,
"relationship": relationship,
"created_at": _now(),
}
},
upsert=True,
)
def _attachment_metadata(parts) -> list[dict]:
return [
{
"filename": part.filename,
"content_type": part.content_type,
"size": len(part.payload),
"content_id": part.content_id,
"disposition": part.disposition,
}
for part in parts
]
async def store_handoff_message(
database,
*,
account_id: str,
thread_id: ObjectId,
direction: str,
sender: dict,
recipients: list[dict],
subject: str,
body_text: str,
body_html: str | None,
internet_message_id: str | None,
in_reply_to: str | None,
references: list[str],
handoff_id: ObjectId,
event_at: datetime | None = None,
attachments: list[dict] | None = None,
raw_mime: dict | None = None,
handoff_delivery_key: str | None = None,
new_reply_content: str | None = None,
new_reply_html: str | None = None,
quoted_content: str | None = None,
message_origin: str | None = None,
generated_by: str | None = None,
mirrored_from_external_message_id: ObjectId | None = None,
relay_status: str | None = None,
) -> dict:
if handoff_delivery_key:
existing = await database.messages.find_one(
{"handoff_delivery_key": handoff_delivery_key}
)
if existing:
return existing
now = event_at or _now()
id_fields = message_id_fields(canonical_message_id(internet_message_id))
document = {
"account_id": account_id,
"direction": direction,
"message_scope": MESSAGE_SCOPE_HANDOFF,
"handoff_id": handoff_id,
"sender": sender,
"to": recipients,
"subject": subject,
"snippet": _snippet(new_reply_content or body_text),
"body_text": body_text,
"body_html": body_html,
"new_reply_content": new_reply_content,
"new_reply_html": new_reply_html,
"quoted_content": quoted_content or "",
"has_quoted_text": bool(quoted_content),
"attachments": attachments or [],
"created_at": now,
"received_at": now if direction == "inbound" else None,
"sent_at": now if direction == "outbound" else None,
"status": "received" if direction == "inbound" else "accepted",
**id_fields,
"in_reply_to": canonical_message_id(in_reply_to),
"references": references or [],
"message_origin": message_origin,
"generated_by": generated_by,
"relay_status": relay_status,
}
if raw_mime:
document["raw_mime"] = raw_mime
if handoff_delivery_key:
document["handoff_delivery_key"] = handoff_delivery_key
if mirrored_from_external_message_id:
document["mirrored_from_external_message_id"] = mirrored_from_external_message_id
try:
result = await database.messages.insert_one(document)
document["_id"] = result.inserted_id
except DuplicateKeyError:
query = (
{"handoff_delivery_key": handoff_delivery_key}
if handoff_delivery_key
else {"mirrored_from_external_message_id": mirrored_from_external_message_id}
)
existing = await database.messages.find_one(query)
if existing:
return existing
raise
await attach_message_to_thread(
database,
account_id=account_id,
thread_id=thread_id,
message_id=document["_id"],
subject=subject,
event_at=now,
)
mailbox = "handoff_inbox" if direction == "inbound" else "handoff_outbox"
await database.mailbox_entries.update_one(
{
"account_id": account_id,
"message_id": document["_id"],
"mailbox": mailbox,
},
{
"$setOnInsert": {
"account_id": account_id,
"message_id": document["_id"],
"thread_id": thread_id,
"mailbox": mailbox,
"message_scope": MESSAGE_SCOPE_HANDOFF,
"direction": direction,
"sender": sender,
"recipients": recipients,
"subject": subject,
"snippet": document["snippet"],
"attachments": attachments or [],
"status": document["status"],
"is_read": direction == "outbound",
"is_starred": False,
"event_at": now,
"created_at": now,
"trashed_at": None,
"deletion_state": None,
}
},
upsert=True,
)
return document
async def _create_handoff_mapping(
database,
*,
account_id: str,
agent_email: str,
external_message: dict,
external_thread_id: ObjectId,
rule: dict,
) -> dict:
sender = external_message.get("sender") or {}
sender_text = sender.get("address") or sender.get("name") or "unknown sender"
private_subject = build_handoff_subject(
rule_name=rule.get("name") or rule.get("owner_name") or "Human",
external_sender=sender_text,
external_subject=external_message.get("subject", ""),
)
thread_id = await create_handoff_thread(
database,
account_id=account_id,
subject=private_subject,
)
now = _now()
document = {
"account_id": account_id,
"external_thread_id": external_thread_id,
"handoff_thread_id": thread_id,
"rule_id": rule["_id"],
"rule_name": rule.get("name") or rule.get("owner_name") or "Human",
"rule_priority": rule.get("priority", 0),
"human_email": rule["owner_email"].lower(),
"human_name": rule.get("owner_name") or rule["owner_email"].split("@", 1)[0],
"agent_email": agent_email.lower(),
"external_sender": sender.get("address", "").lower(),
"external_subject": external_message.get("subject", ""),
"private_subject": private_subject,
"allow_replies": rule.get("allow_replies", True),
"status": HANDOFF_STATUS_WAITING,
"current_owner": OWNER_HUMAN,
"trigger_external_message_id": external_message["_id"],
"trigger_internet_message_id": external_message.get("internet_message_id")
or external_message.get("message_id"),
"created_at": now,
"updated_at": now,
}
try:
result = await database.human_handoffs.insert_one(document)
document["_id"] = result.inserted_id
return document
except DuplicateKeyError:
await database.email_threads.delete_one({"_id": thread_id, "message_count": 0})
existing = await find_handoff_for_external_thread(
database,
account_id=account_id,
external_thread_id=external_thread_id,
)
if existing:
return existing
raise
async def maybe_trigger_handoff(
database,
*,
account_id: str,
agent_email: str,
external_message: dict,
external_thread_id: ObjectId,
) -> dict | None:
if not should_mirror_to_human(external_message):
return None
sender_address = (
(external_message.get("sender") or {}).get("address") or ""
).lower()
if sender_address == agent_email.lower():
return None
handoff = await find_handoff_for_external_thread(
database,
account_id=account_id,
external_thread_id=external_thread_id,
)
if handoff is None:
if external_message.get("direction") != "inbound":
return None
owner_sender = await database.human_handoff_rules.find_one(
{
"account_id": account_id,
"owner_email": sender_address,
"active": True,
},
{"_id": 1},
)
if owner_sender:
return None
rules = await database.human_handoff_rules.find(
{"account_id": account_id, "active": True}
).sort([("priority", 1), ("created_at", 1), ("_id", 1)]).to_list(length=100)
email_payload = {
"subject": external_message.get("subject", ""),
"snippet": external_message.get("snippet", ""),
"body_text": external_message.get("body_text", ""),
"is_starred": False,
}
rule = select_matching_handoff_rule(
rules,
email=email_payload,
)
if rule is None:
return None
handoff = await _create_handoff_mapping(
database,
account_id=account_id,
agent_email=agent_email,
external_message=external_message,
external_thread_id=external_thread_id,
rule=rule,
)
await enqueue_handoff_delivery(
database,
kind=JOB_MIRROR,
account_id=account_id,
handoff_id=handoff["_id"],
source_message_id=external_message["_id"],
agent_email=agent_email,
)
return handoff
async def process_human_reply(
database,
*,
account_id: str,
agent_email: str,
handoff: dict,
parsed: dict,
body_text: str,
body_html: str | None,
raw_mime: dict,
delivery_key: str,
) -> dict:
extracted = extract_new_reply_content(
body_text=body_text,
body_html=body_html,
)
rich = None
try:
raw = await get_raw_mime_storage().get(raw_mime)
rich = extract_new_reply_mime_content(raw)
except Exception:
logger.exception("handoff_reply_rich_extract_failed delivery=%s", delivery_key)
human_email = (parsed.get("sender") or {}).get("address", "").lower()
new_text = (
rich.body_text if rich and rich.body_text else extracted["new_reply_content"]
).strip()
new_html = rich.body_html if rich else None
if human_email:
new_text = redact_human_address(
new_text,
human_email=human_email,
agent_email=agent_email,
)
new_html = redact_human_address(
new_html,
human_email=human_email,
agent_email=agent_email,
)
auto_submitted = (
(parsed.get("raw_headers") or {}).get("auto_submitted") or ""
).strip().lower()
is_automatic = bool(auto_submitted and auto_submitted != "no")
private_markers_ok = validate_external_reply_body(new_text)
relay_status = (
RELAY_STATUS_PENDING
if (
new_text
and private_markers_ok
and not is_automatic
and handoff.get("allow_replies", True)
)
else RELAY_STATUS_BLOCKED
)
human_message = await store_handoff_message(
database,
account_id=account_id,
thread_id=handoff["handoff_thread_id"],
direction="inbound",
sender=parsed.get("sender") or {"name": "", "address": human_email},
recipients=parsed.get("to", []),
subject=parsed.get("subject", ""),
body_text=body_text,
body_html=body_html,
internet_message_id=parsed.get("message_id"),
in_reply_to=parsed.get("in_reply_to"),
references=parsed.get("references", []),
handoff_id=handoff["_id"],
event_at=parsed.get("sent_at") or _now(),
attachments=parsed.get("attachments", []),
raw_mime=raw_mime,
handoff_delivery_key=f"handoff:{account_id}:{handoff['_id']}:{delivery_key}",
new_reply_content=new_text,
new_reply_html=new_html,
quoted_content=extracted.get("quoted_content", ""),
message_origin=MESSAGE_ORIGIN_HUMAN_OWNER,
generated_by=GENERATED_BY_HUMAN,
relay_status=relay_status,
)
if relay_status == RELAY_STATUS_PENDING:
await enqueue_handoff_delivery(
database,
kind=JOB_RELAY,
account_id=account_id,
handoff_id=handoff["_id"],
source_message_id=human_message["_id"],
agent_email=agent_email,
)
else:
await database.messages.update_one(
{"_id": human_message["_id"]},
{
"$set": {
"relay_error": (
"Human replies are disabled for this handoff."
if not handoff.get("allow_replies", True)
else (
"Automatic replies are not relayed through Human Handoff."
if is_automatic
else "Reply content could not be safely separated from private history."
)
)
}
},
)
await database.human_handoffs.update_one(
{"_id": handoff["_id"]},
{
"$set": {
"status": HANDOFF_STATUS_HUMAN_REPLIED,
"current_owner": OWNER_AGENT,
"last_human_message_at": _now(),
"updated_at": _now(),
}
},
)
return human_message
async def _deliver_mirror(database, job: dict, handoff: dict, source: dict) -> ObjectId:
existing = await database.messages.find_one(
{
"message_scope": MESSAGE_SCOPE_HANDOFF,
"mirrored_from_external_message_id": source["_id"],
}
)
if existing:
return existing["_id"]
_, domain, sender_name = await _account_and_domain(
job["account_id"], handoff["agent_email"]
)
payload = await load_mirror_payload(source)
parent = await _latest_thread_message(
database,
account_id=job["account_id"],
thread_id=handoff["handoff_thread_id"],
scope=MESSAGE_SCOPE_HANDOFF,
)
threading = threading_fields_from_parent(parent) if parent else {}
subject = handoff["private_subject"]
if parent and not re.match(r"^re:\s*", subject, re.IGNORECASE):
subject = f"Re: {subject}"
internet_message_id = await deliver_email(
sender=handoff["agent_email"],
sender_name=sender_name,
recipients=[handoff["human_email"]],
cc=[],
bcc=[],
subject=subject,
body_text=payload.body_text,
body_html=payload.body_html,
domain=domain,
in_reply_to=threading.get("in_reply_to"),
references=threading.get("references"),
message_id=job["generated_message_id"],
inline_parts=payload.inline_parts,
file_attachments=payload.file_attachments,
)
attachments = _attachment_metadata(
[*payload.inline_parts, *payload.file_attachments]
)
mirror = await store_handoff_message(
database,
account_id=job["account_id"],
thread_id=handoff["handoff_thread_id"],
direction="outbound",
sender={"name": sender_name, "address": handoff["agent_email"]},
recipients=[
{
"name": handoff.get("human_name", ""),
"address": handoff["human_email"],
}
],
subject=subject,
body_text=payload.body_text,
body_html=payload.body_html,
internet_message_id=internet_message_id,
in_reply_to=threading.get("in_reply_to"),
references=threading.get("references", []),
handoff_id=handoff["_id"],
attachments=attachments,
message_origin=MESSAGE_ORIGIN_HANDOFF_MIRROR,
generated_by=GENERATED_BY_HANDOFF_BRIDGE,
mirrored_from_external_message_id=source["_id"],
)
await _record_link(
database,
handoff_id=handoff["_id"],
source_message_id=source["_id"],
external_message_id=source["_id"],
handoff_message_id=mirror["_id"],
relationship=LINK_EXTERNAL_MIRRORED,
)
await database.human_handoffs.update_one(
{"_id": handoff["_id"]},
{
"$set": {
"status": HANDOFF_STATUS_WAITING,
"current_owner": OWNER_HUMAN,
"last_mirrored_at": _now(),
"updated_at": _now(),
}
},
)
return mirror["_id"]
async def _deliver_relay(database, job: dict, handoff: dict, source: dict) -> ObjectId:
existing = await database.messages.find_one(
{
"message_scope": MESSAGE_SCOPE_EXTERNAL,
"generated_from_handoff_message_id": source["_id"],
}
)
if existing:
return existing["_id"]
new_text = (source.get("new_reply_content") or "").strip()
if not new_text or not validate_external_reply_body(new_text):
raise HandoffPreflightError(
"Reply content failed the private-content leakage guard."
)
external_parent = await resolve_external_reply_parent(
database,
account_id=job["account_id"],
external_thread_id=handoff["external_thread_id"],
customer_address=handoff["external_sender"],
handoff=handoff,
)
if external_parent is None:
raise HandoffPreflightError("No valid external reply parent was found.")
parent_internet_id = await resolve_internet_message_id(database, external_parent)
if not parent_internet_id:
raise HandoffPreflightError(
"The external reply parent has no RFC Message-ID."
)
recipient = handoff.get("external_sender") or (
external_parent.get("sender") or {}
).get("address")
if not recipient:
raise HandoffPreflightError("The external sender address is missing.")
_, domain, sender_name = await _account_and_domain(
job["account_id"], handoff["agent_email"]
)
inline_parts = []
file_attachments = []
body_html = source.get("new_reply_html")
if source.get("raw_mime"):
raw = await get_raw_mime_storage().get(source["raw_mime"])
rich = extract_new_reply_mime_content(raw)
inline_parts = rich.inline_parts
file_attachments = rich.file_attachments
plan = build_external_reply_plan(
parent_external_message=external_parent,
body_text=new_text,
body_html=body_html,
subject_override=None,
recipients=[recipient],
parent_internet_id_override=parent_internet_id,
)
try:
external = await send_external_reply(
database,
account_id=job["account_id"],
sender=handoff["agent_email"],
sender_name=sender_name,
external_thread_id=handoff["external_thread_id"],
plan=plan,
domain=domain,
resolution_method="human_handoff_bridge",
generated_by=GENERATED_BY_HANDOFF_BRIDGE,
generated_from_handoff_message_id=source["_id"],
message_origin=MESSAGE_ORIGIN_HUMAN_OWNER,
message_id=job["generated_message_id"],
inline_parts=inline_parts,
file_attachments=file_attachments,
)
except ExternalReplyThreadingError as error:
raise HandoffPreflightError(str(error)) from error
await database.messages.update_one(
{"_id": source["_id"]},
{
"$set": {
"relay_status": RELAY_STATUS_SENT,
"generated_external_message_id": external["_id"],
"relay_delivered_at": _now(),
}
},
)
await _record_link(
database,
handoff_id=handoff["_id"],
source_message_id=source["_id"],
external_message_id=external["_id"],
handoff_message_id=source["_id"],
relationship=LINK_HUMAN_RELAYED,
)
await database.human_handoffs.update_one(
{"_id": handoff["_id"]},
{
"$set": {
"status": HANDOFF_STATUS_ACTIVE,
"current_owner": OWNER_AGENT,
"updated_at": _now(),
}
},
)
return external["_id"]
async def deliver_handoff_job(database, job: dict) -> ObjectId:
handoff = await database.human_handoffs.find_one(
{"_id": job["handoff_id"], "account_id": job["account_id"]}
)
if handoff is None:
raise HandoffPreflightError("Linked handoff no longer exists.")
source = await database.messages.find_one({"_id": job["source_message_id"]})
if source is None:
raise HandoffPreflightError("Source message no longer exists.")
if job["kind"] == JOB_MIRROR:
return await _deliver_mirror(database, job, handoff, source)
if job["kind"] == JOB_RELAY:
return await _deliver_relay(database, job, handoff, source)
raise HandoffPreflightError(f"Unknown handoff delivery kind: {job['kind']}")

View file

@ -10,7 +10,16 @@ from app.config import get_settings
from app.database import SessionFactory, mongo_db
from app.models import Account
from app.services.external_reply_service import log_original_external_message
from app.services.handoff_mirror_policy import (
GENERATED_BY_CUSTOMER,
MESSAGE_ORIGIN_EXTERNAL_CUSTOMER,
)
from app.services.human_handoff_service import MESSAGE_SCOPE_EXTERNAL
from app.services.human_handoff_service import (
maybe_trigger_handoff,
process_human_reply,
resolve_handoff_from_inbound,
)
from app.services.message_ids import canonical_message_id, message_id_fields
from app.services.mime_parser import parse_mime
from app.services.reply_extractor import build_reply_display
@ -121,26 +130,65 @@ class InboundHandler:
body_text = parsed.pop("body_text")
body_html = parsed.pop("body_html")
stored = await get_raw_mime_storage().put(raw, delivery_key)
raw_mime = {
"bucket": stored.bucket,
"name": stored.name,
"generation": stored.generation,
"size": stored.size,
"crc32c": stored.crc32c,
}
now = datetime.now(timezone.utc)
sender_address = (parsed.get("sender") or {}).get("address", "").lower()
internet_message_id = canonical_message_id(parsed.get("message_id"))
id_fields = message_id_fields(internet_message_id)
external_recipients: list[str] = []
for recipient in recipients:
account_id = envelope.account_ids[recipient]
handoff = await resolve_handoff_from_inbound(
mongo_db,
account_id=account_id,
agent_email=recipient,
sender_address=sender_address,
in_reply_to=parsed.get("in_reply_to"),
references=parsed.get("references", []),
)
if handoff is None:
external_recipients.append(recipient)
continue
await process_human_reply(
mongo_db,
account_id=account_id,
agent_email=recipient,
handoff=handoff,
parsed=parsed,
body_text=body_text,
body_html=body_html,
raw_mime=raw_mime,
delivery_key=delivery_key,
)
logger.info(
"handoff_reply_received account_id=%s handoff_id=%s message_id=%s",
account_id,
handoff["_id"],
internet_message_id,
)
if not external_recipients:
return f"250 2.0.0 Handoff message accepted as {delivery_key}"
recipients = external_recipients
message = {
**parsed,
"direction": "inbound",
"message_scope": MESSAGE_SCOPE_EXTERNAL,
"message_origin": MESSAGE_ORIGIN_EXTERNAL_CUSTOMER,
"generated_by": GENERATED_BY_CUSTOMER,
"envelope_sender": envelope.mail_from.lower(),
"envelope_recipients": recipients,
"delivery_key": delivery_key,
**id_fields,
"raw_mime": {
"bucket": stored.bucket,
"name": stored.name,
"generation": stored.generation,
"size": stored.size,
"crc32c": stored.crc32c,
},
"raw_mime": raw_mime,
"received_at": now,
"created_at": now,
"has_text_body": bool(body_text),
@ -275,6 +323,21 @@ class InboundHandler:
thread_id,
mailbox_result.upserted_id is not None or mailbox_result.modified_count > 0,
)
try:
await maybe_trigger_handoff(
mongo_db,
account_id=account_id,
agent_email=recipient,
external_message=persisted,
external_thread_id=thread_id,
)
except Exception:
logger.exception(
"handoff_trigger_failed account_id=%s thread_id=%s message_id=%s",
account_id,
thread_id,
persisted["_id"],
)
except Exception:
logger.exception("Temporary failure accepting inbound message")
return "451 4.3.0 Temporary server failure; please retry"

View file

@ -16,6 +16,36 @@ from app.services.mime_body_extractor import ExtractedMimePart
SMTP_POLICY = email.policy.SMTP.clone(max_line_length=998)
logger = logging.getLogger(__name__)
SYSTEM_SENDER_LOCALS = {
"invite",
"invites",
"notification",
"notifications",
"no-reply",
"noreply",
"support",
}
def _sender_identity(sender: str, sender_name: str | None = None) -> tuple[str, str]:
"""Return an RFC-compliant From header and the bare SMTP envelope address."""
if "\r" in sender or "\n" in sender or (sender_name and ("\r" in sender_name or "\n" in sender_name)):
raise ValueError("Sender identity cannot contain line breaks.")
existing_name, address = email.utils.parseaddr(sender)
address = address.strip().lower()
if not address or "@" not in address:
raise ValueError("A valid sender email address is required.")
display_name = (sender_name or existing_name).strip()
if not display_name:
local_part = address.split("@", 1)[0]
if local_part in SYSTEM_SENDER_LOCALS:
display_name = "aGentic Email"
else:
display_name = local_part.replace(".", " ").replace("_", " ").replace("-", " ").title()
return email.utils.formataddr((display_name, address), charset="utf-8"), address
def log_outbound_mime_headers(message: Message) -> None:
@ -62,6 +92,7 @@ def _mime_attachment(part: ExtractedMimePart) -> MIMEBase:
def build_message(
*,
sender: str,
sender_name: str | None = None,
recipients: list[str],
cc: list[str],
subject: str,
@ -76,10 +107,11 @@ def build_message(
) -> Message:
inline_parts = inline_parts or []
file_attachments = file_attachments or []
sender_header, sender_address = _sender_identity(sender, sender_name)
if inline_parts or file_attachments:
root = MIMEMultipart("mixed", policy=SMTP_POLICY)
root["From"] = sender
root["From"] = sender_header
root["To"] = ", ".join(recipients)
if cc:
root["Cc"] = ", ".join(cc)
@ -87,7 +119,7 @@ def build_message(
root["Reply-To"] = reply_to
root["Subject"] = subject
root["Date"] = email.utils.formatdate(localtime=True)
generated_id = message_id or email.utils.make_msgid(domain=sender.split("@")[-1])
generated_id = message_id or email.utils.make_msgid(domain=sender_address.split("@")[-1])
root["Message-ID"] = generated_id
if in_reply_to:
root["In-Reply-To"] = in_reply_to
@ -115,7 +147,7 @@ def build_message(
return root
message = EmailMessage(policy=SMTP_POLICY)
message["From"] = sender
message["From"] = sender_header
message["To"] = ", ".join(recipients)
if cc:
message["Cc"] = ", ".join(cc)
@ -123,7 +155,7 @@ def build_message(
message["Reply-To"] = reply_to
message["Subject"] = subject
message["Date"] = email.utils.formatdate(localtime=True)
generated_id = message_id or email.utils.make_msgid(domain=sender.split("@")[-1])
generated_id = message_id or email.utils.make_msgid(domain=sender_address.split("@")[-1])
message["Message-ID"] = generated_id
if in_reply_to:
message["In-Reply-To"] = in_reply_to
@ -191,6 +223,7 @@ async def send_via_smtp(
async def deliver_email(
*,
sender: str,
sender_name: str | None = None,
recipients: list[str],
cc: list[str],
bcc: list[str],
@ -205,8 +238,10 @@ async def deliver_email(
inline_parts: list[ExtractedMimePart] | None = None,
file_attachments: list[ExtractedMimePart] | None = None,
) -> str:
_, sender_address = _sender_identity(sender, sender_name)
message = build_message(
sender=sender,
sender_name=sender_name,
recipients=recipients,
cc=cc,
subject=subject,
@ -224,7 +259,7 @@ async def deliver_email(
log_signed_mime_headers(signed)
await send_via_smtp(
signed,
sender=sender,
sender=sender_address,
recipients=[*recipients, *cc, *bcc],
)
return str(message["Message-ID"])

View file

@ -2,6 +2,10 @@ from dataclasses import dataclass, field
from email import policy
from email.message import EmailMessage, Message
from email.parser import BytesParser
from html.parser import HTMLParser
import re
from app.services.reply_extractor import extract_new_reply_content
@dataclass
@ -104,3 +108,109 @@ def extract_mirror_content(raw: bytes) -> MirroredMimeContent:
collector = MirroredMimeContent()
_walk_parts(message, collector)
return collector
class _NewReplyHTMLParser(HTMLParser):
QUOTE_CLASSES = {
"gmail_attr",
"gmail_quote",
"protonmail_quote",
"yahoo_quoted",
"moz-cite-prefix",
}
QUOTE_IDS = {"divRplyFwdMsg", "appendonsend"}
VOID_TAGS = {"br", "hr", "img", "meta", "link", "input"}
def __init__(self) -> None:
super().__init__(convert_charrefs=False)
self.output: list[str] = []
self.open_tags: list[str] = []
self.stopped = False
def _is_quote_start(self, tag: str, attrs: list[tuple[str, str | None]]) -> bool:
values = {key.lower(): (value or "") for key, value in attrs}
classes = set(values.get("class", "").split())
return (
tag.lower() == "blockquote"
or bool(classes.intersection(self.QUOTE_CLASSES))
or values.get("id") in self.QUOTE_IDS
)
def handle_starttag(self, tag: str, attrs) -> None:
if self.stopped:
return
if self._is_quote_start(tag, attrs):
self.stopped = True
return
self.output.append(self.get_starttag_text())
if tag.lower() not in self.VOID_TAGS:
self.open_tags.append(tag)
def handle_startendtag(self, tag: str, attrs) -> None:
if not self.stopped:
self.output.append(self.get_starttag_text())
def handle_endtag(self, tag: str) -> None:
if self.stopped:
return
self.output.append(f"</{tag}>")
if tag in self.open_tags:
reverse_index = self.open_tags[::-1].index(tag)
del self.open_tags[len(self.open_tags) - reverse_index - 1]
def handle_data(self, data: str) -> None:
if self.stopped:
return
if re.search(r"(?:^|\n)\s*On .+? wrote:\s*$", data, re.IGNORECASE):
self.stopped = True
return
self.output.append(data)
def handle_entityref(self, name: str) -> None:
if not self.stopped:
self.output.append(f"&{name};")
def handle_charref(self, name: str) -> None:
if not self.stopped:
self.output.append(f"&#{name};")
def html(self) -> str:
value = "".join(self.output).strip()
if not value:
return ""
return value + "".join(f"</{tag}>" for tag in reversed(self.open_tags))
def _new_reply_html(html: str | None) -> str | None:
if not html:
return None
parser = _NewReplyHTMLParser()
parser.feed(html)
parser.close()
value = parser.html().strip()
return value or None
def extract_new_reply_mime_content(raw: bytes) -> MirroredMimeContent:
"""Extract only newly authored MIME content while retaining rich formatting."""
full = extract_mirror_content(raw)
extracted = extract_new_reply_content(
body_text=full.body_text,
body_html=full.body_html,
)
new_html = _new_reply_html(full.body_html)
referenced_cids = {
match.casefold()
for match in re.findall(r"cid:([^\"'\s>)]+)", new_html or "", re.IGNORECASE)
}
inline_parts = [
part
for part in full.inline_parts
if part.content_id and part.content_id.casefold() in referenced_cids
]
return MirroredMimeContent(
body_text=extracted["new_reply_content"].strip(),
body_html=new_html,
inline_parts=inline_parts,
file_attachments=full.file_attachments,
)

View file

@ -94,6 +94,11 @@ def parse_mime(raw: bytes) -> dict:
"to": _header_str(message.get("To")),
"cc": _header_str(message.get("Cc")),
"reply_to": _header_str(message.get("Reply-To")),
"auto_submitted": _header_str(message.get("Auto-Submitted")),
"precedence": _header_str(message.get("Precedence")),
"x_auto_response_suppress": _header_str(
message.get("X-Auto-Response-Suppress")
),
}
return {
"message_id": internet_message_id,

View file

@ -44,9 +44,10 @@ async def send_employee_invitation(
expires_hours=INVITE_EXPIRY_HOURS,
)
sender = f"{BRAND_NAME} <{INVITE_FROM_LOCAL}@{settings.platform_domain}>"
sender = f"{INVITE_FROM_LOCAL}@{settings.platform_domain}"
await deliver_email(
sender=sender,
sender_name=BRAND_NAME,
recipients=[member.invitation_email],
cc=[],
bcc=[],

View file

@ -16,6 +16,7 @@ TEXT_QUOTE_MARKERS = [
PRIVATE_LEAK_MARKERS = [
"human handoff",
" · handoff · ",
"a conversation needs your attention",
"why this was handed off",
"why you're receiving this",
@ -37,11 +38,19 @@ def _normalize_newlines(text: str) -> str:
class _ReplyHTMLTextParser(HTMLParser):
BLOCK_TAGS = {"div", "p", "pre", "li", "ul", "ol", "table", "tr"}
IGNORED_TAGS = {"script", "style", "iframe", "object"}
QUOTE_CLASSES = {
"gmail_attr",
"gmail_quote",
"protonmail_quote",
"yahoo_quoted",
"moz-cite-prefix",
}
def __init__(self) -> None:
super().__init__(convert_charrefs=True)
self.parts: list[str] = []
self.quote_depth = 0
self.quote_tags: list[str] = []
self.ignored_depth = 0
self.at_line_start = True
@ -70,11 +79,19 @@ class _ReplyHTMLTextParser(HTMLParser):
return
if self.ignored_depth:
return
classes = {
value
for key, value in attrs
if key.lower() == "class"
for value in (value or "").split()
}
is_quote = tag == "blockquote" or bool(classes.intersection(self.QUOTE_CLASSES))
if tag == "br":
self._newline()
elif tag == "blockquote":
elif is_quote:
self._newline()
self.quote_depth += 1
self.quote_tags.append(tag)
elif tag in self.BLOCK_TAGS:
self._newline()
@ -85,9 +102,10 @@ class _ReplyHTMLTextParser(HTMLParser):
return
if self.ignored_depth:
return
if tag == "blockquote":
self._newline()
if self.quote_tags and self.quote_tags[-1] == tag:
self.quote_tags.pop()
self.quote_depth = max(0, self.quote_depth - 1)
self._newline()
elif tag in self.BLOCK_TAGS:
self._newline()

View file

@ -31,9 +31,10 @@ async def send_welcome_email(session: AsyncSession, account: Account) -> None:
login_url=login_url,
is_company_account=account.account_type == "company",
)
sender = f"AGMail Team <{NOTIFICATIONS_FROM_LOCAL}@{settings.platform_domain}>"
sender = f"{NOTIFICATIONS_FROM_LOCAL}@{settings.platform_domain}"
await deliver_email(
sender=sender,
sender_name=BRAND_NAME,
recipients=[account.email],
cc=[],
bcc=[],

View file

@ -0,0 +1,83 @@
from datetime import datetime, timezone
import pytest
from bson import ObjectId
from app.services import human_handoff_service
from app.services.handoff_delivery import (
STATUS_AMBIGUOUS,
STATUS_PENDING,
STATUS_SENT,
process_handoff_delivery_once,
)
class FakeJobs:
def __init__(self):
self.job = {
"_id": ObjectId(),
"kind": "MIRROR_TO_HUMAN",
"account_id": "account-1",
"handoff_id": ObjectId(),
"source_message_id": ObjectId(),
"status": STATUS_PENDING,
"created_at": datetime.now(timezone.utc),
"next_attempt_at": datetime.now(timezone.utc),
}
async def find_one_and_update(self, query, update, **kwargs):
if self.job["status"] != STATUS_PENDING:
return None
self.job["status"] = "SENDING"
self.job["attempt_count"] = self.job.get("attempt_count", 0) + 1
return dict(self.job)
async def update_one(self, query, update):
self.job.update(update.get("$set", {}))
class FakeMessages:
async def update_one(self, query, update):
return None
class FakeDatabase:
def __init__(self):
self.handoff_delivery_jobs = FakeJobs()
self.messages = FakeMessages()
@pytest.mark.asyncio
async def test_delivery_job_is_claimed_and_sent_once(monkeypatch):
database = FakeDatabase()
calls = 0
async def deliver(database, job):
nonlocal calls
calls += 1
return ObjectId()
monkeypatch.setattr(human_handoff_service, "deliver_handoff_job", deliver)
assert await process_handoff_delivery_once(database) is True
assert await process_handoff_delivery_once(database) is False
assert calls == 1
assert database.handoff_delivery_jobs.job["status"] == STATUS_SENT
@pytest.mark.asyncio
async def test_ambiguous_smtp_failure_is_not_automatically_retried(monkeypatch):
database = FakeDatabase()
calls = 0
async def deliver(database, job):
nonlocal calls
calls += 1
raise TimeoutError("SMTP acknowledgement lost")
monkeypatch.setattr(human_handoff_service, "deliver_handoff_job", deliver)
assert await process_handoff_delivery_once(database) is True
assert await process_handoff_delivery_once(database) is False
assert calls == 1
assert database.handoff_delivery_jobs.job["status"] == STATUS_AMBIGUOUS

View file

@ -1,41 +1,45 @@
import pytest
from app.services.handoff_email_templates import (
build_mirror_handoff_html,
build_mirror_handoff_text,
external_reply_subject,
format_sender_line,
handoff_email_subject,
)
from app.services.handoff_mirror import load_mirror_payload
from app.services.html_sanitizer import sanitize_email_html
from app.services.mime_body_extractor import extract_mirror_content
from app.services.mime_body_extractor import (
extract_mirror_content,
extract_new_reply_mime_content,
)
from app.services.thread_service import threading_fields_from_parent
def test_mirror_template_is_minimal():
html = build_mirror_handoff_html(
sender_line="091rabbytes@gmail.com",
external_subject="lets try one more time",
original_html="<p>one last attempt. i am tired</p>",
agent_email="suyash@gentic.email",
@pytest.mark.asyncio
async def test_mirror_body_is_unchanged_without_gentic_wrapper():
payload = await load_mirror_payload(
{
"body_text": "one last attempt. i am tired",
"body_html": "<p><strong>one last attempt.</strong> i am tired</p>",
}
)
assert "LATEST MESSAGE" not in html
assert "Why you" not in html
assert "gentic-handoff-original" in html
assert "one last attempt. i am tired" in html
assert "✦ Human Handoff" in html
assert "091rabbytes@gmail.com" in html
assert "lets try one more time" in html
assert payload.body_text == "one last attempt. i am tired"
assert payload.body_html == (
"<p><strong>one last attempt.</strong> i am tired</p>"
)
assert "Human Handoff" not in payload.body_text
assert "gentic-handoff" not in (payload.body_html or "")
def test_mirror_plain_text_template():
text = build_mirror_handoff_text(
sender_line="091rabbytes@gmail.com",
external_subject="lets try one more time",
original_body_text="one last attempt. i am tired",
agent_email="suyash@gentic.email",
@pytest.mark.asyncio
async def test_mirror_plain_text_fallback_is_unchanged():
payload = await load_mirror_payload(
{
"body_text": "one last attempt. i am tired",
"body_html": None,
}
)
assert "one last attempt. i am tired" in text
assert "LATEST MESSAGE" not in text
assert "Reply normally" in text
assert payload.body_text == "one last attempt. i am tired"
assert payload.body_html is None
def test_format_sender_with_name():
@ -83,3 +87,42 @@ def test_extract_html_from_multipart():
extracted = extract_mirror_content(raw)
assert "Invoice" in (extracted.body_html or "")
assert "<table>" in (extracted.body_html or "")
def test_rich_human_reply_keeps_new_html_and_removes_private_quote():
raw = (
b"From: owner@example.com\r\n"
b"To: agent@gentic.email\r\n"
b"Subject: Re: Finance handoff\r\n"
b'MIME-Version: 1.0\r\nContent-Type: multipart/alternative; boundary="b"\r\n\r\n'
b"--b\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n"
b"Approved with urgency.\r\n\r\n"
b"On Tue, Oct 6, 2026 at 6:00 AM agent@gentic.email wrote:\r\n"
b"> private mirror\r\n"
b"--b\r\nContent-Type: text/html; charset=utf-8\r\n\r\n"
b'<div><strong>Approved</strong> with <em>urgency</em>.</div>'
b'<div class="gmail_quote">private mirror</div>\r\n'
b"--b--\r\n"
)
extracted = extract_new_reply_mime_content(raw)
assert extracted.body_text == "Approved with urgency."
assert "<strong>Approved</strong>" in (extracted.body_html or "")
assert "private mirror" not in (extracted.body_html or "")
def test_rich_human_reply_keeps_new_attachment():
raw = (
b"From: owner@example.com\r\n"
b"To: agent@gentic.email\r\n"
b"MIME-Version: 1.0\r\n"
b'Content-Type: multipart/mixed; boundary="m"\r\n\r\n'
b"--m\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nApproved\r\n"
b"--m\r\nContent-Type: application/pdf\r\n"
b'Content-Disposition: attachment; filename="approval.pdf"\r\n\r\nPDF\r\n'
b"--m--\r\n"
)
extracted = extract_new_reply_mime_content(raw)
assert extracted.body_text == "Approved"
assert [part.filename for part in extracted.file_attachments] == [
"approval.pdf"
]

View file

@ -1,11 +1,13 @@
from bson import ObjectId
from app.services.handoff_mirror_policy import (
GENERATED_BY_AGENT,
GENERATED_BY_CUSTOMER,
GENERATED_BY_HANDOFF_BRIDGE,
GENERATED_BY_RELAY_ACK,
MESSAGE_ORIGIN_EXTERNAL_CUSTOMER,
MESSAGE_ORIGIN_HUMAN_OWNER,
MESSAGE_ORIGIN_AGENT,
is_relay_acknowledgment_message,
should_mirror_to_human,
)
@ -40,6 +42,27 @@ def test_should_not_mirror_human_origin():
assert should_mirror_to_human(message) is False
def test_should_mirror_agent_authored_external_reply():
message = {
"message_scope": "EXTERNAL",
"direction": "outbound",
"generated_by": GENERATED_BY_AGENT,
"message_origin": MESSAGE_ORIGIN_AGENT,
}
assert should_mirror_to_human(message) is True
def test_should_not_echo_human_relay_back_to_owner():
message = {
"message_scope": "EXTERNAL",
"direction": "outbound",
"generated_by": GENERATED_BY_HANDOFF_BRIDGE,
"message_origin": MESSAGE_ORIGIN_HUMAN_OWNER,
"generated_from_handoff_message_id": ObjectId(),
}
assert should_mirror_to_human(message) is False
def test_is_relay_acknowledgment_detects_legacy_body():
message = {
"direction": "outbound",

View file

@ -0,0 +1,423 @@
from datetime import datetime, timezone
from types import SimpleNamespace
import pytest
from bson import ObjectId
from app.services.handoff_delivery import enqueue_handoff_delivery
from app.services.handoff_mirror_policy import (
GENERATED_BY_AGENT,
GENERATED_BY_HANDOFF_BRIDGE,
MESSAGE_ORIGIN_AGENT,
MESSAGE_ORIGIN_HUMAN_OWNER,
RELAY_STATUS_BLOCKED,
should_mirror_to_human,
)
from app.services import human_handoff_service
from app.services.human_handoff_service import (
MESSAGE_SCOPE_EXTERNAL,
MESSAGE_SCOPE_HANDOFF,
build_handoff_subject,
maybe_trigger_handoff,
process_human_reply,
resolve_handoff_from_inbound,
)
from app.services.mailbox_scope import apply_external_scope
from app.services.external_reply_service import build_external_reply_plan
class FakeCursor:
def __init__(self, items):
self.items = items
def sort(self, *_args, **_kwargs):
return self
async def to_list(self, length=None):
return list(self.items)[:length]
class FakeCollection:
def __init__(self, items=None):
self.items = list(items or [])
self.inserted = []
self.updated = []
async def find_one(self, query, projection=None):
for item in self.items:
if _matches(item, query):
return item
return None
def find(self, query=None):
query = query or {}
return FakeCursor([item for item in self.items if _matches(item, query)])
async def insert_one(self, document):
document = dict(document)
document.setdefault("_id", ObjectId())
self.items.append(document)
self.inserted.append(document)
return SimpleNamespace(inserted_id=document["_id"])
async def update_one(self, query, update, upsert=False):
self.updated.append((query, update, upsert))
existing = await self.find_one(query)
if existing is None and upsert:
inserted = {"_id": ObjectId(), **query, **update.get("$setOnInsert", {})}
self.items.append(inserted)
return SimpleNamespace(upserted_id=inserted["_id"], modified_count=0)
if existing is not None:
existing.update(update.get("$set", {}))
return SimpleNamespace(upserted_id=None, modified_count=1)
return SimpleNamespace(upserted_id=None, modified_count=0)
async def find_one_and_update(self, query, update, **kwargs):
existing = await self.find_one(query)
if existing is None:
return None
existing.update(update.get("$set", {}))
return existing
def _matches(document, query):
for key, expected in query.items():
if key == "$or":
if not any(_matches(document, clause) for clause in expected):
return False
continue
if key == "$ne":
continue
actual = document.get(key)
if isinstance(expected, dict):
if "$ne" in expected and actual == expected["$ne"]:
return False
if "$in" in expected and actual not in expected["$in"]:
return False
continue
if actual != expected:
return False
return True
def test_external_mailbox_filter_excludes_handoff_scope():
match = {"account_id": "account-1", "mailbox": "inbox"}
apply_external_scope(match)
assert match["$or"] == [
{"message_scope": "EXTERNAL"},
{"message_scope": {"$exists": False}},
]
def test_bridge_plan_never_references_private_ids():
customer = {
"_id": ObjectId(),
"internet_message_id": "<A@gmail.com>",
"message_id": "<A@gmail.com>",
"message_scope": MESSAGE_SCOPE_EXTERNAL,
"references": [],
"subject": "Invoice 42",
}
private_id = "<H1@gentic.email>"
plan = build_external_reply_plan(
parent_external_message=customer,
body_text="Approved",
recipients=["customer@gmail.com"],
)
assert plan.threading.in_reply_to == "<A@gmail.com>"
assert private_id not in plan.threading.references
assert plan.subject == "Re: Invoice 42"
assert "handoff" not in plan.subject.lower()
@pytest.mark.asyncio
async def test_existing_handoff_is_sticky_without_reevaluating_rules():
external_thread_id = ObjectId()
source_id = ObjectId()
handoff = {
"_id": ObjectId(),
"account_id": "account-1",
"external_thread_id": external_thread_id,
"status": "ACTIVE",
"rule_name": "Finance approvals",
}
database = SimpleNamespace(
human_handoffs=FakeCollection([handoff]),
human_handoff_rules=FakeCollection(
[
{
"_id": ObjectId(),
"name": "Finance approvals",
"priority": 1,
"labels": ["Finance"],
"email_types": ["transactional"],
"instructions": "only invoices",
"active": True,
"created_at": datetime.now(timezone.utc),
}
]
),
handoff_delivery_jobs=FakeCollection(),
)
message = {
"_id": source_id,
"message_scope": MESSAGE_SCOPE_EXTERNAL,
"direction": "inbound",
"sender": {"address": "customer@example.com"},
"subject": "Hello there",
"body_text": "just checking in",
"snippet": "just checking in",
}
result = await maybe_trigger_handoff(
database,
account_id="account-1",
agent_email="agent@gentic.email",
external_message=message,
external_thread_id=external_thread_id,
)
assert result["_id"] == handoff["_id"]
assert database.handoff_delivery_jobs.items
assert (
database.handoff_delivery_jobs.items[0]["kind"] == "MIRROR_TO_HUMAN"
)
@pytest.mark.asyncio
async def test_duplicate_delivery_jobs_share_an_idempotency_key():
database = SimpleNamespace(handoff_delivery_jobs=FakeCollection())
handoff_id = ObjectId()
source_id = ObjectId()
first = await enqueue_handoff_delivery(
database,
kind="MIRROR_TO_HUMAN",
account_id="account-1",
handoff_id=handoff_id,
source_message_id=source_id,
agent_email="agent@gentic.email",
)
second = await enqueue_handoff_delivery(
database,
kind="MIRROR_TO_HUMAN",
account_id="account-1",
handoff_id=handoff_id,
source_message_id=source_id,
agent_email="agent@gentic.email",
)
assert first["idempotency_key"] == second["idempotency_key"]
assert len(database.handoff_delivery_jobs.items) == 1
@pytest.mark.asyncio
async def test_human_reply_resolves_only_to_private_handoff():
parent_id = ObjectId()
thread_id = ObjectId()
handoff = {
"_id": ObjectId(),
"account_id": "account-1",
"handoff_thread_id": thread_id,
"human_email": "owner@example.com",
"agent_email": "agent@gentic.email",
"status": "ACTIVE",
}
database = SimpleNamespace(
messages=FakeCollection(
[
{
"_id": parent_id,
"message_scope": MESSAGE_SCOPE_HANDOFF,
"message_id": "<H1@gentic.email>",
"message_id_folded": "<h1@gentic.email>",
}
]
),
mailbox_entries=FakeCollection(
[
{
"account_id": "account-1",
"message_id": parent_id,
"message_scope": MESSAGE_SCOPE_HANDOFF,
"thread_id": thread_id,
}
]
),
human_handoffs=FakeCollection([handoff]),
)
resolved = await resolve_handoff_from_inbound(
database,
account_id="account-1",
agent_email="agent@gentic.email",
sender_address="owner@example.com",
in_reply_to="<H1@gentic.email>",
references=["<H1@gentic.email>"],
)
assert resolved["_id"] == handoff["_id"]
def test_human_echo_and_agent_mirror_policy():
assert should_mirror_to_human(
{
"message_scope": MESSAGE_SCOPE_EXTERNAL,
"direction": "outbound",
"generated_by": GENERATED_BY_HANDOFF_BRIDGE,
"message_origin": MESSAGE_ORIGIN_HUMAN_OWNER,
"generated_from_handoff_message_id": ObjectId(),
}
) is False
assert should_mirror_to_human(
{
"message_scope": MESSAGE_SCOPE_EXTERNAL,
"direction": "outbound",
"generated_by": GENERATED_BY_AGENT,
"message_origin": MESSAGE_ORIGIN_AGENT,
}
) is True
def test_private_subject_stays_off_the_external_plane():
private = build_handoff_subject(
rule_name="Finance approvals",
external_sender="customer@example.com",
external_subject="Invoice 42",
)
assert private.startswith("Finance approvals · handoff ·")
assert "Invoice 42" in private
@pytest.mark.asyncio
async def test_first_matching_rule_creates_a_new_linked_handoff():
external_thread_id = ObjectId()
source_id = ObjectId()
rule = {
"_id": ObjectId(),
"account_id": "account-1",
"name": "Finance approvals",
"priority": 1,
"owner_email": "owner@example.com",
"owner_name": "Owner",
"labels": ["All mail"],
"email_types": ["all"],
"instructions": "",
"active": True,
"allow_replies": True,
"created_at": datetime.now(timezone.utc),
}
database = SimpleNamespace(
human_handoffs=FakeCollection(),
human_handoff_rules=FakeCollection([rule]),
handoff_delivery_jobs=FakeCollection(),
email_threads=FakeCollection(),
)
message = {
"_id": source_id,
"message_scope": MESSAGE_SCOPE_EXTERNAL,
"direction": "inbound",
"sender": {"address": "customer@example.com"},
"subject": "Invoice 42",
"body_text": "please review this invoice",
"snippet": "please review this invoice",
}
result = await maybe_trigger_handoff(
database,
account_id="account-1",
agent_email="agent@gentic.email",
external_message=message,
external_thread_id=external_thread_id,
)
assert result["rule_name"] == "Finance approvals"
assert result["external_thread_id"] == external_thread_id
assert result["handoff_thread_id"]
assert result["private_subject"].startswith("Finance approvals · handoff ·")
assert database.handoff_delivery_jobs.items[0]["kind"] == "MIRROR_TO_HUMAN"
@pytest.mark.asyncio
async def test_maybe_trigger_does_not_echo_human_origin_messages():
database = SimpleNamespace(
human_handoffs=FakeCollection(
[
{
"_id": ObjectId(),
"account_id": "account-1",
"external_thread_id": ObjectId(),
"status": "ACTIVE",
}
]
),
handoff_delivery_jobs=FakeCollection(),
)
result = await maybe_trigger_handoff(
database,
account_id="account-1",
agent_email="agent@gentic.email",
external_message={
"_id": ObjectId(),
"message_scope": MESSAGE_SCOPE_EXTERNAL,
"direction": "outbound",
"generated_by": GENERATED_BY_HANDOFF_BRIDGE,
"message_origin": MESSAGE_ORIGIN_HUMAN_OWNER,
"generated_from_handoff_message_id": ObjectId(),
"sender": {"address": "agent@gentic.email"},
},
external_thread_id=database.human_handoffs.items[0]["external_thread_id"],
)
assert result is None
assert database.handoff_delivery_jobs.items == []
@pytest.mark.asyncio
async def test_leaked_private_subject_blocks_human_relay(monkeypatch):
stored = {}
async def store(_database, **kwargs):
stored.update(kwargs)
return {"_id": ObjectId(), **kwargs}
enqueued = []
async def enqueue(*args, **kwargs):
enqueued.append(kwargs)
return {}
class BrokenStorage:
async def get(self, *_args, **_kwargs):
raise RuntimeError("raw mime unavailable")
monkeypatch.setattr(human_handoff_service, "store_handoff_message", store)
monkeypatch.setattr(human_handoff_service, "enqueue_handoff_delivery", enqueue)
monkeypatch.setattr(human_handoff_service, "get_raw_mime_storage", lambda: BrokenStorage())
database = SimpleNamespace(messages=FakeCollection(), human_handoffs=FakeCollection())
await process_human_reply(
database,
account_id="account-1",
agent_email="agent@gentic.email",
handoff={
"_id": ObjectId(),
"handoff_thread_id": ObjectId(),
"allow_replies": True,
},
parsed={
"sender": {"name": "Owner", "address": "owner@example.com"},
"to": [{"address": "agent@gentic.email"}],
"subject": "Re: Finance approvals · handoff · customer@example.com · Invoice 42",
"message_id": "<H2@gmail.com>",
"in_reply_to": "<H1@gentic.email>",
"references": ["<H1@gentic.email>"],
"attachments": [],
"raw_headers": {},
},
body_text=(
"Finance approvals · handoff · customer@example.com · Invoice 42\n"
"Please approve"
),
body_html=None,
raw_mime={"bucket": "mail", "name": "raw.eml"},
delivery_key="delivery-1",
)
assert stored["relay_status"] == RELAY_STATUS_BLOCKED
assert enqueued == []

View file

@ -1,13 +1,74 @@
from app.services.human_handoff_service import MESSAGE_SCOPE_EXTERNAL, MESSAGE_SCOPE_HANDOFF
from app.services.human_handoff_service import (
MESSAGE_SCOPE_EXTERNAL,
MESSAGE_SCOPE_HANDOFF,
build_handoff_subject,
maybe_trigger_handoff,
process_human_reply,
redact_human_address,
resolve_handoff_from_inbound,
select_matching_handoff_rule,
)
def test_scope_constants_are_distinct():
assert MESSAGE_SCOPE_EXTERNAL != MESSAGE_SCOPE_HANDOFF
def test_human_handoff_runtime_is_disabled():
import app.services.human_handoff_service as module
def test_human_handoff_runtime_is_enabled():
assert callable(maybe_trigger_handoff)
assert callable(process_human_reply)
assert callable(resolve_handoff_from_inbound)
assert not hasattr(module, "maybe_trigger_handoff")
assert not hasattr(module, "process_human_reply")
assert not hasattr(module, "resolve_handoff_from_inbound")
def test_private_subject_contains_rule_sender_and_external_subject():
subject = build_handoff_subject(
rule_name="Finance approvals",
external_sender="customer@example.com",
external_subject="Invoice 42",
)
assert subject == (
"Finance approvals · handoff · customer@example.com · Invoice 42"
)
def test_private_subject_removes_header_line_breaks():
subject = build_handoff_subject(
rule_name="Urgent\r\nBcc: attacker@example.com",
external_sender="customer@example.com",
external_subject="Help",
)
assert "\r" not in subject
assert "\n" not in subject
def test_human_address_is_replaced_before_external_relay():
value = "Regards,\nOwner <private.owner@example.com>"
assert redact_human_address(
value,
human_email="private.owner@example.com",
agent_email="agent@gentic.email",
) == "Regards,\nOwner <agent@gentic.email>"
def test_lowest_priority_matching_rule_wins():
rules = [
{
"name": "Backup",
"priority": 20,
"labels": ["All mail"],
"email_types": ["all"],
"instructions": "",
},
{
"name": "Primary",
"priority": 1,
"labels": ["All mail"],
"email_types": ["all"],
"instructions": "",
},
]
selected = select_matching_handoff_rule(
rules,
email={"subject": "Hello", "body_text": "Need help"},
)
assert selected["name"] == "Primary"

View file

@ -22,6 +22,7 @@ class FakeMessages:
async def update_one(self, *args, **kwargs):
self.updates += 1
return SimpleNamespace(upserted_id=ObjectId(), modified_count=0)
async def find_one(self, *args, **kwargs):
return {"_id": self.id}
@ -33,6 +34,12 @@ class FakeEntries:
async def update_one(self, *args, **kwargs):
self.updates += 1
return SimpleNamespace(upserted_id=ObjectId(), modified_count=0)
class FakeThreads:
async def update_one(self, *args, **kwargs):
return None
class FakeStorage:
@ -84,7 +91,7 @@ async def test_rcpt_rejects_unknown_account(monkeypatch):
async def test_rcpt_accepts_account_and_tracks_owner(monkeypatch):
account = SimpleNamespace(id="account-1")
monkeypatch.setattr(inbound_smtp, "SessionFactory", lambda: FakeSession(account))
target = SimpleNamespace(rcpt_tos=[], rcpt_options=[])
target = SimpleNamespace(rcpt_tos=[], rcpt_options=[], mail_from="sender@example.com")
response = await inbound_smtp.InboundHandler().handle_RCPT(
None,
@ -95,8 +102,8 @@ async def test_rcpt_accepts_account_and_tracks_owner(monkeypatch):
)
assert response.startswith("250 2.1.5")
assert target.rcpt_tos == ["user@gentic.email"]
assert target.account_ids == {"user@gentic.email": "account-1"}
assert target.rcpt_tos == ["user@getall.help"]
assert target.account_ids == {"user@getall.help": "account-1"}
@pytest.mark.asyncio
@ -106,15 +113,98 @@ async def test_data_returns_success_only_after_idempotent_records(monkeypatch):
monkeypatch.setattr(
inbound_smtp,
"mongo_db",
SimpleNamespace(messages=messages, mailbox_entries=entries),
SimpleNamespace(
messages=messages,
mailbox_entries=entries,
email_threads=FakeThreads(),
),
)
monkeypatch.setattr(inbound_smtp, "get_raw_mime_storage", lambda: FakeStorage())
async def no_handoff(*args, **kwargs):
return None
monkeypatch.setattr(inbound_smtp, "resolve_handoff_from_inbound", no_handoff)
monkeypatch.setattr(inbound_smtp, "maybe_trigger_handoff", no_handoff)
async def ensure_thread(*args, **kwargs):
return ObjectId()
monkeypatch.setattr(inbound_smtp, "ensure_thread_for_message", ensure_thread)
response = await inbound_smtp.InboundHandler().handle_DATA(None, None, envelope())
assert response.startswith("250 2.0.0")
assert messages.updates == 1
assert entries.updates == 1
assert messages.updates >= 1
assert entries.updates >= 1
@pytest.mark.asyncio
async def test_human_reply_stays_on_the_private_plane(monkeypatch):
messages = FakeMessages()
entries = FakeEntries()
monkeypatch.setattr(
inbound_smtp,
"mongo_db",
SimpleNamespace(
messages=messages,
mailbox_entries=entries,
email_threads=FakeThreads(),
),
)
monkeypatch.setattr(inbound_smtp, "get_raw_mime_storage", lambda: FakeStorage())
async def resolve(*args, **kwargs):
return {"_id": ObjectId()}
processed = []
async def process(*args, **kwargs):
processed.append(kwargs)
return {"_id": ObjectId()}
monkeypatch.setattr(inbound_smtp, "resolve_handoff_from_inbound", resolve)
monkeypatch.setattr(inbound_smtp, "process_human_reply", process)
response = await inbound_smtp.InboundHandler().handle_DATA(None, None, envelope())
assert response.startswith("250 2.0.0 Handoff message accepted")
assert processed
assert messages.updates == 0
assert entries.updates == 0
@pytest.mark.asyncio
async def test_data_still_accepted_when_handoff_trigger_fails(monkeypatch):
messages = FakeMessages()
entries = FakeEntries()
monkeypatch.setattr(
inbound_smtp,
"mongo_db",
SimpleNamespace(
messages=messages,
mailbox_entries=entries,
email_threads=FakeThreads(),
),
)
monkeypatch.setattr(inbound_smtp, "get_raw_mime_storage", lambda: FakeStorage())
async def no_handoff(*args, **kwargs):
return None
async def boom(*args, **kwargs):
raise RuntimeError("mirror queue unavailable")
monkeypatch.setattr(inbound_smtp, "resolve_handoff_from_inbound", no_handoff)
monkeypatch.setattr(inbound_smtp, "maybe_trigger_handoff", boom)
async def ensure_thread(*args, **kwargs):
return ObjectId()
monkeypatch.setattr(inbound_smtp, "ensure_thread_for_message", ensure_thread)
response = await inbound_smtp.InboundHandler().handle_DATA(None, None, envelope())
assert response.startswith("250 2.0.0")
assert messages.updates >= 1
@pytest.mark.asyncio

View file

@ -1,7 +1,49 @@
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from app.services.mail_sender import build_message, send_via_smtp, sign_message
import pytest
from app.services.mail_sender import build_message, deliver_email, send_via_smtp, sign_message
def test_build_message_formats_named_sender_header():
message = build_message(
sender="suyash@gentic.email",
sender_name="Suyash Dixit",
recipients=["recipient@example.com"],
cc=[],
subject="Named sender",
body_text="Hello",
body_html=None,
)
assert message["From"] == "Suyash Dixit <suyash@gentic.email>"
def test_build_message_preserves_preformatted_transactional_sender():
message = build_message(
sender="aGentic Email <notifications@gentic.email>",
recipients=["recipient@example.com"],
cc=[],
subject="Welcome",
body_text="Hello",
body_html=None,
)
assert message["From"] == "aGentic Email <notifications@gentic.email>"
def test_build_message_adds_safe_fallback_sender_name():
message = build_message(
sender="notifications@gentic.email",
recipients=["recipient@example.com"],
cc=[],
subject="Notification",
body_text="Hello",
body_html=None,
)
assert message["From"] == "aGentic Email <notifications@gentic.email>"
def test_dkim_sign_uses_selector_before_domain():
@ -29,6 +71,7 @@ def test_dkim_sign_uses_selector_before_domain():
assert signer.call_args.args[2] == b"ritoesis.com"
@pytest.mark.asyncio
async def test_send_via_smtp_uses_configured_client_hostname():
with patch(
"app.services.mail_sender.aiosmtplib.send",
@ -43,6 +86,38 @@ async def test_send_via_smtp_uses_configured_client_hostname():
assert send_mock.await_args.kwargs["local_hostname"] == "backend.gentic.email"
@pytest.mark.asyncio
async def test_deliver_email_uses_bare_address_for_smtp_envelope():
domain = SimpleNamespace(
domain_name="gentic.email",
dkim_selector="agmail",
dkim_private_key="private-key",
)
with (
patch(
"app.services.mail_sender.sign_message",
return_value=b"signed",
),
patch(
"app.services.mail_sender.send_via_smtp",
new_callable=AsyncMock,
) as send_mock,
):
await deliver_email(
sender="notifications@gentic.email",
sender_name="aGentic Email",
recipients=["user@example.com"],
cc=[],
bcc=[],
subject="Welcome",
body_text="Hello",
body_html=None,
domain=domain,
)
assert send_mock.await_args.kwargs["sender"] == "notifications@gentic.email"
def test_build_message_sets_in_reply_to_and_references():
message = build_message(
sender="suyash@gentic.email",

View file

@ -99,6 +99,26 @@ def test_html_reply_preserves_nested_quoted_history():
assert "> > sender: C" in result["quoted_content"]
def test_apple_mail_attribution_is_treated_as_quote():
body = """Approved.
On 6 Oct 2026, at 04:48, Suyash Dixit <suyash@gentic.email> wrote:
sender: C"""
result = extract_new_reply_content(body_text=body)
assert result["new_reply_content"] == "Approved."
assert "sender: C" in result["quoted_content"]
def test_private_handoff_subject_fingerprint_is_blocked():
from app.services.reply_extractor import validate_external_reply_body
leaked = (
"Finance approvals · handoff · customer@example.com · Invoice 42\n"
"Please approve"
)
assert validate_external_reply_body(leaked) is False
def test_outlook_original_message_split():
body = """Thanks for the update.