feat(gmail): auto-populate In-Reply-To/References when thread_id is provided

When draft_gmail_message is called with a thread_id but without
in_reply_to or references headers, fetch the thread via the Gmail API
to extract Message-ID headers. This ensures reply drafts render inline
in Gmail's thread view instead of appearing as ghost drafts.

Fixes #555
This commit is contained in:
Bortlesboat
2026-03-15 17:17:15 -04:00
parent 92b4a7847f
commit ebc3fcb044

View File

@@ -323,6 +323,53 @@ def _extract_headers(payload: dict, header_names: List[str]) -> Dict[str, str]:
return headers
async def _fetch_thread_message_ids(service, thread_id: str) -> tuple[Optional[str], Optional[str]]:
"""
Fetch Message-ID headers from a Gmail thread for reply threading.
Returns the last message's Message-ID (for In-Reply-To) and the full chain
of Message-IDs (for References).
Args:
service: Gmail API service instance
thread_id: Gmail thread ID
Returns:
Tuple of (last_message_id, references_chain) where both are strings or None
"""
try:
thread = await asyncio.to_thread(
service.users().threads().get(
userId="me",
id=thread_id,
format="metadata",
metadataHeaders=["Message-ID"],
).execute
)
messages = thread.get("messages", [])
if not messages:
return None, None
# Collect all Message-IDs in thread order
message_ids = []
for msg in messages:
headers = _extract_headers(msg.get("payload", {}), ["Message-ID"])
mid = headers.get("Message-ID")
if mid:
message_ids.append(mid)
if not message_ids:
return None, None
last_message_id = message_ids[-1]
references_chain = " ".join(message_ids)
return last_message_id, references_chain
except Exception as e:
logger.warning(f"Failed to fetch thread Message-IDs for thread {thread_id}: {e}")
return None, None
def _prepare_gmail_message(
subject: str,
body: str,
@@ -1484,6 +1531,17 @@ async def draft_gmail_message(
)
draft_body = _append_signature_to_body(draft_body, body_format, signature_html)
# Auto-populate In-Reply-To and References when thread_id is provided
# but headers are missing, to ensure the draft renders inline in Gmail
if thread_id and (not in_reply_to or not references):
fetched_reply_to, fetched_references = await _fetch_thread_message_ids(
service, thread_id
)
if not in_reply_to and fetched_reply_to:
in_reply_to = fetched_reply_to
if not references and fetched_references:
references = fetched_references
raw_message, thread_id_final, attached_count = _prepare_gmail_message(
subject=subject,
body=draft_body,