googleapis / google-api-python-client

🐍 The official Python client library for Google's discovery based APIs.
https://googleapis.github.io/google-api-python-client/docs/
Apache License 2.0
7.72k stars 2.41k forks source link

GMail REST API[google-api-python-client]: multipart/alternative only sends last type specified for text + html parts #1007

Closed dnintzel closed 3 years ago

dnintzel commented 4 years ago

DESCRIPTION: Using GMail API (google-api-python-client), to send emails only sends last multipart type specified for text + html parts is sent when sending to different device types.

SAMPLE CODE: Attaching 'plain' and 'html' parts

message = EmailMessage()
message['to'] = to_email` _# For 'ascii' devices you can use an email-to-text gateway (e.g. Sprint: '1234567890@messaging.sprintpcs.com')_
message['from'] = from_email
message['subject'] = subject
message.set_content('plain text', subtype='plain')
message.add_alternative("<u><b>html:<br>content</b></u>", subtype='html')

Gmail API calls:

raw_string = base64.urlsafe_b64encode(message.as_bytes()).decode()
request = service.users().messages().send(userId=from_email,body={'raw':raw_string})
message = request.execute()

REPRO STEPS What steps will reproduce the problem?

  1. Instantiate mail object with class EmailMessage()
  2. Call method set_content to create a 'plain' subtype
  3. Call method add_alternative to create a 'html' subtype with a different body.
  4. Use gmail create a service and 'send

' API to send the email to a text device/app AND an app/device that renders HTML. EXPECTED: The 'plain' text should go to the 'ascii' device/app (e.g. texting app) and the 'html' text will go to the app that renders HTML (e.g. gmail app).

ACTUAL RESULT: The 'html' text goes to both apps/devices.

ADDITIONAL INFO: Please provide any additional information below.

Switching the order of specifying the parts, that is 'html' first, 'plain' second, both apps will get the 'plain' text. Using python library 'smtlib' to send the email.

Here are my installed modules:

REFERENCE: I had first tried to file the below bug but it was rejected as a bug against google-api-python-client https://issuetracker.google.com/issues/164400465

busunkim96 commented 4 years ago

Hi @dnintzel,

Would you mind trying out a similar request in the 'Try this API' interface on this page. This helps confirm if this is a client library issue or a API issue.

Could you also edit your code sample to include the relevant imports? I'm not very familiar with the Gmail API and am having trouble following where EmailMessage().

Thanks!

dnintzel commented 4 years ago

Hello, I am not clear how to fill out all the fields in the 'Try this API' interface for this use-case. Can you point me to examples of using multipart for plain and html body text?

Here is the complete code sample I am using. It is based on this sample and requires having pre-created the 'gmail-credentials.json' (or 'token.pickle')

"""
   DESCRIPTION:
      GMail API email option:
   This module enables sending an email through GMail using oauth2 to
   avoid password handling and better security.
   REFERENCE: https://developers.google.com/gmail/api/quickstart/python
"""
from googleapiclient.discovery import build
from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import Flow, InstalledAppFlow
from apiclient import errors
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import smtplib # Used to "send" an email 'message'

from email.message import EmailMessage # New to Replace MIMEText and MIMEMultipart
import base64
import pickle
import os

#****************************************************************************
# build_gmail_service:
#  If token file exists from a previously created call to
#  oauth2 from a web browser, read token, else launch web browser
#  to request for credentials. THIS CANNOT BE DONE ON HEADLESS SERVER!
#
#****************************************************************************
def build_gmail_service(client_secret_file = "gmail-credentials.json", api_service_name = "gmail", api_version = "v1", scopes = ["https://www.googleapis.com/auth/gmail.send/"]):
    print("client_secret_file:{}, api_service_name:{}, api_version:{}, scopes:{}".format(client_secret_file, api_service_name, api_version, scopes))

    cred = None
    client_secret_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),client_secret_file)
    if not os.path.exists(client_secret_file):
        print("//ERROR: missing file:{}".format(client_secret_file),log_level_class.ERROR)
        return None
    print("client_secret_file:{}, found.".format(client_secret_file))

    gmail_token_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),'gmail-token.pickle')
    if os.path.exists(gmail_token_file):
        with open( gmail_token_file, 'rb') as token: # Token created here: https://console.developers.google.com/apis/dashboard?authuser=1&project=empyrean-engine-285918
            cred = pickle.load(token)
    print("gmail_token_file:{}, found.".format(gmail_token_file))

    if not cred or not cred.valid:
        if cred and cred.expired and cred.refresh_token:
            cred.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                client_secret_file, scopes)
            cred = flow.run_local_server()

        with open( gmail_token_file, 'wb') as token:
            pickle.dump(cred, token)

    try:
        # Build the service here:
        service = build(api_service_name, api_version, credentials=cred)  
        print("{} service created successfully.".format(api_service_name))
        return service
    except Exception as e:
        print("//ERROR: Unable to create gmail api service to send email. Reason: {} ".format(e),log_level_class.ERROR)
    return None

#****************************************************************************
# send_email_from_gmail:
#  Calls build_gmail_service to retrieve oauth2 permissions to send gmail
#  Requires previous oauth2 authorization and network connection to check 
#  the permissions and send the email.
#****************************************************************************
def send_email_from_gmail(to_email=None,from_email=None, email_body=None, subject=None, password = None):
   CLIENT_SECRET_FILE = "gmail-credentials.json"  # oauth2 credentials created/downloaded from here: https://console.developers.google.com/apis/dashboard?authuser=1&project=empyrean-engine-285918
   SCOPES = ["https://www.googleapis.com/auth/gmail.send/"]    # Scope permission to send; See https://developers.google.com/gmail/api/v1/reference/users/messages/send#auth; ALL permissions : ["https://mail.google.com/"] (not needed)
   API_NAME = "gmail"
   API_VERSION = "v1"
   service = build_gmail_service(CLIENT_SECRET_FILE,API_NAME,API_VERSION,SCOPES)
   use_legacy = False #False #True
   # CONCLUSION FOR BELOW: Legacy vs. New doesn't matter...it seems to be the GMAIL-API that appears not to be sending both parts.
   if use_legacy:
      message = MIMEMultipart('alternative')
      #message = MIMEMultipart()  # This does not work with any combination of multi-part
      message['to'] = to_email
      message['from'] = from_email
      message['subject'] = subject
      # Note: Hardcoding body instead of using email_body for debug
      #Expect this to go to ascii devices/apps like when texting.
      message.attach(MIMEText('legacy plain','plain'))
      #Expect this to devices/apps that render HTML.
      message.attach(MIMEText("<u><b>legacy html:<br>content</b></u>",'html')) 
   else:
      message = EmailMessage()
      message['to'] = to_email
      message['from'] = from_email
      message['subject'] = subject
      #Expect this to go to ascii devices/apps like when texting.
      message.set_content("new plain text", subtype='plain')         
      #Expect this to devices/apps that render HTML.
      message.add_alternative("<u><b>new html:<br>content</b></u>", subtype='html')

   use_gmail = True #False #True
   if use_gmail:
      print("using gmail")
      # NOTE: Below will NOT send both parts using gmail
      CLIENT_SECRET_FILE = "gmail-credentials.json"  # oauth2 credentials created/downloaded from here: https://console.developers.google.com/apis/dashboard?authuser=1&project=empyrean-engine-285918
      SCOPES = ["https://www.googleapis.com/auth/gmail.send/"]    # Scope permission to send; See https://developers.google.com/gmail/api/v1/reference/users/messages/send#auth; ALL permissions : ["https://mail.google.com/"] (not needed)
      API_NAME = "gmail"
      API_VERSION = "v1"
      #service = build_gmail_service(CLIENT_SECRET_FILE,API_NAME,API_VERSION,SCOPES)
      service = build_gmail_service()
      if service is None:
         print("//ERROR: Unable to create gmail api service to send email: {} {} {} {}".format(CLIENT_SECRET_FILE,API_NAME,API_VERSION,SCOPES),log_level_class.ERROR)
         return False
      else:
         print("sending email using gmail")
         raw_string = base64.urlsafe_b64encode(message.as_bytes()).decode()
         request = service.users().messages().send(userId=from_email,body={'raw':raw_string})
         response_message = request.execute()
   else:
      # NOTE: Below will send both parts using smtplib
      print("using smtplib")
      server = smtplib.SMTP("smtp.gmail.com",587)
      print(" Connected to server!")

      server.ehlo()
      server.starttls()
      server.login(from_email,password)
      server.send_message(message)
      print('smtplib.sendmail - email sent!')
      server.quit()

   return True

#****************************************************************************
# test_gmail:
#  Test to send gmail
#****************************************************************************
def test_gmail():
   from_email = 'myemail@gmail.com'
   subject = "gmail api test"
   email_body="hello world"
   smtplib_password = 'mypsw'  # <<<NEEDS valid password

   to_email = 'myemail@gmail.com'  # <<<NEEDS valid email
   send_email_from_gmail(to_email = to_email, from_email = from_email, email_body=email_body, subject = subject, password = smtplib_password)

   to_email = '0123456789@messaging.sprintpcs.com'  # <<<NEEDS phone#@carrier-gateway.com for email-2-text
   send_email_from_gmail(to_email = to_email, from_email = from_email, email_body=email_body, subject = subject, password = smtplib_password)

test_gmail()
parthea commented 3 years ago

Hi @dnintzel ,

I'm going to close off this issue due to inactivity. If you're still having trouble, please re-open it and we can take a look.