ibra
ibra

Reputation: 116

Sending a reply with Gmail API Python

I created two Gmail accounts and I'm trying to create an e-mail thread between them with the Python Gmail API.

I can send e-mails without any issue, but when it comes to replying to each other and creating a thread, it is simply not working : the new message is successfully displaying as the answer of the received email for the sender, but it is appearing as a new message - without a linked thread - for the receiver.

This problem was described here in 2019 : https://stackoverflow.com/a/63186609/21966625

However, the Gmail APIs changed a lot since this article and I didn't find how to use these advices with today's API.

I tried to carefully respect the instructions of the docs by defining the message's parameters References and In-Reply-To as the received message's id when replying.

Indeed, I retrieve the email :

received_email= service.users().messages().get(userId='me', id=label['id']).execute()

I get a dict looking like:

{'id': '189462395f418017', 'threadId': '189462395f418017', 'labelIds': ['UNREAD','INBOX'], 'snippet': 'xxx'....}

Hence, when I'm building my e-mail, the following method should work :

message_id=received_email['id']

message = EmailMessage()
message.set_content('')
message['To'] = '[email protected]'
message['From'] = '[email protected]'
message['References'] = message_id
message['In-Reply-To'] = message_id
message['Subject'] = 'Automated draft'

In the same way, I defined the threadId as the id of the message I wanted to reply to.

create_message = {'raw': encoded_message,
                  'threadId': message_id
                 }
send_message = (service.users().messages().send(userId="me", body=create_message).execute())

Thanks to this part of the code, the answers are correctly displayed (for the sender of the answer) as explained above, but it appears as a new message - unlinked to a thread - for the receiver.

Upvotes: 3

Views: 2395

Answers (3)

ibra
ibra

Reputation: 116

Actually I found why my method did not work ; even if the dict mention a kind of message id :

email = {'id': '189462395f418017', 'threadId': '189462395f418017', 'labelIds': ['UNREAD','INBOX'], 'snippet': 'xxx'....}

I thought the messageIDcould be taken just by call email['id'].

The real messageID is somewhere in the ['payload']['headers'] dictionnary ; one could find it by a loop like :

for p in email['payload']['headers']:
    if p["name"] == "Message-Id":
        message_id = p['value']

This way we have the true messageID of the email, and the threads are successfully created.

Upvotes: 5

George
George

Reputation: 1610

Suggestion:

You are on the right track using the message's parameters References and In-Reply-To on your reply body but the issue lies on the messageId and threadId that you have obtained from your previous request.

You can alternatively use the code below that ensures you obtain the threadId of the thread that you want.

You just need to specify the subject of the thread that you want to reply to. The code below behaves as follows:

  1. List the threads on your inbox and collect all thread id using users.threads.list
  2. Extract the thread id that matches the subject of the thread that you want to reply
  3. Retrieve the thread details and extract necessary metadata for the reply using users.threads.get
  4. Send the reply to the thread using users.messages.send

Code:

"""Authorization and Authentication of Account to Access GMAIL API """
import os.path
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from email.message import EmailMessage
import base64

# If modifying these scopes, delete the file token.json.
SCOPES = ['https://mail.google.com/','https://www.googleapis.com/auth/gmail.modify','https://www.googleapis.com/auth/gmail.modify','https://www.googleapis.com/auth/gmail.modify']
creds = None
# The file token.json stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists('token.json'):
    creds = Credentials.from_authorized_user_file('token.json', SCOPES)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
    if creds and creds.expired and creds.refresh_token:
        creds.refresh(Request())
    else:
        flow = InstalledAppFlow.from_client_secrets_file(
            'credentials.json', SCOPES)
        creds = flow.run_local_server(port=0)
    # Save the credentials for the next run
    with open('token.json', 'w') as token:
        token.write(creds.to_json())

"""Authorization and Authentication of Account to Access GMAIL API """

# Set up the Gmail API client
service = build('gmail', 'v1', credentials=creds)

# Specify the subject to match
subject = 'Sample email thread'

# Retrieve the list of threads
threads = service.users().threads().list(userId='me').execute().get('threads', [])

# Find the thread with the matching subject
matching_thread = None
for thread in threads:
    thread_id = thread['id']
    thread_details = service.users().threads().get(userId='me', id=thread_id).execute()
    message = thread_details['messages'][0]  # Get the first message in the thread
    message_subject = next((header['value'] for header in message['payload']['headers'] if header['name'] == 'Subject'), None)
    if message_subject == subject:
        matching_thread = thread
        break

if matching_thread:
    thread_id = matching_thread['id']
else:
    print("No matching thread found.")

# Retrieve the details of the thread
thread = service.users().threads().get(userId='me', id=thread_id).execute()
messages = thread['messages'][0]['payload']['headers']

# Retrieve the metadata of the thread
for (k) in messages:
    if k['name'] == 'To':
        recipient = k['value']
    if k['name'] == 'Subject':
        email_subject = k['value']
    if k['name'] == 'From':
        sender = k['value']
    if k['name'] == 'Message-ID':
        message_id = k['value']

# Constructing the reply message
message = EmailMessage()
message.set_content('This is a sample reply')
message['To'] = recipient
message['From'] = sender
message['Subject'] = email_subject
message['References '] = message_id
message['In-Reply-To '] = message_id

encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode()

create_message = {'raw': encoded_message,
                  'threadId': thread_id}
# Sending the reply message to the thread
send_message = (service.users().messages().send(userId="me", body=create_message).execute())

Note: My code on authorization and authentication part may differ from yours

Sample Thread

enter image description here

Reply using the code:

enter image description here

References:

Gmail API

Users.Threads

Method: users.threads.list

Method: users.threads.get

Method: users.messages.send

Upvotes: 1

Brent Martin Miller
Brent Martin Miller

Reputation: 63

You'll need to send the reply message as part of a thread. You'll need to identify the thread ID from the original message, not the message ID, and add that as part of the new message's message object...

create_message = {
    'raw': encoded_message,
    'threadId': threadId
}

https://developers.google.com/gmail/api/guides/threads https://developers.google.com/gmail/api/reference/rest/v1/users.messages#Message

You may also need to include the headers "References" and "In-Reply-To" with the message ID of the last message.

Upvotes: 2

Related Questions