gmail API insert – How to write image data that gmail can read into an existing email

Question:

I’m attempting to put together a script to resize images that are in existing emails (as inline attachments) in our gmail account. But the image data that gets written back to replace the original photos is not subsequently displayed by gmail.

With both the attempt == 1 and attempt == 2 code below, I can manually download the resulting email as a .eml file from the three-dot menu, open that .eml file in python and get PIL’s Image.open function to read the image data (using base64.urlsafe_decode) without complaints, and PIL can also save that data into a file that is read without issue by image viewers on my system.

With the attempt == 1 code, after using set_payload, I can’t use get_payload to get something that PIL will read back in it’s current form (presumably because it’s not base64 encoded at that point).

With the attempt == 2 code, I can use get_payload to get something PIL can read, save etc.

But either way, gmail won’t recognize/display the image in the web interface, just showing the text that appears before and after the image in the original email.

from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from google.auth.transport.requests import Request
import base64
import email
from PIL import Image
import io

SCOPES = ['https://mail.google.com/']
query = "123456 before:2020-05-30 after:2020-05-20" 
# query that finds one specific email containing two images (both having width > 400 pixels)

flow = InstalledAppFlow.from_client_secrets_file(OURSECRETSJSONFILE, SCOPES)
creds = flow.run_local_server(port=0)
service = build('gmail', 'v1', credentials=creds)


res1 = service.users().messages().list(userId='me', q=query).execute() # or any other q= string
fullmsg = service.users().messages().get(userId='me', id=res1['messages'][0]['id'], format='raw').execute()

unencoded = base64.urlsafe_b64decode(fullmsg['raw']).decode('utf-8')
mmsg = email.message_from_string(unencoded)

for part in mmsg.walk():
    if part.get_content_maintype() != 'multipart' and part.get('Content-Disposition') is not None and 'image' in part.get_content_type(): # image
        image = Image.open(io.BytesIO(part.get_payload(decode=True)))
        if image.size[0] > 400:

            image.thumbnail((400,400))
            resizedIO = io.BytesIO()
            image.save(resizedIO, format='png')
            resizedIO.seek(0)
           
            if attempt == 1:
                part.set_payload(resizedIO.read())
                # attempting to read this back with the Image.open line above fails with
                #
                # PIL.UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x04B3D320>
                #
                # using the same line without the decode parameter yields
                #
                # TypeError: a bytes-like object is required, not 'str'
                #
                # and adding a .encode() on to the end of get_payload() again gives PIL.UnidentifiedImageError

            elif attempt == 2:
                part.set_payload(base64.urlsafe_b64encode(resizedIO.read()))
                # attempting to read this back with the Image.open line above works,
                # and the image data can be saved, viewed in an image viewer and confirmed
                # as non-corrupted. But when viewing this email in gmail's web interface,
                # the image is not displayed.

encmsg = base64.urlsafe_b64encode(mmsg.as_string().encode('utf-8'))
temp2 = { 'raw' : encmsg.decode(), 'labelIds' : fullmsg['labelIds'], 'threadId' : fullmsg['threadId']}
resp2 = service.users().messages().insert(userId='me', body=temp2, internalDateSource='dateHeader').execute()
#service.users().messages().delete(userId='me', id=res1['messages'][0]['id']).execute()
# Don't enable the line deleting the original email until everything is working.
Asked By: Blair

||

Answers:

When I saw your script, I thought that part.set_payload(base64.urlsafe_b64encode(resizedIO.read())) is required to be modified at attempt == 2. In this case, how about the following modification?

From:

part.set_payload(base64.urlsafe_b64encode(resizedIO.read()))

To:

part.set_payload(base64.b64encode(resizedIO.read()).decode())
  • Please test this with the condition of attempt == 2.
  • When I tested your script by reflecting this modification, I confirmed that the inline image could be resized, and I can see the resized image in the browser.
Answered By: Tanaike