5

I am automating the creation of Gmail drafts in Python using the Gmail API. I am required to create HTML-formatted emails, but I also personally require creating a plaintext fallback, because it's the Right Thing To Do.

I thought I had all of the above working, until I tried making the plaintext fallback a little different from the HTML. It seems that Google takes it upon itself to create the plaintext fallback for me, rather than using the one I provide, so if my html body is <HTML><BODY>HTML Body</BODY></HTML> and my plaintext body is Plaintext body, the final plaintext body will be HTML Body, discarding the plaintext I provided.

My question: Does anyone know a way to get the Gmail API to use the plaintext I provide, rather than auto-generating the fallback for me?

One relevant item I've noticed: if I attach the HTML and Plaintext bodies in a different order, the reverse happens - GMail will automatically generate an HTML body based on my plaintext. So it seems like it's only paying attention to the last attached body.

Stripped down version of the code I'm using:

import base64
import os
import httplib2
import oauth2client
from oauth2client import client
from oauth2client import tools
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from pathlib import Path
from apiclient import errors
from apiclient import discovery

SCOPES = 'https://mail.google.com/'
CLIENT_SECRET_FILE = 'client_id.json'
APPLICATION_NAME = 'Test Client'

def get_credentials():
    home_dir = os.path.expanduser('~')
    credential_dir = os.path.join(home_dir, '.credentials')
    if not os.path.exists(credential_dir):
        os.makedirs(credential_dir)
    credential_path = os.path.join(credential_dir, 'gmail-python-quickstart.json')

    store = oauth2client.file.Storage(credential_path)
    credentials = store.get()
    if not credentials or credentials.invalid:
        flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
        flow.user_agent = APPLICATION_NAME
        if flags:
            credentials = tools.run_flow(flow, store, flags)
        else:  # Needed only for compatibility with Python 2.6
            credentials = tools.run(flow, store)
        print('Storing credentials to ' + credential_path)
    return credentials


def CreateDraft(service, user_id, message_body):
    message = {'message': message_body}
    draft = service.users().drafts().create(userId=user_id, body=message).execute()
    return draft


def CreateTestMessage(sender, to, subject):
    msg = MIMEMultipart('alternative')
    msg['Subject'] = subject
    msg['From'] = sender
    msg['To'] = to
    plain_text = "Text email message. It's plain. It's text."
    html_text = """\
<html>
  <head></head>
  <body>
    <p>HTML email message</p>
    <ol>
        <li>As easy as one</li>
        <li>two</li>
        <li>three!</li>
    </ol>
    <p>Includes <a href="http://stackoverflow.com/">linktacular</a> goodness</p>
  </body>
</html>
"""

    # Swapping the following two lines results in Gmail generating HTML
    # based on plaintext, as opposed to generating plaintext based on HTML
    msg.attach(MIMEText(plain_text, 'plain')) 
    msg.attach(MIMEText(html_text, 'html'))

    print('-----\nHere is the message:\n\n{m}'.format(m=msg))
    encoded = base64.urlsafe_b64encode(msg.as_string().encode('UTF-8')).decode('UTF-8') 
    return {'raw': encoded}


def main():
    credentials = get_credentials()
    http = credentials.authorize(httplib2.Http())
    service = discovery.build('gmail', 'v1', http=http)

    my_address = 'example@gmail.com' # Obscured to protect the culpable

    test_message = CreateTestMessage(sender=my_address,
                                 to='example@gmail.com',
                                 subject='Subject line Here')

    draft = CreateDraft(service, my_address, test_message)


if __name__ == '__main__':
    main()

Update: Here are example gists of what I'm sending to Gmail vs what gets sent from Gmail, in both the HTML-then-plaintext and the plaintext-then-HTML orders (which generate different results)

Bryant
  • 622
  • 4
  • 18
  • 1
    Could you create a draft with your own email as the receiver, send it, and then look at the sent message with `show original` and show the content? Would be interesting to see. :) – Tholle Feb 09 '16 at 20:28
  • 1
    I could indeed. In fact, that's how I discovered the discrepancy. Updating post with a link to draft samples. – Bryant Feb 09 '16 at 20:54
  • Also, etiquette/style question: Is it best to include context like that as an external link (e.g. gists) or embed it in the original post? I worry that the main post is overly long, but I also worry that perhaps the entirety of the context should be available in the main post. – Bryant Feb 09 '16 at 20:59
  • I don't know the etiquette for that, to be honest. I like the way it is now, since the gist is fairly large as you said. :) – Tholle Feb 09 '16 at 21:03
  • This is very peculiar. Did some experiments, and your messages works great when just sending them. I would guess a draft is simply meant to have one type (as if you were typing in the regular Gmail client, which actually will create the plain text part for you). – Tholle Feb 09 '16 at 22:08
  • 1
    I appreciate the sanity check. I hadn't tried just sending, because this particular use-case calls for drafts to be created and then run through a QA process before being sent. Good to know I'd be doing things right if I were going straight to sending! – Bryant Feb 09 '16 at 22:40

1 Answers1

3

TL;DR: No.

Draft objects are shared with the web UI and mobile UIs, if the text/plain were not just a simple transformation of the text/html, then as soon as user used any other UI to edit the message that special customization would be lost. The reason to use the drafts UI is to allow user to share those drafts between other interfaces.

If you don't care about/want that ability don't use drafts and just send() it at the end, which like SMTP-MSA allows more flexibility.

Eric D
  • 6,901
  • 1
  • 15
  • 26
  • We have a requirement that these emails be reviewed by a quality assurance person before they're sent, so going straight to send() is not an option unless we can find another means of reviewing the final form of the email before sending it. Given the constraints, I think my best solution is "Just post the HTML version, and be happy that Gmail automatically provides a text fallback" – Bryant Feb 10 '16 at 17:22
  • Yeah I think that works. Either store/review in another forum if you want to have a customized text/plain transformation. Or yeah, as you mention, only review the text/html and assume Gmail will continue to provide a reasonable text/plain transformation (which is quite good and reasonable for vast majority of use-cases). – Eric D Feb 10 '16 at 18:41