2025-05-12 17:22:58 -04:00
"""
Google Gmail MCP Tools
This module provides MCP tools for interacting with the Gmail API.
"""
2025-05-22 14:21:52 -04:00
2025-05-12 17:22:58 -04:00
import logging
import asyncio
import base64
2025-07-23 12:40:46 -04:00
import ssl
2026-01-23 02:32:58 +02:00
import mimetypes
from pathlib import Path
2025-12-19 15:58:45 -05:00
from html.parser import HTMLParser
2025-10-23 14:49:47 -04:00
from typing import Optional , List , Dict , Literal , Any
2025-05-12 17:22:58 -04:00
2025-05-19 20:57:11 -07:00
from email.mime.text import MIMEText
2026-01-23 02:32:58 +02:00
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
2026-01-30 10:33:49 -05:00
from email.utils import formataddr
2025-05-19 20:57:11 -07:00
2025-05-24 10:43:55 -04:00
from fastapi import Body
2025-08-13 12:04:07 -04:00
from pydantic import Field
2025-05-12 17:22:58 -04:00
2025-06-07 09:50:53 -04:00
from auth.service_decorator import require_google_service
2025-06-18 17:31:57 -04:00
from core.utils import handle_http_errors
2025-08-03 10:30:04 -04:00
from core.server import server
from auth.scopes import (
2025-05-12 17:22:58 -04:00
GMAIL_SEND_SCOPE ,
2025-05-22 00:02:51 +05:30
GMAIL_COMPOSE_SCOPE ,
2025-06-06 12:02:49 -04:00
GMAIL_MODIFY_SCOPE ,
GMAIL_LABELS_SCOPE ,
2025-05-12 17:22:58 -04:00
)
logger = logging . getLogger ( __name__ )
2025-07-23 14:01:02 -04:00
GMAIL_BATCH_SIZE = 25
GMAIL_REQUEST_DELAY = 0.1
2025-08-13 12:04:07 -04:00
HTML_BODY_TRUNCATE_LIMIT = 20000
2026-01-05 23:20:31 +00:00
GMAIL_METADATA_HEADERS = [ "Subject" , "From" , "To" , "Cc" , "Message-ID" , "Date" ]
2025-07-23 14:01:02 -04:00
2025-05-22 14:21:52 -04:00
2025-12-19 15:58:45 -05:00
class _HTMLTextExtractor ( HTMLParser ):
"""Extract readable text from HTML using stdlib."""
def __init__ ( self ):
super () . __init__ ()
self . _text = []
self . _skip = False
def handle_starttag ( self , tag , attrs ):
self . _skip = tag in ( "script" , "style" )
def handle_endtag ( self , tag ):
if tag in ( "script" , "style" ):
self . _skip = False
def handle_data ( self , data ):
if not self . _skip :
self . _text . append ( data )
def get_text ( self ) -> str :
return " " . join ( "" . join ( self . _text ) . split ())
def _html_to_text ( html : str ) -> str :
"""Convert HTML to readable plain text."""
try :
parser = _HTMLTextExtractor ()
parser . feed ( html )
return parser . get_text ()
except Exception :
return html
2025-05-24 00:08:19 +08:00
def _extract_message_body ( payload ):
"""
Helper function to extract plain text body from a Gmail message payload.
2025-08-13 00:14:26 -04:00
(Maintained for backward compatibility)
2025-05-24 00:08:19 +08:00
Args:
payload (dict): The message payload from Gmail API
Returns:
str: The plain text body content, or empty string if not found
"""
2025-08-13 00:14:26 -04:00
bodies = _extract_message_bodies ( payload )
return bodies . get ( "text" , "" )
def _extract_message_bodies ( payload ):
"""
Helper function to extract both plain text and HTML bodies from a Gmail message payload.
Args:
payload (dict): The message payload from Gmail API
Returns:
dict: Dictionary with 'text' and 'html' keys containing body content
"""
text_body = ""
html_body = ""
2025-05-24 00:08:19 +08:00
parts = [ payload ] if "parts" not in payload else payload . get ( "parts" , [])
part_queue = list ( parts ) # Use a queue for BFS traversal of parts
while part_queue :
part = part_queue . pop ( 0 )
2025-08-13 00:14:26 -04:00
mime_type = part . get ( "mimeType" , "" )
body_data = part . get ( "body" , {}) . get ( "data" )
2025-08-13 12:05:06 -04:00
2025-08-13 00:14:26 -04:00
if body_data :
try :
2025-12-13 13:49:28 -08:00
decoded_data = base64 . urlsafe_b64decode ( body_data ) . decode (
"utf-8" , errors = "ignore"
)
2025-08-13 00:14:26 -04:00
if mime_type == "text/plain" and not text_body :
text_body = decoded_data
elif mime_type == "text/html" and not html_body :
html_body = decoded_data
except Exception as e :
logger . warning ( f "Failed to decode body part: { e } " )
2025-08-13 12:05:06 -04:00
2025-08-13 00:14:26 -04:00
# Add sub-parts to queue for multipart messages
if mime_type . startswith ( "multipart/" ) and "parts" in part :
part_queue . extend ( part . get ( "parts" , []))
# Check the main payload if it has body data directly
if payload . get ( "body" , {}) . get ( "data" ):
try :
2025-12-13 13:49:28 -08:00
decoded_data = base64 . urlsafe_b64decode ( payload [ "body" ][ "data" ]) . decode (
"utf-8" , errors = "ignore"
)
2025-08-13 00:14:26 -04:00
mime_type = payload . get ( "mimeType" , "" )
if mime_type == "text/plain" and not text_body :
text_body = decoded_data
elif mime_type == "text/html" and not html_body :
html_body = decoded_data
except Exception as e :
logger . warning ( f "Failed to decode main payload body: { e } " )
2025-12-13 13:49:28 -08:00
return { "text" : text_body , "html" : html_body }
2025-05-24 00:08:19 +08:00
2025-08-13 12:04:07 -04:00
def _format_body_content ( text_body : str , html_body : str ) -> str :
"""
Helper function to format message body content with HTML fallback and truncation.
2025-12-19 15:58:45 -05:00
Detects useless text/plain fallbacks (e.g., "Your client does not support HTML").
2025-08-13 12:05:06 -04:00
2025-08-13 12:04:07 -04:00
Args:
text_body: Plain text body content
html_body: HTML body content
2025-08-13 12:05:06 -04:00
2025-08-13 12:04:07 -04:00
Returns:
Formatted body content string
"""
2025-12-19 15:58:45 -05:00
text_stripped = text_body . strip ()
html_stripped = html_body . strip ()
# Detect useless fallback: HTML comments in text, or HTML is 50x+ longer
use_html = html_stripped and (
2025-12-19 16:13:01 -05:00
not text_stripped
or "<!--" in text_stripped
or len ( html_stripped ) > len ( text_stripped ) * 50
2025-12-19 15:58:45 -05:00
)
if use_html :
2025-12-19 16:10:24 -05:00
content = _html_to_text ( html_stripped )
2025-12-19 15:58:45 -05:00
if len ( content ) > HTML_BODY_TRUNCATE_LIMIT :
content = content [: HTML_BODY_TRUNCATE_LIMIT ] + " \n\n [Content truncated...]"
return content
elif text_stripped :
2025-08-13 12:04:07 -04:00
return text_body
else :
return "[No readable content found]"
2025-10-23 14:49:47 -04:00
def _extract_attachments ( payload : dict ) -> List [ Dict [ str , Any ]]:
2025-10-15 16:12:52 -04:00
"""
Extract attachment metadata from a Gmail message payload.
Args:
payload: The message payload from Gmail API
Returns:
List of attachment dictionaries with filename, mimeType, size, and attachmentId
"""
attachments = []
def search_parts ( part ):
"""Recursively search for attachments in message parts"""
# Check if this part is an attachment
if part . get ( "filename" ) and part . get ( "body" , {}) . get ( "attachmentId" ):
2025-12-13 13:49:28 -08:00
attachments . append (
{
"filename" : part [ "filename" ],
"mimeType" : part . get ( "mimeType" , "application/octet-stream" ),
"size" : part . get ( "body" , {}) . get ( "size" , 0 ),
"attachmentId" : part [ "body" ][ "attachmentId" ],
}
)
2025-10-15 16:12:52 -04:00
# Recursively search sub-parts
if "parts" in part :
for subpart in part [ "parts" ]:
search_parts ( subpart )
# Start searching from the root payload
search_parts ( payload )
return attachments
2025-05-26 19:00:27 -04:00
def _extract_headers ( payload : dict , header_names : List [ str ]) -> Dict [ str , str ]:
"""
Extract specified headers from a Gmail message payload.
Args:
payload: The message payload from Gmail API
header_names: List of header names to extract
Returns:
Dict mapping header names to their values
"""
headers = {}
2025-12-23 11:25:29 +00:00
target_headers = { name . lower (): name for name in header_names }
2025-05-26 19:00:27 -04:00
for header in payload . get ( "headers" , []):
2025-12-23 11:25:29 +00:00
header_name_lower = header [ "name" ] . lower ()
if header_name_lower in target_headers :
# Store using the original requested casing
headers [ target_headers [ header_name_lower ]] = header [ "value" ]
2025-05-26 19:00:27 -04:00
return headers
2025-08-04 12:53:04 +02:00
def _prepare_gmail_message (
subject : str ,
body : str ,
to : Optional [ str ] = None ,
2025-08-04 12:56:38 +02:00
cc : Optional [ str ] = None ,
bcc : Optional [ str ] = None ,
2025-08-04 12:53:04 +02:00
thread_id : Optional [ str ] = None ,
in_reply_to : Optional [ str ] = None ,
references : Optional [ str ] = None ,
2025-10-05 16:50:58 -04:00
body_format : Literal [ "plain" , "html" ] = "plain" ,
2025-10-24 21:40:36 -04:00
from_email : Optional [ str ] = None ,
2026-01-09 19:14:27 +01:00
from_name : Optional [ str ] = None ,
2026-01-23 02:32:58 +02:00
attachments : Optional [ List [ Dict [ str , str ]]] = None ,
2025-08-04 12:53:04 +02:00
) -> tuple [ str , Optional [ str ]]:
"""
2026-01-23 02:32:58 +02:00
Prepare a Gmail message with threading and attachment support.
2025-08-13 12:05:06 -04:00
2025-08-04 12:53:04 +02:00
Args:
subject: Email subject
2025-10-05 16:50:58 -04:00
body: Email body content
2025-08-04 12:53:04 +02:00
to: Optional recipient email address
2025-08-04 12:56:38 +02:00
cc: Optional CC email address
bcc: Optional BCC email address
2025-08-04 12:53:04 +02:00
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
2025-10-05 16:50:58 -04:00
body_format: Content type for the email body ('plain' or 'html')
2025-10-24 21:40:36 -04:00
from_email: Optional sender email address
2026-01-09 19:14:27 +01:00
from_name: Optional sender display name (e.g., "Peter Hartree")
2026-01-23 02:32:58 +02:00
attachments: Optional list of attachments. Each can have 'path' (file path) OR 'content' (base64) + 'filename'
2025-08-13 12:05:06 -04:00
2025-08-04 12:53:04 +02:00
Returns:
Tuple of (raw_message, thread_id) where raw_message is base64 encoded
"""
# Handle reply subject formatting
reply_subject = subject
2025-12-13 13:49:28 -08:00
if in_reply_to and not subject . lower () . startswith ( "re:" ):
2025-08-04 12:53:04 +02:00
reply_subject = f "Re: { subject } "
# Prepare the email
2025-10-05 16:50:58 -04:00
normalized_format = body_format . lower ()
if normalized_format not in { "plain" , "html" }:
raise ValueError ( "body_format must be either 'plain' or 'html'." )
2026-01-23 02:32:58 +02:00
# Use multipart if attachments are provided
if attachments :
message = MIMEMultipart ()
message . attach ( MIMEText ( body , normalized_format ))
# Process attachments
for attachment in attachments :
file_path = attachment . get ( "path" )
filename = attachment . get ( "filename" )
content_base64 = attachment . get ( "content" )
mime_type = attachment . get ( "mime_type" )
try :
# If path is provided, read and encode the file
if file_path :
path_obj = Path ( file_path )
if not path_obj . exists ():
logger . error ( f "File not found: { file_path } " )
continue
# Read file content
with open ( path_obj , "rb" ) as f :
file_data = f . read ()
# Use provided filename or extract from path
if not filename :
filename = path_obj . name
# Auto-detect MIME type if not provided
if not mime_type :
mime_type , _ = mimetypes . guess_type ( str ( path_obj ))
if not mime_type :
mime_type = "application/octet-stream"
# If content is provided (base64), decode it
elif content_base64 :
if not filename :
2026-01-29 19:23:23 -05:00
logger . warning ( "Skipping attachment: missing filename" )
2026-01-23 02:32:58 +02:00
continue
2026-01-29 19:43:09 -05:00
file_data = base64 . b64decode ( content_base64 )
2026-01-23 02:32:58 +02:00
if not mime_type :
mime_type = "application/octet-stream"
else :
2026-01-29 19:43:21 -05:00
logger . warning ( "Skipping attachment: missing both path and content" )
2026-01-23 02:32:58 +02:00
continue
# Create MIME attachment
2026-01-29 19:43:09 -05:00
main_type , sub_type = mime_type . split ( "/" , 1 )
part = MIMEBase ( main_type , sub_type )
2026-01-23 02:32:58 +02:00
part . set_payload ( file_data )
encoders . encode_base64 ( part )
2026-01-23 20:16:55 +02:00
# Sanitize filename to prevent header injection and ensure valid quoting
2026-01-29 19:43:09 -05:00
safe_filename = (
( filename or "" )
. replace ( " \r " , "" )
. replace ( " \n " , "" )
. replace ( " \\ " , " \\\\ " )
. replace ( '"' , r " \" " )
)
2026-01-23 20:16:55 +02:00
2026-01-23 02:32:58 +02:00
part . add_header (
2026-01-23 20:16:55 +02:00
"Content-Disposition" , f 'attachment; filename=" { safe_filename } "'
2026-01-23 02:32:58 +02:00
)
message . attach ( part )
logger . info ( f "Attached file: { filename } ( { len ( file_data ) } bytes)" )
except Exception as e :
logger . error ( f "Failed to attach { filename or file_path } : { e } " )
continue
else :
message = MIMEText ( body , normalized_format )
2025-10-24 21:40:36 -04:00
message [ "Subject" ] = reply_subject
# Add sender if provided
if from_email :
2026-01-09 19:14:27 +01:00
if from_name :
2026-01-30 10:33:49 -05:00
# Sanitize from_name to prevent header injection
2026-01-30 10:47:54 -05:00
safe_name = (
from_name . replace ( " \r " , "" ) . replace ( " \n " , "" ) . replace ( " \x00 " , "" )
)
2026-01-30 10:33:49 -05:00
message [ "From" ] = formataddr (( safe_name , from_email ))
2026-01-09 19:14:27 +01:00
else :
message [ "From" ] = from_email
2025-08-04 12:53:04 +02:00
2025-08-04 12:56:38 +02:00
# Add recipients if provided
2025-08-04 12:53:04 +02:00
if to :
2025-10-24 21:40:36 -04:00
message [ "To" ] = to
2025-08-04 12:56:38 +02:00
if cc :
2025-10-24 21:40:36 -04:00
message [ "Cc" ] = cc
2025-08-04 12:56:38 +02:00
if bcc :
2025-10-24 21:40:36 -04:00
message [ "Bcc" ] = bcc
2025-08-04 12:53:04 +02:00
# Add reply headers for threading
if in_reply_to :
message [ "In-Reply-To" ] = in_reply_to
2025-08-13 12:05:06 -04:00
2025-08-04 12:53:04 +02:00
if references :
message [ "References" ] = references
# Encode message
raw_message = base64 . urlsafe_b64encode ( message . as_bytes ()) . decode ()
2025-08-13 12:05:06 -04:00
2025-08-04 12:53:04 +02:00
return raw_message , thread_id
2025-05-25 17:35:58 +08:00
def _generate_gmail_web_url ( item_id : str , account_index : int = 0 ) -> str :
"""
Generate Gmail web interface URL for a message or thread ID.
Uses #all to access messages from any Gmail folder/label (not just inbox).
Args:
item_id: Gmail message ID or thread ID
account_index: Google account index (default 0 for primary account)
Returns:
Gmail web interface URL that opens the message/thread in Gmail web interface
"""
return f "https://mail.google.com/mail/u/ { account_index } /#all/ { item_id } "
2025-12-13 13:49:28 -08:00
def _format_gmail_results_plain (
messages : list , query : str , next_page_token : Optional [ str ] = None
) -> str :
2025-05-26 17:51:34 -04:00
"""Format Gmail search results in clean, LLM-friendly plain text."""
if not messages :
return f "No messages found for query: ' { query } '"
lines = [
f "Found { len ( messages ) } messages matching ' { query } ':" ,
"" ,
"📧 MESSAGES:" ,
]
for i , msg in enumerate ( messages , 1 ):
2025-07-23 12:40:46 -04:00
# Handle potential null/undefined message objects
if not msg or not isinstance ( msg , dict ):
2025-12-13 13:49:28 -08:00
lines . extend (
[
f " { i } . Message: Invalid message data" ,
" Error: Message object is null or malformed" ,
"" ,
]
)
2025-07-23 12:40:46 -04:00
continue
# Handle potential null/undefined values from Gmail API
message_id = msg . get ( "id" )
thread_id = msg . get ( "threadId" )
# Convert None, empty string, or missing values to "unknown"
if not message_id :
message_id = "unknown"
if not thread_id :
thread_id = "unknown"
if message_id != "unknown" :
message_url = _generate_gmail_web_url ( message_id )
else :
message_url = "N/A"
if thread_id != "unknown" :
thread_url = _generate_gmail_web_url ( thread_id )
else :
thread_url = "N/A"
2025-05-26 17:51:34 -04:00
2025-07-17 14:46:25 -04:00
lines . extend (
[
2025-07-23 12:40:46 -04:00
f " { i } . Message ID: { message_id } " ,
2025-07-17 14:46:25 -04:00
f " Web Link: { message_url } " ,
2025-07-23 12:40:46 -04:00
f " Thread ID: { thread_id } " ,
2025-07-17 14:46:25 -04:00
f " Thread Link: { thread_url } " ,
"" ,
]
)
lines . extend (
[
"💡 USAGE:" ,
" • Pass the Message IDs **as a list** to get_gmail_messages_content_batch()" ,
" e.g. get_gmail_messages_content_batch(message_ids=[...])" ,
" • Pass the Thread IDs to get_gmail_thread_content() (single) or get_gmail_threads_content_batch() (batch)" ,
]
)
2025-05-26 17:51:34 -04:00
2025-11-29 14:45:05 +01:00
# Add pagination info if there's a next page
if next_page_token :
lines . append ( "" )
2025-12-13 13:49:28 -08:00
lines . append (
f "📄 PAGINATION: To get the next page, call search_gmail_messages again with page_token=' { next_page_token } '"
)
2025-11-29 14:45:05 +01:00
2025-05-26 17:51:34 -04:00
return " \n " . join ( lines )
2025-05-12 17:22:58 -04:00
@server.tool ()
2025-07-28 11:49:01 -04:00
@handle_http_errors ( "search_gmail_messages" , is_read_only = True , service_type = "gmail" )
2025-06-07 09:50:53 -04:00
@require_google_service ( "gmail" , "gmail_read" )
2025-05-20 10:43:47 -04:00
async def search_gmail_messages (
2025-12-13 13:49:28 -08:00
service ,
query : str ,
user_google_email : str ,
page_size : int = 10 ,
page_token : Optional [ str ] = None ,
2025-06-06 17:32:09 -04:00
) -> str :
2025-05-12 17:22:58 -04:00
"""
Searches messages in a user's Gmail account based on a query.
2025-05-25 17:35:58 +08:00
Returns both Message IDs and Thread IDs for each found message, along with Gmail web interface links for manual verification.
2025-11-29 14:45:05 +01:00
Supports pagination via page_token parameter.
2025-05-12 17:22:58 -04:00
Args:
query (str): The search query. Supports standard Gmail search operators.
2025-05-24 10:43:55 -04:00
user_google_email (str): The user's Google email address. Required.
2025-05-12 17:22:58 -04:00
page_size (int): The maximum number of messages to return. Defaults to 10.
2025-11-29 14:45:05 +01:00
page_token (Optional[str]): Token for retrieving the next page of results. Use the next_page_token from a previous response.
2025-05-12 17:22:58 -04:00
Returns:
2025-06-06 17:32:09 -04:00
str: LLM-friendly structured results with Message IDs, Thread IDs, and clickable Gmail web interface URLs for each found message.
2025-11-29 14:45:05 +01:00
Includes pagination token if more results are available.
2025-05-12 17:22:58 -04:00
"""
2025-07-17 14:46:25 -04:00
logger . info (
2025-11-29 14:45:05 +01:00
f "[search_gmail_messages] Email: ' { user_google_email } ', Query: ' { query } ', Page size: { page_size } "
2025-07-17 14:46:25 -04:00
)
2025-05-12 17:22:58 -04:00
2025-11-29 14:45:05 +01:00
# Build the API request parameters
2025-12-13 13:49:28 -08:00
request_params = { "userId" : "me" , "q" : query , "maxResults" : page_size }
2025-11-29 14:45:05 +01:00
# Add page token if provided
if page_token :
request_params [ "pageToken" ] = page_token
2025-12-04 16:21:43 +01:00
logger . info ( "[search_gmail_messages] Using page_token for pagination" )
2025-11-29 14:45:05 +01:00
2025-06-18 17:31:57 -04:00
response = await asyncio . to_thread (
2025-12-13 13:49:28 -08:00
service . users () . messages () . list ( ** request_params ) . execute
2025-06-18 17:31:57 -04:00
)
2025-07-23 12:40:46 -04:00
# Handle potential null response (but empty dict {} is valid)
if response is None :
2025-07-23 14:36:32 -04:00
logger . warning ( "[search_gmail_messages] Null response from Gmail API" )
2025-07-23 12:40:46 -04:00
return f "No response received from Gmail API for query: ' { query } '"
2025-06-18 17:31:57 -04:00
messages = response . get ( "messages" , [])
2025-07-23 12:40:46 -04:00
# Additional safety check for null messages array
if messages is None :
messages = []
2025-11-29 14:45:05 +01:00
# Extract next page token for pagination
next_page_token = response . get ( "nextPageToken" )
formatted_output = _format_gmail_results_plain ( messages , query , next_page_token )
2025-05-12 17:22:58 -04:00
2025-06-18 17:31:57 -04:00
logger . info ( f "[search_gmail_messages] Found { len ( messages ) } messages" )
2025-11-29 14:45:05 +01:00
if next_page_token :
2025-12-13 13:49:28 -08:00
logger . info (
"[search_gmail_messages] More results available (next_page_token present)"
)
2025-06-18 17:31:57 -04:00
return formatted_output
2025-05-12 17:22:58 -04:00
@server.tool ()
2025-12-13 13:49:28 -08:00
@handle_http_errors (
"get_gmail_message_content" , is_read_only = True , service_type = "gmail"
)
2025-06-07 09:50:53 -04:00
@require_google_service ( "gmail" , "gmail_read" )
2025-05-20 10:43:47 -04:00
async def get_gmail_message_content (
2025-06-07 09:50:53 -04:00
service , message_id : str , user_google_email : str
2025-06-06 17:32:09 -04:00
) -> str :
2025-05-12 17:22:58 -04:00
"""
2025-11-04 16:40:04 -05:00
Retrieves the full content (subject, sender, recipients, plain text body) of a specific Gmail message.
2025-05-12 17:22:58 -04:00
Args:
message_id (str): The unique ID of the Gmail message to retrieve.
2025-05-24 10:43:55 -04:00
user_google_email (str): The user's Google email address. Required.
2025-05-12 17:22:58 -04:00
Returns:
2026-01-05 23:20:31 +00:00
str: The message details including subject, sender, date, Message-ID, recipients (To, Cc), and body content.
2025-05-12 17:22:58 -04:00
"""
2025-05-22 14:21:52 -04:00
logger . info (
2025-06-07 09:50:53 -04:00
f "[get_gmail_message_content] Invoked. Message ID: ' { message_id } ', Email: ' { user_google_email } '"
2025-05-22 14:21:52 -04:00
)
2025-05-13 12:36:53 -04:00
2025-06-18 17:31:57 -04:00
logger . info ( f "[get_gmail_message_content] Using service for: { user_google_email } " )
# Fetch message metadata first to get headers
message_metadata = await asyncio . to_thread (
service . users ()
. messages ()
. get (
userId = "me" ,
id = message_id ,
format = "metadata" ,
2025-12-23 11:25:29 +00:00
metadataHeaders = GMAIL_METADATA_HEADERS ,
2025-05-12 17:22:58 -04:00
)
2025-06-18 17:31:57 -04:00
. execute
)
2025-05-12 17:22:58 -04:00
2025-12-23 11:25:29 +00:00
headers = _extract_headers (
message_metadata . get ( "payload" , {}), GMAIL_METADATA_HEADERS
)
2025-06-18 17:31:57 -04:00
subject = headers . get ( "Subject" , "(no subject)" )
sender = headers . get ( "From" , "(unknown sender)" )
2025-11-04 16:40:04 -05:00
to = headers . get ( "To" , "" )
cc = headers . get ( "Cc" , "" )
2025-12-23 11:25:29 +00:00
rfc822_msg_id = headers . get ( "Message-ID" , "" )
2025-06-18 17:31:57 -04:00
# Now fetch the full message to get the body parts
message_full = await asyncio . to_thread (
service . users ()
. messages ()
. get (
userId = "me" ,
id = message_id ,
format = "full" , # Request full payload for body
2025-05-12 17:22:58 -04:00
)
2025-06-18 17:31:57 -04:00
. execute
)
2025-05-12 17:22:58 -04:00
2025-08-13 00:14:26 -04:00
# Extract both text and HTML bodies using enhanced helper function
2025-06-18 17:31:57 -04:00
payload = message_full . get ( "payload" , {})
2025-08-13 00:14:26 -04:00
bodies = _extract_message_bodies ( payload )
text_body = bodies . get ( "text" , "" )
html_body = bodies . get ( "html" , "" )
2025-08-13 12:05:06 -04:00
2025-08-13 00:14:26 -04:00
# Format body content with HTML fallback
2025-08-13 12:04:07 -04:00
body_data = _format_body_content ( text_body , html_body )
2025-05-22 14:21:52 -04:00
2025-10-15 16:12:52 -04:00
# Extract attachment metadata
attachments = _extract_attachments ( payload )
content_lines = [
f "Subject: { subject } " ,
f "From: { sender } " ,
2026-01-05 23:20:31 +00:00
f "Date: { headers . get ( 'Date' , '(unknown date)' ) } " ,
2025-10-15 16:12:52 -04:00
]
2025-12-23 11:25:29 +00:00
if rfc822_msg_id :
content_lines . append ( f "Message-ID: { rfc822_msg_id } " )
2025-11-04 16:40:04 -05:00
if to :
content_lines . append ( f "To: { to } " )
if cc :
content_lines . append ( f "Cc: { cc } " )
content_lines . append ( f " \n --- BODY --- \n { body_data or '[No text/plain body found]' } " )
2025-10-15 16:12:52 -04:00
# Add attachment information if present
if attachments :
content_lines . append ( " \n --- ATTACHMENTS ---" )
for i , att in enumerate ( attachments , 1 ):
2025-12-13 13:49:28 -08:00
size_kb = att [ "size" ] / 1024
2025-10-15 16:12:52 -04:00
content_lines . append (
f " { i } . { att [ 'filename' ] } ( { att [ 'mimeType' ] } , { size_kb : .1f } KB) \n "
f " Attachment ID: { att [ 'attachmentId' ] } \n "
f " Use get_gmail_attachment_content(message_id=' { message_id } ', attachment_id=' { att [ 'attachmentId' ] } ') to download"
)
return " \n " . join ( content_lines )
2025-05-26 19:00:27 -04:00
@server.tool ()
2025-12-13 13:49:28 -08:00
@handle_http_errors (
"get_gmail_messages_content_batch" , is_read_only = True , service_type = "gmail"
)
2025-06-07 09:50:53 -04:00
@require_google_service ( "gmail" , "gmail_read" )
2025-05-26 19:00:27 -04:00
async def get_gmail_messages_content_batch (
2025-06-07 09:50:53 -04:00
service ,
2025-05-26 19:00:27 -04:00
message_ids : List [ str ],
user_google_email : str ,
format : Literal [ "full" , "metadata" ] = "full" ,
2025-06-06 17:32:09 -04:00
) -> str :
2025-05-26 19:00:27 -04:00
"""
Retrieves the content of multiple Gmail messages in a single batch request.
2025-07-23 12:40:46 -04:00
Supports up to 25 messages per batch to prevent SSL connection exhaustion.
2025-05-26 19:00:27 -04:00
Args:
2025-07-23 14:01:02 -04:00
message_ids (List[str]): List of Gmail message IDs to retrieve (max 25 per batch).
2025-05-26 19:00:27 -04:00
user_google_email (str): The user's Google email address. Required.
format (Literal["full", "metadata"]): Message format. "full" includes body, "metadata" only headers.
Returns:
2026-01-05 23:20:31 +00:00
str: A formatted list of message contents including subject, sender, date, Message-ID, recipients (To, Cc), and body (if full format).
2025-05-26 19:00:27 -04:00
"""
logger . info (
2025-06-07 09:50:53 -04:00
f "[get_gmail_messages_content_batch] Invoked. Message count: { len ( message_ids ) } , Email: ' { user_google_email } '"
2025-05-26 19:00:27 -04:00
)
if not message_ids :
2025-06-06 17:32:09 -04:00
raise Exception ( "No message IDs provided" )
2025-05-26 19:00:27 -04:00
2025-06-18 17:31:57 -04:00
output_messages = []
2025-05-26 19:00:27 -04:00
2025-07-23 12:40:46 -04:00
# Process in smaller chunks to prevent SSL connection exhaustion
2025-07-23 14:01:02 -04:00
for chunk_start in range ( 0 , len ( message_ids ), GMAIL_BATCH_SIZE ):
chunk_ids = message_ids [ chunk_start : chunk_start + GMAIL_BATCH_SIZE ]
2025-06-18 17:31:57 -04:00
results : Dict [ str , Dict ] = {}
2025-05-26 19:00:27 -04:00
2025-06-18 17:31:57 -04:00
def _batch_callback ( request_id , response , exception ):
"""Callback for batch requests"""
results [ request_id ] = { "data" : response , "error" : exception }
2025-05-26 19:00:27 -04:00
2025-06-18 17:31:57 -04:00
# Try to use batch API
try :
batch = service . new_batch_http_request ( callback = _batch_callback )
2025-05-26 19:00:27 -04:00
2025-06-18 17:31:57 -04:00
for mid in chunk_ids :
if format == "metadata" :
2025-07-17 14:46:25 -04:00
req = (
service . users ()
. messages ()
. get (
userId = "me" ,
id = mid ,
format = "metadata" ,
2025-12-23 11:25:29 +00:00
metadataHeaders = GMAIL_METADATA_HEADERS ,
2025-07-17 14:46:25 -04:00
)
2025-06-18 17:31:57 -04:00
)
else :
2025-07-17 14:46:25 -04:00
req = (
service . users ()
. messages ()
. get ( userId = "me" , id = mid , format = "full" )
2025-06-18 17:31:57 -04:00
)
batch . add ( req , request_id = mid )
# Execute batch request
await asyncio . to_thread ( batch . execute )
except Exception as batch_error :
2025-07-23 12:40:46 -04:00
# Fallback to sequential processing instead of parallel to prevent SSL exhaustion
2025-06-18 17:31:57 -04:00
logger . warning (
2025-07-23 12:40:46 -04:00
f "[get_gmail_messages_content_batch] Batch API failed, falling back to sequential processing: { batch_error } "
2025-06-18 17:31:57 -04:00
)
2025-07-23 12:40:46 -04:00
async def fetch_message_with_retry ( mid : str , max_retries : int = 3 ):
"""Fetch a single message with exponential backoff retry for SSL errors"""
for attempt in range ( max_retries ):
try :
if format == "metadata" :
msg = await asyncio . to_thread (
service . users ()
. messages ()
. get (
userId = "me" ,
id = mid ,
format = "metadata" ,
2025-12-23 11:25:29 +00:00
metadataHeaders = GMAIL_METADATA_HEADERS ,
2025-07-23 12:40:46 -04:00
)
. execute
2025-07-17 14:46:25 -04:00
)
2025-07-23 12:40:46 -04:00
else :
msg = await asyncio . to_thread (
service . users ()
. messages ()
. get ( userId = "me" , id = mid , format = "full" )
. execute
)
return mid , msg , None
except ssl . SSLError as ssl_error :
if attempt < max_retries - 1 :
# Exponential backoff: 1s, 2s, 4s
2025-12-13 13:49:28 -08:00
delay = 2 ** attempt
2025-07-23 12:40:46 -04:00
logger . warning (
f "[get_gmail_messages_content_batch] SSL error for message { mid } on attempt { attempt + 1 } : { ssl_error } . Retrying in { delay } s..."
)
await asyncio . sleep ( delay )
else :
logger . error (
f "[get_gmail_messages_content_batch] SSL error for message { mid } on final attempt: { ssl_error } "
)
return mid , None , ssl_error
except Exception as e :
return mid , None , e
2025-05-26 19:00:27 -04:00
2025-07-23 12:40:46 -04:00
# Process messages sequentially with small delays to prevent connection exhaustion
for mid in chunk_ids :
mid_result , msg_data , error = await fetch_message_with_retry ( mid )
results [ mid_result ] = { "data" : msg_data , "error" : error }
# Brief delay between requests to allow connection cleanup
2025-07-23 14:01:02 -04:00
await asyncio . sleep ( GMAIL_REQUEST_DELAY )
2025-05-26 19:00:27 -04:00
2025-06-18 17:31:57 -04:00
# Process results for this chunk
for mid in chunk_ids :
entry = results . get ( mid , { "data" : None , "error" : "No result" })
2025-05-26 19:00:27 -04:00
2025-06-18 17:31:57 -04:00
if entry [ "error" ]:
2025-07-17 14:46:25 -04:00
output_messages . append ( f "⚠️ Message { mid } : { entry [ 'error' ] } \n " )
2025-06-18 17:31:57 -04:00
else :
message = entry [ "data" ]
if not message :
2025-07-17 14:46:25 -04:00
output_messages . append ( f "⚠️ Message { mid } : No data returned \n " )
2025-06-18 17:31:57 -04:00
continue
2025-05-26 19:00:27 -04:00
2025-06-18 17:31:57 -04:00
# Extract content based on format
payload = message . get ( "payload" , {})
2025-05-26 19:00:27 -04:00
2025-06-18 17:31:57 -04:00
if format == "metadata" :
2025-12-27 12:08:58 -08:00
headers = _extract_headers ( payload , GMAIL_METADATA_HEADERS )
2025-06-18 17:31:57 -04:00
subject = headers . get ( "Subject" , "(no subject)" )
sender = headers . get ( "From" , "(unknown sender)" )
2025-11-04 16:40:04 -05:00
to = headers . get ( "To" , "" )
cc = headers . get ( "Cc" , "" )
2025-12-23 11:25:29 +00:00
rfc822_msg_id = headers . get ( "Message-ID" , "" )
2025-05-26 19:00:27 -04:00
2025-12-13 13:49:28 -08:00
msg_output = (
f "Message ID: { mid } \n Subject: { subject } \n From: { sender } \n "
2026-01-05 23:20:31 +00:00
f "Date: { headers . get ( 'Date' , '(unknown date)' ) } \n "
2025-12-13 13:49:28 -08:00
)
2025-12-23 11:25:29 +00:00
if rfc822_msg_id :
msg_output += f "Message-ID: { rfc822_msg_id } \n "
2025-11-04 16:40:04 -05:00
if to :
msg_output += f "To: { to } \n "
if cc :
msg_output += f "Cc: { cc } \n "
msg_output += f "Web Link: { _generate_gmail_web_url ( mid ) } \n "
output_messages . append ( msg_output )
2025-05-26 19:00:27 -04:00
else :
2025-06-18 17:31:57 -04:00
# Full format - extract body too
2025-12-27 12:08:58 -08:00
headers = _extract_headers ( payload , GMAIL_METADATA_HEADERS )
2025-06-18 17:31:57 -04:00
subject = headers . get ( "Subject" , "(no subject)" )
sender = headers . get ( "From" , "(unknown sender)" )
2025-11-04 16:40:04 -05:00
to = headers . get ( "To" , "" )
cc = headers . get ( "Cc" , "" )
2025-12-23 11:25:29 +00:00
rfc822_msg_id = headers . get ( "Message-ID" , "" )
2025-08-13 12:05:06 -04:00
2025-08-13 00:14:26 -04:00
# Extract both text and HTML bodies using enhanced helper function
bodies = _extract_message_bodies ( payload )
text_body = bodies . get ( "text" , "" )
html_body = bodies . get ( "html" , "" )
2025-08-13 12:05:06 -04:00
2025-08-13 00:14:26 -04:00
# Format body content with HTML fallback
2025-08-13 12:04:07 -04:00
body_data = _format_body_content ( text_body , html_body )
2025-05-26 19:00:27 -04:00
2025-12-13 13:49:28 -08:00
msg_output = (
f "Message ID: { mid } \n Subject: { subject } \n From: { sender } \n "
2026-01-05 23:20:31 +00:00
f "Date: { headers . get ( 'Date' , '(unknown date)' ) } \n "
2025-12-13 13:49:28 -08:00
)
2025-12-23 11:25:29 +00:00
if rfc822_msg_id :
msg_output += f "Message-ID: { rfc822_msg_id } \n "
2025-11-04 16:40:04 -05:00
if to :
msg_output += f "To: { to } \n "
if cc :
msg_output += f "Cc: { cc } \n "
2025-12-13 13:49:28 -08:00
msg_output += (
f "Web Link: { _generate_gmail_web_url ( mid ) } \n\n { body_data } \n "
)
2025-11-04 16:40:04 -05:00
output_messages . append ( msg_output )
2025-05-26 19:00:27 -04:00
2025-06-18 17:31:57 -04:00
# Combine all messages with separators
final_output = f "Retrieved { len ( message_ids ) } messages: \n\n "
final_output += " \n --- \n\n " . join ( output_messages )
2025-05-26 19:00:27 -04:00
2025-06-18 17:31:57 -04:00
return final_output
2025-05-22 14:21:52 -04:00
2025-05-12 17:22:58 -04:00
2025-10-15 16:12:52 -04:00
@server.tool ()
2025-12-13 13:49:28 -08:00
@handle_http_errors (
"get_gmail_attachment_content" , is_read_only = True , service_type = "gmail"
)
2025-10-15 16:12:52 -04:00
@require_google_service ( "gmail" , "gmail_read" )
async def get_gmail_attachment_content (
service ,
message_id : str ,
attachment_id : str ,
user_google_email : str ,
) -> str :
"""
Downloads the content of a specific email attachment.
Args:
message_id (str): The ID of the Gmail message containing the attachment.
attachment_id (str): The ID of the attachment to download.
user_google_email (str): The user's Google email address. Required.
Returns:
str: Attachment metadata and base64-encoded content that can be decoded and saved.
"""
logger . info (
f "[get_gmail_attachment_content] Invoked. Message ID: ' { message_id } ', Email: ' { user_google_email } '"
)
# Download attachment directly without refetching message metadata.
#
# Important: Gmail attachment IDs are ephemeral and change between API calls for the
# same message. If we refetch the message here to get metadata, the new attachment IDs
# won't match the attachment_id parameter provided by the caller, causing the function
# to fail. The attachment download endpoint returns size information, and filename/mime
# type should be obtained from the original message content call that provided this ID.
try :
attachment = await asyncio . to_thread (
service . users ()
. messages ()
. attachments ()
. get ( userId = "me" , messageId = message_id , id = attachment_id )
. execute
)
except Exception as e :
2025-12-13 13:49:28 -08:00
logger . error (
f "[get_gmail_attachment_content] Failed to download attachment: { e } "
)
2025-10-15 16:12:52 -04:00
return (
f "Error: Failed to download attachment. The attachment ID may have changed. \n "
f "Please fetch the message content again to get an updated attachment ID. \n\n "
f "Error details: { str ( e ) } "
)
# Format response with attachment data
2025-12-13 13:49:28 -08:00
size_bytes = attachment . get ( "size" , 0 )
2025-10-15 16:12:52 -04:00
size_kb = size_bytes / 1024 if size_bytes else 0
2025-12-13 13:49:28 -08:00
base64_data = attachment . get ( "data" , "" )
2025-11-29 15:06:57 +01:00
# Check if we're in stateless mode (can't save files)
from auth.oauth_config import is_stateless_mode
2025-12-13 13:49:28 -08:00
2025-11-29 15:06:57 +01:00
if is_stateless_mode ():
result_lines = [
"Attachment downloaded successfully!" ,
f "Message ID: { message_id } " ,
f "Size: { size_kb : .1f } KB ( { size_bytes } bytes)" ,
" \n ⚠️ Stateless mode: File storage disabled." ,
" \n Base64-encoded content (first 100 characters shown):" ,
f " { base64_data [: 100 ] } ..." ,
2025-12-13 13:49:28 -08:00
" \n Note: Attachment IDs are ephemeral. Always use IDs from the most recent message fetch." ,
2025-11-29 15:06:57 +01:00
]
2025-12-13 13:49:28 -08:00
logger . info (
f "[get_gmail_attachment_content] Successfully downloaded { size_kb : .1f } KB attachment (stateless mode)"
)
2025-11-29 15:06:57 +01:00
return " \n " . join ( result_lines )
2025-10-15 16:12:52 -04:00
2025-11-29 15:06:57 +01:00
# Save attachment and generate URL
try :
from core.attachment_storage import get_attachment_storage , get_attachment_url
2025-12-13 13:49:28 -08:00
2025-11-29 15:06:57 +01:00
storage = get_attachment_storage ()
2025-12-13 13:49:28 -08:00
2025-11-29 15:06:57 +01:00
# Try to get filename and mime type from message (optional - attachment IDs are ephemeral)
filename = None
mime_type = None
try :
# Quick metadata fetch to try to get attachment info
# Note: This might fail if attachment IDs changed, but worth trying
message_metadata = await asyncio . to_thread (
service . users ()
. messages ()
. get ( userId = "me" , id = message_id , format = "metadata" )
2025-12-08 09:22:28 -05:00
. execute
2025-11-29 15:06:57 +01:00
)
payload = message_metadata . get ( "payload" , {})
attachments = _extract_attachments ( payload )
for att in attachments :
if att . get ( "attachmentId" ) == attachment_id :
filename = att . get ( "filename" )
mime_type = att . get ( "mimeType" )
break
except Exception :
# If we can't get metadata, use defaults
2025-12-13 13:49:28 -08:00
logger . debug (
f "Could not fetch attachment metadata for { attachment_id } , using defaults"
)
2025-11-29 15:06:57 +01:00
# Save attachment
file_id = storage . save_attachment (
2025-12-13 13:49:28 -08:00
base64_data = base64_data , filename = filename , mime_type = mime_type
2025-11-29 15:06:57 +01:00
)
2025-12-13 13:49:28 -08:00
2025-11-29 15:06:57 +01:00
# Generate URL
attachment_url = get_attachment_url ( file_id )
2025-12-13 13:49:28 -08:00
2025-11-29 15:06:57 +01:00
result_lines = [
"Attachment downloaded successfully!" ,
f "Message ID: { message_id } " ,
f "Size: { size_kb : .1f } KB ( { size_bytes } bytes)" ,
f " \n 📎 Download URL: { attachment_url } " ,
" \n The attachment has been saved and is available at the URL above." ,
"The file will expire after 1 hour." ,
2025-12-13 13:49:28 -08:00
" \n Note: Attachment IDs are ephemeral. Always use IDs from the most recent message fetch." ,
2025-11-29 15:06:57 +01:00
]
2025-12-13 13:49:28 -08:00
logger . info (
f "[get_gmail_attachment_content] Successfully saved { size_kb : .1f } KB attachment as { file_id } "
)
2025-11-29 15:06:57 +01:00
return " \n " . join ( result_lines )
2025-12-13 13:49:28 -08:00
2025-11-29 15:06:57 +01:00
except Exception as e :
2025-12-13 13:49:28 -08:00
logger . error (
f "[get_gmail_attachment_content] Failed to save attachment: { e } " ,
exc_info = True ,
)
2025-11-29 15:06:57 +01:00
# Fallback to showing base64 preview
result_lines = [
"Attachment downloaded successfully!" ,
f "Message ID: { message_id } " ,
f "Size: { size_kb : .1f } KB ( { size_bytes } bytes)" ,
" \n ⚠️ Failed to save attachment file. Showing preview instead." ,
" \n Base64-encoded content (first 100 characters shown):" ,
f " { base64_data [: 100 ] } ..." ,
f " \n Error: { str ( e ) } " ,
2025-12-13 13:49:28 -08:00
" \n Note: Attachment IDs are ephemeral. Always use IDs from the most recent message fetch." ,
2025-11-29 15:06:57 +01:00
]
return " \n " . join ( result_lines )
2025-10-15 16:12:52 -04:00
2025-05-19 20:57:11 -07:00
@server.tool ()
2025-07-28 11:49:01 -04:00
@handle_http_errors ( "send_gmail_message" , service_type = "gmail" )
2025-07-17 13:57:21 -04:00
@require_google_service ( "gmail" , GMAIL_SEND_SCOPE )
2025-05-19 20:57:11 -07:00
async def send_gmail_message (
2025-06-07 09:50:53 -04:00
service ,
2025-05-24 10:43:55 -04:00
user_google_email : str ,
2025-05-19 20:57:11 -07:00
to : str = Body ( ... , description = "Recipient email address." ),
subject : str = Body ( ... , description = "Email subject." ),
2025-10-05 16:50:58 -04:00
body : str = Body ( ... , description = "Email body content (plain text or HTML)." ),
body_format : Literal [ "plain" , "html" ] = Body (
2025-12-13 13:49:28 -08:00
"plain" ,
description = "Email body format. Use 'plain' for plaintext or 'html' for HTML content." ,
2025-10-05 16:50:58 -04:00
),
2025-08-04 12:56:38 +02:00
cc : Optional [ str ] = Body ( None , description = "Optional CC email address." ),
bcc : Optional [ str ] = Body ( None , description = "Optional BCC email address." ),
2026-01-09 19:14:27 +01:00
from_name : Optional [ str ] = Body (
2026-01-30 10:20:05 -05:00
None ,
description = "Optional sender display name (e.g., 'Peter Hartree'). If provided, the From header will be formatted as 'Name <email>'." ,
),
2026-01-22 19:35:55 +01:00
from_email : Optional [ str ] = Body (
None ,
description = "Optional 'Send As' alias email address. Must be configured in Gmail settings (Settings > Accounts > Send mail as). If not provided, uses the authenticated user's email." ,
2026-01-09 19:14:27 +01:00
),
2025-12-13 13:49:28 -08:00
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."
),
2026-01-23 20:12:42 +02:00
attachments : Optional [ List [ Dict [ str , str ]]] = Body (
2026-01-23 02:32:58 +02:00
None ,
2026-01-29 19:43:09 -05:00
description = 'Optional list of attachments. Each can have: "path" (file path, auto-encodes), OR "content" (standard base64, not urlsafe) + "filename". Optional "mime_type". Example: [{"path": "/path/to/file.pdf"}] or [{"filename": "doc.pdf", "content": "base64data", "mime_type": "application/pdf"}]' ,
2026-01-23 02:32:58 +02:00
),
2025-06-06 17:32:09 -04:00
) -> str :
2025-05-19 20:57:11 -07:00
"""
2026-01-23 02:32:58 +02:00
Sends an email using the user's Gmail account. Supports both new emails and replies with optional attachments.
2026-01-22 19:35:55 +01:00
Supports Gmail's "Send As" feature to send from configured alias addresses.
2025-05-19 20:57:11 -07:00
Args:
to (str): Recipient email address.
subject (str): Email subject.
2025-10-05 16:50:58 -04:00
body (str): Email body content.
body_format (Literal['plain', 'html']): Email body format. Defaults to 'plain'.
2026-01-23 20:21:00 +02:00
attachments (Optional[List[Dict[str, str]]]): Optional list of attachments. Each dict can contain:
2026-01-23 02:32:58 +02:00
Option 1 - File path (auto-encodes):
- 'path' (required): File path to attach
- 'filename' (optional): Override filename
- 'mime_type' (optional): Override MIME type (auto-detected if not provided)
Option 2 - Base64 content:
2026-01-29 19:43:09 -05:00
- 'content' (required): Standard base64-encoded file content (not urlsafe)
2026-01-23 02:32:58 +02:00
- 'filename' (required): Name of the file
- 'mime_type' (optional): MIME type (defaults to 'application/octet-stream')
2025-08-04 12:56:38 +02:00
cc (Optional[str]): Optional CC email address.
bcc (Optional[str]): Optional BCC email address.
2026-01-09 19:14:27 +01:00
from_name (Optional[str]): Optional sender display name. If provided, the From header will be formatted as 'Name <email>'.
2026-01-22 19:35:55 +01:00
from_email (Optional[str]): Optional 'Send As' alias email address. The alias must be
configured in Gmail settings (Settings > Accounts > Send mail as). If not provided,
the email will be sent from the authenticated user's primary email address.
user_google_email (str): The user's Google email address. Required for authentication.
2025-08-04 12:48:38 +02:00
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.
2025-05-19 20:57:11 -07:00
Returns:
2025-06-06 17:32:09 -04:00
str: Confirmation message with the sent email's message ID.
2025-08-13 12:05:06 -04:00
2025-08-04 12:48:38 +02:00
Examples:
2026-01-30 10:43:04 -05:00
# Send a new email
send_gmail_message(to="user@example.com", subject="Hello", body="Hi there!")
# Send with a custom display name
send_gmail_message(to="user@example.com", subject="Hello", body="Hi there!", from_name="John Doe")
2025-08-13 12:05:06 -04:00
2025-10-05 16:50:58 -04:00
# Send an HTML email
send_gmail_message(
to="user@example.com",
subject="Hello",
body="<strong>Hi there!</strong>",
body_format="html"
)
2026-01-22 19:35:55 +01:00
# Send from a configured alias (Send As)
send_gmail_message(
to="user@example.com",
subject="Business Inquiry",
body="Hello from my business address...",
from_email="business@mydomain.com"
)
2025-08-04 12:56:38 +02:00
# Send an email with CC and BCC
send_gmail_message(
2025-08-13 12:05:06 -04:00
to="user@example.com",
2025-08-04 12:56:38 +02:00
cc="manager@example.com",
bcc="archive@example.com",
2025-08-13 12:05:06 -04:00
subject="Project Update",
2025-08-04 12:56:38 +02:00
body="Here's the latest update..."
)
2025-08-13 12:05:06 -04:00
2026-01-23 02:32:58 +02:00
# Send an email with attachments (using file path)
send_gmail_message(
to="user@example.com",
subject="Report",
body="Please see attached report.",
attachments=[{
"path": "/path/to/report.pdf"
}]
)
# Send an email with attachments (using base64 content)
send_gmail_message(
to="user@example.com",
subject="Report",
body="Please see attached report.",
attachments=[{
"filename": "report.pdf",
"content": "JVBERi0xLjQK...", # base64 encoded PDF
"mime_type": "application/pdf"
}]
)
2025-08-04 12:48:38 +02:00
# Send a reply
send_gmail_message(
2025-08-13 12:05:06 -04:00
to="user@example.com",
subject="Re: Meeting tomorrow",
2025-08-04 12:48:38 +02:00
body="Thanks for the update!",
thread_id="thread_123",
in_reply_to="<message123@gmail.com>",
references="<original@gmail.com> <message123@gmail.com>"
)
2025-05-19 20:57:11 -07:00
"""
2025-08-04 12:48:38 +02:00
logger . info (
2026-01-23 20:12:42 +02:00
f "[send_gmail_message] Invoked. Email: ' { user_google_email } ', Subject: ' { subject } ', Attachments: { len ( attachments ) if attachments else 0 } "
2025-08-04 12:48:38 +02:00
)
2025-08-04 12:53:04 +02:00
# Prepare the email message
2026-01-22 19:35:55 +01:00
# Use from_email (Send As alias) if provided, otherwise default to authenticated user
sender_email = from_email or user_google_email
2025-08-04 12:53:04 +02:00
raw_message , thread_id_final = _prepare_gmail_message (
subject = subject ,
body = body ,
to = to ,
2025-08-04 12:56:38 +02:00
cc = cc ,
bcc = bcc ,
2025-08-04 12:53:04 +02:00
thread_id = thread_id ,
in_reply_to = in_reply_to ,
references = references ,
2025-10-05 16:50:58 -04:00
body_format = body_format ,
2026-01-22 19:35:55 +01:00
from_email = sender_email ,
2026-01-09 19:14:27 +01:00
from_name = from_name ,
2026-01-23 02:32:58 +02:00
attachments = attachments if attachments else None ,
2025-08-04 12:53:04 +02:00
)
2025-08-13 12:05:06 -04:00
2025-06-18 17:31:57 -04:00
send_body = { "raw" : raw_message }
2025-08-13 12:05:06 -04:00
2025-08-04 12:48:38 +02:00
# Associate with thread if provided
2025-08-04 12:53:04 +02:00
if thread_id_final :
send_body [ "threadId" ] = thread_id_final
2025-06-18 17:31:57 -04:00
# Send the message
sent_message = await asyncio . to_thread (
service . users () . messages () . send ( userId = "me" , body = send_body ) . execute
)
message_id = sent_message . get ( "id" )
2026-01-23 02:32:58 +02:00
if attachments :
return f "Email sent with { len ( attachments ) } attachment(s)! Message ID: { message_id } "
2025-06-18 17:31:57 -04:00
return f "Email sent! Message ID: { message_id } "
2025-05-22 14:21:52 -04:00
2025-05-22 00:02:51 +05:30
@server.tool ()
2025-07-28 11:49:01 -04:00
@handle_http_errors ( "draft_gmail_message" , service_type = "gmail" )
2025-07-17 13:57:21 -04:00
@require_google_service ( "gmail" , GMAIL_COMPOSE_SCOPE )
2025-05-22 00:02:51 +05:30
async def draft_gmail_message (
2025-06-07 09:50:53 -04:00
service ,
2025-05-24 10:43:55 -04:00
user_google_email : str ,
2025-05-22 00:02:51 +05:30
subject : str = Body ( ... , description = "Email subject." ),
body : str = Body ( ... , description = "Email body (plain text)." ),
2025-10-13 12:15:18 +07:00
body_format : Literal [ "plain" , "html" ] = Body (
2025-12-13 13:49:28 -08:00
"plain" ,
description = "Email body format. Use 'plain' for plaintext or 'html' for HTML content." ,
2025-10-13 12:15:18 +07:00
),
2025-05-22 00:02:51 +05:30
to : Optional [ str ] = Body ( None , description = "Optional recipient email address." ),
2025-08-04 12:56:38 +02:00
cc : Optional [ str ] = Body ( None , description = "Optional CC email address." ),
bcc : Optional [ str ] = Body ( None , description = "Optional BCC email address." ),
2026-01-09 19:14:27 +01:00
from_name : Optional [ str ] = Body (
2026-01-30 10:20:05 -05:00
None ,
description = "Optional sender display name (e.g., 'Peter Hartree'). If provided, the From header will be formatted as 'Name <email>'." ,
),
2026-01-22 19:35:55 +01:00
from_email : Optional [ str ] = Body (
None ,
description = "Optional 'Send As' alias email address. Must be configured in Gmail settings (Settings > Accounts > Send mail as). If not provided, uses the authenticated user's email." ,
2026-01-09 19:14:27 +01:00
),
2025-12-13 13:49:28 -08:00
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."
),
2026-01-23 20:16:55 +02:00
attachments : Optional [ List [ Dict [ str , str ]]] = Body (
None ,
2026-01-29 19:43:09 -05:00
description = "Optional list of attachments. Each can have: 'path' (file path, auto-encodes), OR 'content' (standard base64, not urlsafe) + 'filename'. Optional 'mime_type' (auto-detected from path if not provided)." ,
2026-01-23 02:32:58 +02:00
),
2025-06-06 17:32:09 -04:00
) -> str :
2025-05-22 00:02:51 +05:30
"""
2026-01-23 02:32:58 +02:00
Creates a draft email in the user's Gmail account. Supports both new drafts and reply drafts with optional attachments.
2026-01-22 19:35:55 +01:00
Supports Gmail's "Send As" feature to draft from configured alias addresses.
2025-05-22 00:02:51 +05:30
Args:
2026-01-22 19:35:55 +01:00
user_google_email (str): The user's Google email address. Required for authentication.
2025-05-22 00:02:51 +05:30
subject (str): Email subject.
body (str): Email body (plain text).
2025-10-13 12:15:18 +07:00
body_format (Literal['plain', 'html']): Email body format. Defaults to 'plain'.
2025-05-22 00:02:51 +05:30
to (Optional[str]): Optional recipient email address. Can be left empty for drafts.
2025-08-04 12:56:38 +02:00
cc (Optional[str]): Optional CC email address.
bcc (Optional[str]): Optional BCC email address.
2026-01-09 19:14:27 +01:00
from_name (Optional[str]): Optional sender display name. If provided, the From header will be formatted as 'Name <email>'.
2026-01-22 19:35:55 +01:00
from_email (Optional[str]): Optional 'Send As' alias email address. The alias must be
configured in Gmail settings (Settings > Accounts > Send mail as). If not provided,
the draft will be from the authenticated user's primary email address.
2025-08-04 12:41:01 +02:00
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.
2026-01-23 02:32:58 +02:00
attachments (List[Dict[str, str]]): Optional list of attachments. Each dict can contain:
Option 1 - File path (auto-encodes):
- 'path' (required): File path to attach
- 'filename' (optional): Override filename
- 'mime_type' (optional): Override MIME type (auto-detected if not provided)
Option 2 - Base64 content:
2026-01-29 19:43:09 -05:00
- 'content' (required): Standard base64-encoded file content (not urlsafe)
2026-01-23 02:32:58 +02:00
- 'filename' (required): Name of the file
- 'mime_type' (optional): MIME type (defaults to 'application/octet-stream')
2025-05-22 00:02:51 +05:30
Returns:
2025-06-06 17:32:09 -04:00
str: Confirmation message with the created draft's ID.
2025-08-13 12:05:06 -04:00
2025-08-04 12:41:01 +02:00
Examples:
# Create a new draft
draft_gmail_message(subject="Hello", body="Hi there!", to="user@example.com")
2025-08-13 12:05:06 -04:00
2026-01-22 19:35:55 +01:00
# Create a draft from a configured alias (Send As)
draft_gmail_message(
subject="Business Inquiry",
body="Hello from my business address...",
to="user@example.com",
from_email="business@mydomain.com"
)
2025-10-13 12:15:18 +07:00
# Create a plaintext draft with CC and BCC
2025-08-04 12:56:38 +02:00
draft_gmail_message(
2025-08-13 12:05:06 -04:00
subject="Project Update",
2025-08-04 12:56:38 +02:00
body="Here's the latest update...",
to="user@example.com",
cc="manager@example.com",
bcc="archive@example.com"
)
2025-08-13 12:05:06 -04:00
2025-10-13 12:15:18 +07:00
# Create a HTML draft with CC and BCC
draft_gmail_message(
subject="Project Update",
body="<strong>Hi there!</strong>",
body_format="html",
to="user@example.com",
cc="manager@example.com",
bcc="archive@example.com"
)
# Create a reply draft in plaintext
2025-08-04 12:41:01 +02:00
draft_gmail_message(
2025-08-13 12:05:06 -04:00
subject="Re: Meeting tomorrow",
2025-08-04 12:41:01 +02:00
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>"
)
2025-10-13 12:15:18 +07:00
# Create a reply draft in HTML
draft_gmail_message(
subject="Re: Meeting tomorrow",
body="<strong>Thanks for the update!</strong>",
2026-01-22 19:35:55 +01:00
body_format="html",
2025-10-13 12:15:18 +07:00
to="user@example.com",
thread_id="thread_123",
in_reply_to="<message123@gmail.com>",
references="<original@gmail.com> <message123@gmail.com>"
)
2025-05-22 00:02:51 +05:30
"""
2025-05-22 14:21:52 -04:00
logger . info (
2025-06-07 09:50:53 -04:00
f "[draft_gmail_message] Invoked. Email: ' { user_google_email } ', Subject: ' { subject } '"
2025-05-22 14:21:52 -04:00
)
2025-08-04 12:53:04 +02:00
# Prepare the email message
2026-01-22 19:35:55 +01:00
# Use from_email (Send As alias) if provided, otherwise default to authenticated user
sender_email = from_email or user_google_email
2025-08-04 12:53:04 +02:00
raw_message , thread_id_final = _prepare_gmail_message (
subject = subject ,
body = body ,
2025-10-13 12:15:18 +07:00
body_format = body_format ,
2025-08-04 12:53:04 +02:00
to = to ,
2025-08-04 12:56:38 +02:00
cc = cc ,
bcc = bcc ,
2025-08-04 12:53:04 +02:00
thread_id = thread_id ,
in_reply_to = in_reply_to ,
references = references ,
2026-01-22 19:35:55 +01:00
from_email = sender_email ,
2026-01-09 19:14:27 +01:00
from_name = from_name ,
2026-01-23 02:32:58 +02:00
attachments = attachments ,
2025-08-04 12:53:04 +02:00
)
2025-05-22 00:02:51 +05:30
2025-06-18 17:31:57 -04:00
# Create a draft instead of sending
draft_body = { "message" : { "raw" : raw_message }}
2025-08-13 12:05:06 -04:00
2025-08-04 12:41:01 +02:00
# Associate with thread if provided
2025-08-04 12:53:04 +02:00
if thread_id_final :
draft_body [ "message" ][ "threadId" ] = thread_id_final
2025-05-22 00:02:51 +05:30
2025-06-18 17:31:57 -04:00
# Create the draft
created_draft = await asyncio . to_thread (
service . users () . drafts () . create ( userId = "me" , body = draft_body ) . execute
)
draft_id = created_draft . get ( "id" )
2026-01-23 02:32:58 +02:00
attachment_info = f " with { len ( attachments ) } attachment(s)" if attachments else ""
return f "Draft created { attachment_info } ! Draft ID: { draft_id } "
2025-05-24 00:08:19 +08:00
2025-07-17 14:46:25 -04:00
def _format_thread_content ( thread_data : dict , thread_id : str ) -> str :
2025-05-24 00:08:19 +08:00
"""
2025-07-17 14:46:25 -04:00
Helper function to format thread content from Gmail API response.
2025-05-24 00:08:19 +08:00
Args:
2025-07-17 14:46:25 -04:00
thread_data (dict): Thread data from Gmail API
thread_id (str): Thread ID for display
2025-05-24 00:08:19 +08:00
Returns:
2025-07-17 14:46:25 -04:00
str: Formatted thread content
2025-05-24 00:08:19 +08:00
"""
2025-07-17 14:46:25 -04:00
messages = thread_data . get ( "messages" , [])
2025-06-18 17:31:57 -04:00
if not messages :
return f "No messages found in thread ' { thread_id } '."
# Extract thread subject from the first message
first_message = messages [ 0 ]
first_headers = {
h [ "name" ]: h [ "value" ]
for h in first_message . get ( "payload" , {}) . get ( "headers" , [])
}
thread_subject = first_headers . get ( "Subject" , "(no subject)" )
# Build the thread content
content_lines = [
f "Thread ID: { thread_id } " ,
f "Subject: { thread_subject } " ,
f "Messages: { len ( messages ) } " ,
"" ,
]
2025-05-24 00:08:19 +08:00
2025-06-18 17:31:57 -04:00
# Process each message in the thread
for i , message in enumerate ( messages , 1 ):
# Extract headers
headers = {
2025-07-17 14:46:25 -04:00
h [ "name" ]: h [ "value" ] for h in message . get ( "payload" , {}) . get ( "headers" , [])
2025-05-24 00:08:19 +08:00
}
2025-06-18 17:31:57 -04:00
sender = headers . get ( "From" , "(unknown sender)" )
date = headers . get ( "Date" , "(unknown date)" )
subject = headers . get ( "Subject" , "(no subject)" )
2025-05-24 00:08:19 +08:00
2025-08-13 00:14:26 -04:00
# Extract both text and HTML bodies
2025-06-18 17:31:57 -04:00
payload = message . get ( "payload" , {})
2025-08-13 00:14:26 -04:00
bodies = _extract_message_bodies ( payload )
text_body = bodies . get ( "text" , "" )
html_body = bodies . get ( "html" , "" )
2025-08-13 12:05:06 -04:00
2025-08-13 00:14:26 -04:00
# Format body content with HTML fallback
2025-08-13 12:04:07 -04:00
body_data = _format_body_content ( text_body , html_body )
2025-05-24 00:08:19 +08:00
2025-06-18 17:31:57 -04:00
# Add message to content
content_lines . extend (
[
f "=== Message { i } ===" ,
f "From: { sender } " ,
f "Date: { date } " ,
]
)
2025-05-24 00:08:19 +08:00
2025-06-18 17:31:57 -04:00
# Only show subject if it's different from thread subject
if subject != thread_subject :
content_lines . append ( f "Subject: { subject } " )
2025-05-24 00:08:19 +08:00
2025-06-18 17:31:57 -04:00
content_lines . extend (
[
"" ,
2025-08-13 00:14:26 -04:00
body_data ,
2025-06-18 17:31:57 -04:00
"" ,
]
2025-05-22 14:21:52 -04:00
)
2025-06-18 17:31:57 -04:00
2025-07-17 14:46:25 -04:00
return " \n " . join ( content_lines )
@server.tool ()
@require_google_service ( "gmail" , "gmail_read" )
2025-07-28 11:49:01 -04:00
@handle_http_errors ( "get_gmail_thread_content" , is_read_only = True , service_type = "gmail" )
2025-07-17 14:46:25 -04:00
async def get_gmail_thread_content (
service , thread_id : str , user_google_email : str
) -> str :
"""
Retrieves the complete content of a Gmail conversation thread, including all messages.
Args:
thread_id (str): The unique ID of the Gmail thread to retrieve.
user_google_email (str): The user's Google email address. Required.
Returns:
str: The complete thread content with all messages formatted for reading.
"""
logger . info (
f "[get_gmail_thread_content] Invoked. Thread ID: ' { thread_id } ', Email: ' { user_google_email } '"
)
# Fetch the complete thread with all messages
thread_response = await asyncio . to_thread (
service . users () . threads () . get ( userId = "me" , id = thread_id , format = "full" ) . execute
)
return _format_thread_content ( thread_response , thread_id )
2025-06-06 12:02:49 -04:00
2025-07-17 11:30:03 +01:00
@server.tool ()
@require_google_service ( "gmail" , "gmail_read" )
2025-12-13 13:49:28 -08:00
@handle_http_errors (
"get_gmail_threads_content_batch" , is_read_only = True , service_type = "gmail"
)
2025-07-17 11:30:03 +01:00
async def get_gmail_threads_content_batch (
service ,
thread_ids : List [ str ],
user_google_email : str ,
) -> str :
"""
Retrieves the content of multiple Gmail threads in a single batch request.
2025-07-23 12:40:46 -04:00
Supports up to 25 threads per batch to prevent SSL connection exhaustion.
2025-07-17 11:30:03 +01:00
Args:
2025-07-23 14:01:02 -04:00
thread_ids (List[str]): A list of Gmail thread IDs to retrieve. The function will automatically batch requests in chunks of 25.
2025-07-17 11:30:03 +01:00
user_google_email (str): The user's Google email address. Required.
Returns:
str: A formatted list of thread contents with separators.
"""
logger . info (
f "[get_gmail_threads_content_batch] Invoked. Thread count: { len ( thread_ids ) } , Email: ' { user_google_email } '"
)
if not thread_ids :
2025-07-17 14:46:25 -04:00
raise ValueError ( "No thread IDs provided" )
2025-07-17 11:30:03 +01:00
output_threads = []
2025-07-17 14:46:25 -04:00
def _batch_callback ( request_id , response , exception ):
"""Callback for batch requests"""
results [ request_id ] = { "data" : response , "error" : exception }
2025-07-23 12:40:46 -04:00
# Process in smaller chunks to prevent SSL connection exhaustion
2025-07-23 14:01:02 -04:00
for chunk_start in range ( 0 , len ( thread_ids ), GMAIL_BATCH_SIZE ):
chunk_ids = thread_ids [ chunk_start : chunk_start + GMAIL_BATCH_SIZE ]
2025-07-17 11:30:03 +01:00
results : Dict [ str , Dict ] = {}
# Try to use batch API
try :
batch = service . new_batch_http_request ( callback = _batch_callback )
for tid in chunk_ids :
2025-07-17 14:46:25 -04:00
req = service . users () . threads () . get ( userId = "me" , id = tid , format = "full" )
2025-07-17 11:30:03 +01:00
batch . add ( req , request_id = tid )
# Execute batch request
await asyncio . to_thread ( batch . execute )
except Exception as batch_error :
2025-07-23 12:40:46 -04:00
# Fallback to sequential processing instead of parallel to prevent SSL exhaustion
2025-07-17 11:30:03 +01:00
logger . warning (
2025-07-23 12:40:46 -04:00
f "[get_gmail_threads_content_batch] Batch API failed, falling back to sequential processing: { batch_error } "
2025-07-17 11:30:03 +01:00
)
2025-07-23 12:40:46 -04:00
async def fetch_thread_with_retry ( tid : str , max_retries : int = 3 ):
"""Fetch a single thread with exponential backoff retry for SSL errors"""
for attempt in range ( max_retries ):
try :
thread = await asyncio . to_thread (
service . users ()
. threads ()
. get ( userId = "me" , id = tid , format = "full" )
. execute
)
return tid , thread , None
except ssl . SSLError as ssl_error :
if attempt < max_retries - 1 :
# Exponential backoff: 1s, 2s, 4s
2025-12-13 13:49:28 -08:00
delay = 2 ** attempt
2025-07-23 12:40:46 -04:00
logger . warning (
f "[get_gmail_threads_content_batch] SSL error for thread { tid } on attempt { attempt + 1 } : { ssl_error } . Retrying in { delay } s..."
)
await asyncio . sleep ( delay )
else :
logger . error (
f "[get_gmail_threads_content_batch] SSL error for thread { tid } on final attempt: { ssl_error } "
)
return tid , None , ssl_error
except Exception as e :
return tid , None , e
2025-07-17 11:30:03 +01:00
2025-07-23 12:40:46 -04:00
# Process threads sequentially with small delays to prevent connection exhaustion
for tid in chunk_ids :
tid_result , thread_data , error = await fetch_thread_with_retry ( tid )
results [ tid_result ] = { "data" : thread_data , "error" : error }
# Brief delay between requests to allow connection cleanup
2025-07-23 14:01:02 -04:00
await asyncio . sleep ( GMAIL_REQUEST_DELAY )
2025-07-17 11:30:03 +01:00
# Process results for this chunk
for tid in chunk_ids :
entry = results . get ( tid , { "data" : None , "error" : "No result" })
if entry [ "error" ]:
2025-07-17 14:46:25 -04:00
output_threads . append ( f "⚠️ Thread { tid } : { entry [ 'error' ] } \n " )
2025-07-17 11:30:03 +01:00
else :
thread = entry [ "data" ]
if not thread :
2025-07-17 14:46:25 -04:00
output_threads . append ( f "⚠️ Thread { tid } : No data returned \n " )
2025-07-17 11:30:03 +01:00
continue
2025-07-17 14:46:25 -04:00
output_threads . append ( _format_thread_content ( thread , tid ))
2025-07-17 11:30:03 +01:00
# Combine all threads with separators
2025-07-17 14:46:25 -04:00
header = f "Retrieved { len ( thread_ids ) } threads:"
return header + " \n\n " + " \n --- \n\n " . join ( output_threads )
2025-07-17 11:30:03 +01:00
2025-06-06 12:02:49 -04:00
@server.tool ()
2025-07-28 11:49:01 -04:00
@handle_http_errors ( "list_gmail_labels" , is_read_only = True , service_type = "gmail" )
2025-06-07 09:50:53 -04:00
@require_google_service ( "gmail" , "gmail_read" )
async def list_gmail_labels ( service , user_google_email : str ) -> str :
2025-06-06 12:02:49 -04:00
"""
Lists all labels in the user's Gmail account.
Args:
user_google_email (str): The user's Google email address. Required.
Returns:
2025-06-06 17:32:09 -04:00
str: A formatted list of all labels with their IDs, names, and types.
2025-06-06 12:02:49 -04:00
"""
2025-06-07 09:50:53 -04:00
logger . info ( f "[list_gmail_labels] Invoked. Email: ' { user_google_email } '" )
2025-06-06 12:02:49 -04:00
2025-06-18 17:31:57 -04:00
response = await asyncio . to_thread (
service . users () . labels () . list ( userId = "me" ) . execute
)
labels = response . get ( "labels" , [])
2025-06-06 12:02:49 -04:00
2025-06-18 17:31:57 -04:00
if not labels :
return "No labels found."
2025-06-06 12:50:32 -04:00
2025-06-18 17:31:57 -04:00
lines = [ f "Found { len ( labels ) } labels:" , "" ]
2025-06-06 12:50:32 -04:00
2025-06-18 17:31:57 -04:00
system_labels = []
user_labels = []
2025-06-06 12:02:49 -04:00
2025-06-18 17:31:57 -04:00
for label in labels :
if label . get ( "type" ) == "system" :
system_labels . append ( label )
else :
user_labels . append ( label )
2025-06-06 12:02:49 -04:00
2025-06-18 17:31:57 -04:00
if system_labels :
lines . append ( "📂 SYSTEM LABELS:" )
for label in system_labels :
lines . append ( f " • { label [ 'name' ] } (ID: { label [ 'id' ] } )" )
lines . append ( "" )
2025-06-06 12:02:49 -04:00
2025-06-18 17:31:57 -04:00
if user_labels :
lines . append ( "🏷️ USER LABELS:" )
for label in user_labels :
lines . append ( f " • { label [ 'name' ] } (ID: { label [ 'id' ] } )" )
2025-06-06 12:02:49 -04:00
2025-06-18 17:31:57 -04:00
return " \n " . join ( lines )
2025-06-06 12:02:49 -04:00
@server.tool ()
2025-07-28 11:49:01 -04:00
@handle_http_errors ( "manage_gmail_label" , service_type = "gmail" )
2025-07-17 13:57:21 -04:00
@require_google_service ( "gmail" , GMAIL_LABELS_SCOPE )
2025-06-06 12:02:49 -04:00
async def manage_gmail_label (
2025-06-07 09:50:53 -04:00
service ,
2025-06-06 12:02:49 -04:00
user_google_email : str ,
action : Literal [ "create" , "update" , "delete" ],
name : Optional [ str ] = None ,
label_id : Optional [ str ] = None ,
label_list_visibility : Literal [ "labelShow" , "labelHide" ] = "labelShow" ,
message_list_visibility : Literal [ "show" , "hide" ] = "show" ,
2025-06-06 17:32:09 -04:00
) -> str :
2025-06-06 12:02:49 -04:00
"""
Manages Gmail labels: create, update, or delete labels.
Args:
user_google_email (str): The user's Google email address. Required.
action (Literal["create", "update", "delete"]): Action to perform on the label.
name (Optional[str]): Label name. Required for create, optional for update.
label_id (Optional[str]): Label ID. Required for update and delete operations.
label_list_visibility (Literal["labelShow", "labelHide"]): Whether the label is shown in the label list.
message_list_visibility (Literal["show", "hide"]): Whether the label is shown in the message list.
Returns:
2025-06-06 17:32:09 -04:00
str: Confirmation message of the label operation.
2025-06-06 12:02:49 -04:00
"""
2025-07-17 14:46:25 -04:00
logger . info (
f "[manage_gmail_label] Invoked. Email: ' { user_google_email } ', Action: ' { action } '"
)
2025-06-06 12:02:49 -04:00
if action == "create" and not name :
2025-06-06 17:32:09 -04:00
raise Exception ( "Label name is required for create action." )
2025-06-06 12:50:32 -04:00
2025-06-06 12:02:49 -04:00
if action in [ "update" , "delete" ] and not label_id :
2025-06-06 17:32:09 -04:00
raise Exception ( "Label ID is required for update and delete actions." )
2025-06-06 12:02:49 -04:00
2025-06-18 17:31:57 -04:00
if action == "create" :
label_object = {
"name" : name ,
"labelListVisibility" : label_list_visibility ,
"messageListVisibility" : message_list_visibility ,
}
created_label = await asyncio . to_thread (
service . users () . labels () . create ( userId = "me" , body = label_object ) . execute
)
return f "Label created successfully! \n Name: { created_label [ 'name' ] } \n ID: { created_label [ 'id' ] } "
2025-06-06 12:50:32 -04:00
2025-06-18 17:31:57 -04:00
elif action == "update" :
current_label = await asyncio . to_thread (
service . users () . labels () . get ( userId = "me" , id = label_id ) . execute
)
2025-06-06 12:50:32 -04:00
2025-06-18 17:31:57 -04:00
label_object = {
"id" : label_id ,
"name" : name if name is not None else current_label [ "name" ],
"labelListVisibility" : label_list_visibility ,
"messageListVisibility" : message_list_visibility ,
}
2025-06-06 12:02:49 -04:00
2025-06-18 17:31:57 -04:00
updated_label = await asyncio . to_thread (
2025-07-17 14:46:25 -04:00
service . users ()
. labels ()
. update ( userId = "me" , id = label_id , body = label_object )
. execute
2025-06-18 17:31:57 -04:00
)
return f "Label updated successfully! \n Name: { updated_label [ 'name' ] } \n ID: { updated_label [ 'id' ] } "
2025-06-06 12:50:32 -04:00
2025-06-18 17:31:57 -04:00
elif action == "delete" :
label = await asyncio . to_thread (
service . users () . labels () . get ( userId = "me" , id = label_id ) . execute
)
label_name = label [ "name" ]
2025-06-06 12:02:49 -04:00
2025-06-18 17:31:57 -04:00
await asyncio . to_thread (
service . users () . labels () . delete ( userId = "me" , id = label_id ) . execute
)
return f "Label ' { label_name } ' (ID: { label_id } ) deleted successfully!"
2025-06-06 12:02:49 -04:00
2025-12-13 13:40:05 -08:00
@server.tool ()
@handle_http_errors ( "list_gmail_filters" , is_read_only = True , service_type = "gmail" )
@require_google_service ( "gmail" , "gmail_settings_basic" )
async def list_gmail_filters ( service , user_google_email : str ) -> str :
"""
Lists all Gmail filters configured in the user's mailbox.
Args:
user_google_email (str): The user's Google email address. Required.
Returns:
str: A formatted list of filters with their criteria and actions.
"""
logger . info ( f "[list_gmail_filters] Invoked. Email: ' { user_google_email } '" )
response = await asyncio . to_thread (
service . users () . settings () . filters () . list ( userId = "me" ) . execute
)
filters = response . get ( "filter" ) or response . get ( "filters" ) or []
if not filters :
return "No filters found."
lines = [ f "Found { len ( filters ) } filters:" , "" ]
for filter_obj in filters :
filter_id = filter_obj . get ( "id" , "(no id)" )
criteria = filter_obj . get ( "criteria" , {})
action = filter_obj . get ( "action" , {})
lines . append ( f "🔹 Filter ID: { filter_id } " )
lines . append ( " Criteria:" )
criteria_lines = []
if criteria . get ( "from" ):
criteria_lines . append ( f "From: { criteria [ 'from' ] } " )
if criteria . get ( "to" ):
criteria_lines . append ( f "To: { criteria [ 'to' ] } " )
if criteria . get ( "subject" ):
criteria_lines . append ( f "Subject: { criteria [ 'subject' ] } " )
if criteria . get ( "query" ):
criteria_lines . append ( f "Query: { criteria [ 'query' ] } " )
if criteria . get ( "negatedQuery" ):
criteria_lines . append ( f "Exclude Query: { criteria [ 'negatedQuery' ] } " )
if criteria . get ( "hasAttachment" ):
criteria_lines . append ( "Has attachment" )
if criteria . get ( "excludeChats" ):
criteria_lines . append ( "Exclude chats" )
if criteria . get ( "size" ):
comparison = criteria . get ( "sizeComparison" , "" )
criteria_lines . append (
f "Size { comparison or '' } { criteria [ 'size' ] } bytes" . strip ()
)
if not criteria_lines :
criteria_lines . append ( "(none)" )
lines . extend ([ f " • { line } " for line in criteria_lines ])
lines . append ( " Actions:" )
action_lines = []
if action . get ( "forward" ):
action_lines . append ( f "Forward to: { action [ 'forward' ] } " )
if action . get ( "removeLabelIds" ):
2025-12-13 13:49:28 -08:00
action_lines . append ( f "Remove labels: { ', ' . join ( action [ 'removeLabelIds' ]) } " )
2025-12-13 13:40:05 -08:00
if action . get ( "addLabelIds" ):
action_lines . append ( f "Add labels: { ', ' . join ( action [ 'addLabelIds' ]) } " )
if not action_lines :
action_lines . append ( "(none)" )
lines . extend ([ f " • { line } " for line in action_lines ])
lines . append ( "" )
return " \n " . join ( lines ) . rstrip ()
@server.tool ()
@handle_http_errors ( "create_gmail_filter" , service_type = "gmail" )
2025-12-13 13:47:41 -08:00
@require_google_service ( "gmail" , "gmail_settings_basic" )
2025-12-13 13:40:05 -08:00
async def create_gmail_filter (
service ,
user_google_email : str ,
criteria : Dict [ str , Any ] = Body (
... , description = "Filter criteria object as defined in the Gmail API."
),
action : Dict [ str , Any ] = Body (
... , description = "Filter action object as defined in the Gmail API."
),
) -> str :
"""
Creates a Gmail filter using the users.settings.filters API.
Args:
user_google_email (str): The user's Google email address. Required.
criteria (Dict[str, Any]): Criteria for matching messages.
action (Dict[str, Any]): Actions to apply to matched messages.
Returns:
str: Confirmation message with the created filter ID.
"""
logger . info ( "[create_gmail_filter] Invoked" )
filter_body = { "criteria" : criteria , "action" : action }
created_filter = await asyncio . to_thread (
2025-12-13 13:49:28 -08:00
service . users ()
. settings ()
. filters ()
. create ( userId = "me" , body = filter_body )
. execute
2025-12-13 13:40:05 -08:00
)
filter_id = created_filter . get ( "id" , "(unknown)" )
return f "Filter created successfully! \n Filter ID: { filter_id } "
@server.tool ()
@handle_http_errors ( "delete_gmail_filter" , service_type = "gmail" )
2025-12-13 13:47:41 -08:00
@require_google_service ( "gmail" , "gmail_settings_basic" )
2025-12-13 13:40:05 -08:00
async def delete_gmail_filter (
2025-12-13 13:49:28 -08:00
service ,
user_google_email : str ,
filter_id : str = Field ( ... , description = "ID of the filter to delete." ),
2025-12-13 13:40:05 -08:00
) -> str :
"""
Deletes a Gmail filter by ID.
Args:
user_google_email (str): The user's Google email address. Required.
filter_id (str): The ID of the filter to delete.
Returns:
str: Confirmation message for the deletion.
"""
logger . info ( f "[delete_gmail_filter] Invoked. Filter ID: ' { filter_id } '" )
filter_details = await asyncio . to_thread (
service . users () . settings () . filters () . get ( userId = "me" , id = filter_id ) . execute
)
await asyncio . to_thread (
service . users () . settings () . filters () . delete ( userId = "me" , id = filter_id ) . execute
)
criteria = filter_details . get ( "criteria" , {})
action = filter_details . get ( "action" , {})
return (
"Filter deleted successfully! \n "
f "Filter ID: { filter_id } \n "
f "Criteria: { criteria or '(none)' } \n "
f "Action: { action or '(none)' } "
)
2025-06-06 12:02:49 -04:00
@server.tool ()
2025-07-28 11:49:01 -04:00
@handle_http_errors ( "modify_gmail_message_labels" , service_type = "gmail" )
2025-07-17 13:57:21 -04:00
@require_google_service ( "gmail" , GMAIL_MODIFY_SCOPE )
2025-06-06 12:02:49 -04:00
async def modify_gmail_message_labels (
2025-06-07 09:50:53 -04:00
service ,
2025-06-06 12:02:49 -04:00
user_google_email : str ,
message_id : str ,
2025-12-13 13:49:28 -08:00
add_label_ids : List [ str ] = Field (
default = [], description = "Label IDs to add to the message."
),
remove_label_ids : List [ str ] = Field (
default = [], description = "Label IDs to remove from the message."
),
2025-06-06 17:32:09 -04:00
) -> str :
2025-06-06 12:02:49 -04:00
"""
Adds or removes labels from a Gmail message.
2025-07-31 10:00:49 -04:00
To archive an email, remove the INBOX label.
To delete an email, add the TRASH label.
2025-06-06 12:02:49 -04:00
Args:
user_google_email (str): The user's Google email address. Required.
message_id (str): The ID of the message to modify.
add_label_ids (Optional[List[str]]): List of label IDs to add to the message.
remove_label_ids (Optional[List[str]]): List of label IDs to remove from the message.
Returns:
2025-06-06 17:32:09 -04:00
str: Confirmation message of the label changes applied to the message.
2025-06-06 12:02:49 -04:00
"""
2025-07-17 14:46:25 -04:00
logger . info (
f "[modify_gmail_message_labels] Invoked. Email: ' { user_google_email } ', Message ID: ' { message_id } '"
)
2025-06-06 12:02:49 -04:00
if not add_label_ids and not remove_label_ids :
2025-07-17 14:46:25 -04:00
raise Exception (
"At least one of add_label_ids or remove_label_ids must be provided."
)
2025-06-06 12:02:49 -04:00
2025-06-18 17:31:57 -04:00
body = {}
if add_label_ids :
body [ "addLabelIds" ] = add_label_ids
if remove_label_ids :
body [ "removeLabelIds" ] = remove_label_ids
2025-06-06 12:02:49 -04:00
2025-06-18 17:31:57 -04:00
await asyncio . to_thread (
service . users () . messages () . modify ( userId = "me" , id = message_id , body = body ) . execute
)
2025-06-06 12:02:49 -04:00
2025-06-18 17:31:57 -04:00
actions = []
if add_label_ids :
actions . append ( f "Added labels: { ', ' . join ( add_label_ids ) } " )
if remove_label_ids :
actions . append ( f "Removed labels: { ', ' . join ( remove_label_ids ) } " )
2025-06-06 12:02:49 -04:00
2025-06-18 17:31:57 -04:00
return f "Message labels updated successfully! \n Message ID: { message_id } \n { '; ' . join ( actions ) } "
2025-07-18 05:32:00 +01:00
@server.tool ()
2025-07-28 11:49:01 -04:00
@handle_http_errors ( "batch_modify_gmail_message_labels" , service_type = "gmail" )
2025-07-18 05:32:00 +01:00
@require_google_service ( "gmail" , GMAIL_MODIFY_SCOPE )
async def batch_modify_gmail_message_labels (
service ,
user_google_email : str ,
message_ids : List [ str ],
2025-12-13 13:49:28 -08:00
add_label_ids : List [ str ] = Field (
default = [], description = "Label IDs to add to messages."
),
remove_label_ids : List [ str ] = Field (
default = [], description = "Label IDs to remove from messages."
),
2025-07-18 05:32:00 +01:00
) -> str :
"""
Adds or removes labels from multiple Gmail messages in a single batch request.
Args:
user_google_email (str): The user's Google email address. Required.
message_ids (List[str]): A list of message IDs to modify.
add_label_ids (Optional[List[str]]): List of label IDs to add to the messages.
remove_label_ids (Optional[List[str]]): List of label IDs to remove from the messages.
Returns:
str: Confirmation message of the label changes applied to the messages.
"""
logger . info (
f "[batch_modify_gmail_message_labels] Invoked. Email: ' { user_google_email } ', Message IDs: ' { message_ids } '"
)
if not add_label_ids and not remove_label_ids :
raise Exception (
"At least one of add_label_ids or remove_label_ids must be provided."
)
body = { "ids" : message_ids }
if add_label_ids :
body [ "addLabelIds" ] = add_label_ids
if remove_label_ids :
body [ "removeLabelIds" ] = remove_label_ids
await asyncio . to_thread (
service . users () . messages () . batchModify ( userId = "me" , body = body ) . execute
)
actions = []
if add_label_ids :
actions . append ( f "Added labels: { ', ' . join ( add_label_ids ) } " )
if remove_label_ids :
actions . append ( f "Removed labels: { ', ' . join ( remove_label_ids ) } " )
return f "Labels updated for { len ( message_ids ) } messages: { '; ' . join ( actions ) } "