Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 159 additions & 16 deletions gmail/gmail_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,62 @@ def _extract_headers(payload: dict, header_names: List[str]) -> Dict[str, str]:
return headers


def _prepare_gmail_message(
subject: str,
body: str,
to: Optional[str] = None,
cc: Optional[str] = None,
bcc: Optional[str] = None,
thread_id: Optional[str] = None,
in_reply_to: Optional[str] = None,
references: Optional[str] = None,
) -> tuple[str, Optional[str]]:
Copy link

Copilot AI Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Tuple from typing instead of the built-in tuple for better compatibility with older Python versions. Import Tuple from typing and change to -> Tuple[str, Optional[str]]:

Suggested change
) -> tuple[str, Optional[str]]:
) -> Tuple[str, Optional[str]]:

Copilot uses AI. Check for mistakes.
"""
Prepare a Gmail message with threading support.

Args:
subject: Email subject
body: Email body (plain text)
to: Optional recipient email address
cc: Optional CC email address
bcc: Optional BCC email address
thread_id: Optional Gmail thread ID to reply within
in_reply_to: Optional Message-ID of the message being replied to
references: Optional chain of Message-IDs for proper threading

Returns:
Tuple of (raw_message, thread_id) where raw_message is base64 encoded
"""
# Handle reply subject formatting
reply_subject = subject
if in_reply_to and not subject.lower().startswith('re:'):
Copy link

Copilot AI Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition checks for 'Re:' prefix but should handle variations like 'RE:', 'Re:', 'rE:', etc. Consider using a case-insensitive regex or normalizing the check: if in_reply_to and not re.match(r'^re:\s*', subject, re.IGNORECASE):

Suggested change
if in_reply_to and not subject.lower().startswith('re:'):
if in_reply_to and not re.match(r'^\s*re:\s*', subject, re.IGNORECASE):

Copilot uses AI. Check for mistakes.
reply_subject = f"Re: {subject}"

# Prepare the email
message = MIMEText(body)
message["subject"] = reply_subject

# Add recipients if provided
if to:
message["to"] = to
if cc:
message["cc"] = cc
if bcc:
message["bcc"] = bcc

# Add reply headers for threading
if in_reply_to:
message["In-Reply-To"] = in_reply_to

if references:
message["References"] = references

# Encode message
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()

return raw_message, thread_id


def _generate_gmail_web_url(item_id: str, account_index: int = 0) -> str:
"""
Generate Gmail web interface URL for a message or thread ID.
Expand Down Expand Up @@ -450,25 +506,73 @@ async def send_gmail_message(
to: str = Body(..., description="Recipient email address."),
subject: str = Body(..., description="Email subject."),
body: str = Body(..., description="Email body (plain text)."),
cc: Optional[str] = Body(None, description="Optional CC email address."),
bcc: Optional[str] = Body(None, description="Optional BCC email address."),
thread_id: Optional[str] = Body(None, description="Optional Gmail thread ID to reply within."),
in_reply_to: Optional[str] = Body(None, description="Optional Message-ID of the message being replied to."),
references: Optional[str] = Body(None, description="Optional chain of Message-IDs for proper threading."),
) -> str:
"""
Sends an email using the user's Gmail account.
Sends an email using the user's Gmail account. Supports both new emails and replies.

Args:
to (str): Recipient email address.
subject (str): Email subject.
body (str): Email body (plain text).
cc (Optional[str]): Optional CC email address.
bcc (Optional[str]): Optional BCC email address.
user_google_email (str): The user's Google email address. Required.
thread_id (Optional[str]): Optional Gmail thread ID to reply within. When provided, sends a reply.
in_reply_to (Optional[str]): Optional Message-ID of the message being replied to. Used for proper threading.
references (Optional[str]): Optional chain of Message-IDs for proper threading. Should include all previous Message-IDs.

Returns:
str: Confirmation message with the sent email's message ID.

Examples:
# Send a new email
send_gmail_message(to="user@example.com", subject="Hello", body="Hi there!")

# Send an email with CC and BCC
send_gmail_message(
to="user@example.com",
cc="manager@example.com",
bcc="archive@example.com",
subject="Project Update",
body="Here's the latest update..."
)

# Send a reply
send_gmail_message(
to="user@example.com",
subject="Re: Meeting tomorrow",
body="Thanks for the update!",
thread_id="thread_123",
in_reply_to="<message123@gmail.com>",
references="<original@gmail.com> <message123@gmail.com>"
)
"""
# Prepare the email
message = MIMEText(body)
message["to"] = to
message["subject"] = subject
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
logger.info(
f"[send_gmail_message] Invoked. Email: '{user_google_email}', Subject: '{subject}'"
)

# Prepare the email message
raw_message, thread_id_final = _prepare_gmail_message(
subject=subject,
body=body,
to=to,
cc=cc,
bcc=bcc,
thread_id=thread_id,
in_reply_to=in_reply_to,
references=references,
)

send_body = {"raw": raw_message}

# Associate with thread if provided
if thread_id_final:
send_body["threadId"] = thread_id_final
Comment on lines +574 to +575
Copy link

Copilot AI Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable thread_id_final is always the same as the input thread_id parameter since _prepare_gmail_message returns it unchanged. This check should use thread_id directly instead of thread_id_final.

Suggested change
if thread_id_final:
send_body["threadId"] = thread_id_final
if thread_id:
send_body["threadId"] = thread_id

Copilot uses AI. Check for mistakes.

# Send the message
sent_message = await asyncio.to_thread(
Expand All @@ -487,35 +591,74 @@ async def draft_gmail_message(
subject: str = Body(..., description="Email subject."),
body: str = Body(..., description="Email body (plain text)."),
to: Optional[str] = Body(None, description="Optional recipient email address."),
cc: Optional[str] = Body(None, description="Optional CC email address."),
bcc: Optional[str] = Body(None, description="Optional BCC email address."),
thread_id: Optional[str] = Body(None, description="Optional Gmail thread ID to reply within."),
in_reply_to: Optional[str] = Body(None, description="Optional Message-ID of the message being replied to."),
references: Optional[str] = Body(None, description="Optional chain of Message-IDs for proper threading."),
) -> str:
"""
Creates a draft email in the user's Gmail account.
Creates a draft email in the user's Gmail account. Supports both new drafts and reply drafts.

Args:
user_google_email (str): The user's Google email address. Required.
subject (str): Email subject.
body (str): Email body (plain text).
to (Optional[str]): Optional recipient email address. Can be left empty for drafts.
cc (Optional[str]): Optional CC email address.
bcc (Optional[str]): Optional BCC email address.
thread_id (Optional[str]): Optional Gmail thread ID to reply within. When provided, creates a reply draft.
in_reply_to (Optional[str]): Optional Message-ID of the message being replied to. Used for proper threading.
references (Optional[str]): Optional chain of Message-IDs for proper threading. Should include all previous Message-IDs.

Returns:
str: Confirmation message with the created draft's ID.

Examples:
# Create a new draft
draft_gmail_message(subject="Hello", body="Hi there!", to="user@example.com")

# Create a draft with CC and BCC
draft_gmail_message(
subject="Project Update",
body="Here's the latest update...",
to="user@example.com",
cc="manager@example.com",
bcc="archive@example.com"
)

# Create a reply draft
draft_gmail_message(
subject="Re: Meeting tomorrow",
body="Thanks for the update!",
to="user@example.com",
thread_id="thread_123",
in_reply_to="<message123@gmail.com>",
references="<original@gmail.com> <message123@gmail.com>"
)
"""
logger.info(
f"[draft_gmail_message] Invoked. Email: '{user_google_email}', Subject: '{subject}'"
)

# Prepare the email
message = MIMEText(body)
message["subject"] = subject

# Add recipient if provided
if to:
message["to"] = to

raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
# Prepare the email message
raw_message, thread_id_final = _prepare_gmail_message(
subject=subject,
body=body,
to=to,
cc=cc,
bcc=bcc,
thread_id=thread_id,
in_reply_to=in_reply_to,
references=references,
)

# Create a draft instead of sending
draft_body = {"message": {"raw": raw_message}}

# Associate with thread if provided
if thread_id_final:
draft_body["message"]["threadId"] = thread_id_final
Comment on lines +660 to +661
Copy link

Copilot AI Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as in send_gmail_message - thread_id_final is always identical to the input thread_id. Use thread_id directly instead of thread_id_final.

Suggested change
if thread_id_final:
draft_body["message"]["threadId"] = thread_id_final
if thread_id:
draft_body["message"]["threadId"] = thread_id

Copilot uses AI. Check for mistakes.

# Create the draft
created_draft = await asyncio.to_thread(
Expand Down