How do I send HTML Formatted emails, through the gmail-api for python
Question:
Using the sample code from the GMail API Example: Send Mail, and after following rules for authentication, it’s simple enough to send a programmatically generated email, via a gmail account. What isn’t obvious from the example is how to set that email to be HTML formatted.
The Question
How do I get HTML formatting in my gmail-api send messages, using python?
I have this…
message_body = "Hello!nYou've just received a test message!nnSincerely,n-Test Message Generatorn"
and I want it to be this…
Hello!
You've just received a test message!
Sincerely,
-Test Message Generator
Example Source Code from GMail-API
Below is a slightly modified version of the example, but still works:
import argparse
import base64
from pprint import pformat
from pprint import pprint
import httplib2
import os
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from apiclient import discovery
from oauth2client import client
from oauth2client import tools
from oauth2client.file import Storage
SCOPES = 'https://mail.google.com/'
CLIENT_SECRET_FILE = 'client_secret.json'
APPLICATION_NAME = 'Test EMail App'
def get_credentials():
"""Gets valid user credentials from storage.
If nothing has been stored, or if the stored credentials are invalid,
the OAuth2 flow is completed to obtain the new credentials.
Returns:
Credentials, the obtained credential.
"""
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 = 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 create_message(sender, to, cc, subject, message_text):
"""Create a message for an email.
Args:
sender: Email address of the sender.
to: Email address of the receiver.
subject: The subject of the email message.
message_text: The text of the email message.
Returns:
An object containing a base64url encoded email object.
"""
print(sender + ', ' + to + ', ' + subject + ', ' + message_text)
message = MIMEText(message_text)
message['to'] = to
message['from'] = sender
message['subject'] = subject
message['cc'] = cc
pprint(message)
return {'raw': base64.urlsafe_b64encode(message.as_string())}
def send_message(service, user_id, message_in):
"""Send an email message.
Args:
service: Authorized Gmail API service instance.
user_id: User's email address. The special value "me"
can be used to indicate the authenticated user.
message: Message to be sent.
Returns:
Sent Message.
"""
pprint(message_in)
try:
message = (service.users().messages().send(userId=user_id, body=message_in).execute())
pprint(message)
print ('Message Id: %s' % message['id'])
return message
except errors.HttpError, error:
print ('An error occurred: %s' % error)
def main(cli):
"""Shows basic usage of the Gmail API.
Creates a Gmail API service object and outputs a list of label names
of the user's Gmail account.
"""
credentials = get_credentials()
http = credentials.authorize(httplib2.Http())
service = discovery.build('gmail', 'v1', http=http)
email_msg = create_message(cli.addr_from, cli.addr_to, cli.addr_cc, cli.subject, cli.message)
msg_out = service.users().messages().send(userId = 'me', body = email_msg).execute()
pprint(msg_out)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-m', '--message', help = 'The message to send in the email', default='<MESSAGE = unfinished>')
parser.add_argument('-t', '--addr_to', help = 'the list of comma separated emails to send', default='[email protected]')
parser.add_argument('-s', '--subject', help = 'the email subject', default='<SUBJECT = undefined>')
parser.add_argument('-c', '--addr_cc', help = 'email CC's', default='')
parser.add_argument('-f', '--addr_from', help = 'Email address to send from', default='[email protected]')
cli = parser.parse_args()
pprint(dir(cli))
main(cli)
Tried as I might, with this code, and variations on it, I could not get html formatted code, nor could I get simple escape characters to create carriage returns where they needed to be.
Here’s what didn’t work
Trying the following didn’t work either:
- modifying
line 69
to add additional message dictionary parameters… ie.
{'raw': base64.urlsafe_b64encode(message.as_string()), 'payload': {'mimeType': 'text/html'}}
- As documented here in the GMail API Docs
- Adding a variety of escaped backslashes into the message text:
n
… ie: \n
nor \n
and just rendered as those exact characters
- Adding
<br> </br> <br/>
did not add new lines and just rendered as those exact characters
- Adding
r
did not add new lines and just rendered as that exact character
Reasons this is not a duplicate question
- This SO link deals with multipart/signed messages
- I am uninterested in multipart messaging because it is an inelegant solution. I want the WHOLE message to be
HTML
.
- Additionally, this link has no accepted answer, and the one answer present is a non-answer as it simply states that they are sending a problem statement to Google.
- This one deals with c#, which I am not interested in since I’m writing in python
- This one is for Rails
Answers:
After doing a lot of digging around, I started looking in to the python side of the message handling, and noticed that a python object is actually constructing the message to be sent for base64 encoding into the gmail-api message object constructor.
See line 63 from above: message = MIMEText(message_text)
The one trick that finally worked for me, after all the attempts to modify the header values and payload dict (which is a member of the message
object), was to set (line 63
):
message = MIMEText(message_text, 'html')
<– add the 'html'
as the second parameter of the MIMEText object constructor
The default code supplied by Google for their gmail API only tells you how to send plain text emails, but they hide how they’re doing that.
ala…
message = MIMEText(message_text)
I had to look up the python class email.mime.text.MIMEText
object.
That’s where you’ll see this definition of the constructor for the MIMEText object:
- class email.mime.text.MIMEText(_text[, _subtype[, _charset]])
We want to explicitly pass it a value to the _subtype
. In this case, we want to pass: 'html'
as the _subtype
.
Now, you won’t have anymore unexpected word wrapping applied to your messages by Google, or the Python mime.text.MIMEText
object
The Fixed Code
def create_message(sender, to, cc, subject, message_text):
"""Create a message for an email.
Args:
sender: Email address of the sender.
to: Email address of the receiver.
subject: The subject of the email message.
message_text: The text of the email message.
Returns:
An object containing a base64url encoded email object.
"""
print(sender + ', ' + to + ', ' + subject + ', ' + message_text)
message = MIMEText(message_text,'html')
message['to'] = to
message['from'] = sender
message['subject'] = subject
message['cc'] = cc
pprint(message)
return {'raw': base64.urlsafe_b64encode(message.as_string())}
Try this:
def CreateMessage(emailSubject, emailTo, emailFrom, message_body, emailCc, html_content=None):
try:
message = MIMEMultipart('alternative')
message['to'] = emailTo
message['from'] = emailFrom
message['subject'] = emailSubject
message['Cc'] = emailCc
body_mime = MIMEText(message_body, 'plain')
message.attach(body_mime)
if html_content:
html_mime = MIMEText(html_content, 'html')
message.attach(html_mime)
return {
'raw': base64.urlsafe_b64encode(
bytes(
message.as_string(),
"utf-8")).decode("utf-8")}
except Exception as e:
print('Error in CreateMessage()', e)
return '400'
The accepted answer works, but in my journey down this rabbit hole, I found the right place to put the payload from the original question, so here’s a full example. I’m using a service account with domain-wide delegation:
import base64
from googleapiclient.discovery import build
from google.oauth2 import service_account
from email.message import EmailMessage
SCOPES = ['https://www.googleapis.com/auth/gmail.send']
CREDS = service_account.Credentials.from_service_account_file(
serviceAcct, scopes=SCOPES, subject=senderEmail)
with build('gmail', 'v1', credentials=CREDS) as service:
msg = EmailMessage()
content="Message body in <b>html</b> format!"
msg['To'] = recipientEmail
msg['From'] = senderEmail
msg['Subject'] = 'Gmail API test'
# Use this for plain text
# msg.set_content(content)
# Otherwise, use this for html
msg.add_header('Content-Type','text/html')
msg.set_payload(content)
encodedMsg = base64.urlsafe_b64encode(msg.as_bytes()).decode()
try:
sendMsg = service.users().messages().send(
userId=senderEmail,
body={ 'raw': encodedMsg }
).execute()
print('Msg id:', sendMsg['id'])
except Exception as e:
print('Error:', e)
Using the sample code from the GMail API Example: Send Mail, and after following rules for authentication, it’s simple enough to send a programmatically generated email, via a gmail account. What isn’t obvious from the example is how to set that email to be HTML formatted.
The Question
How do I get HTML formatting in my gmail-api send messages, using python?
I have this…
message_body = "Hello!nYou've just received a test message!nnSincerely,n-Test Message Generatorn"
and I want it to be this…
Hello!
You've just received a test message!
Sincerely,
-Test Message Generator
Example Source Code from GMail-API
Below is a slightly modified version of the example, but still works:
import argparse
import base64
from pprint import pformat
from pprint import pprint
import httplib2
import os
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from apiclient import discovery
from oauth2client import client
from oauth2client import tools
from oauth2client.file import Storage
SCOPES = 'https://mail.google.com/'
CLIENT_SECRET_FILE = 'client_secret.json'
APPLICATION_NAME = 'Test EMail App'
def get_credentials():
"""Gets valid user credentials from storage.
If nothing has been stored, or if the stored credentials are invalid,
the OAuth2 flow is completed to obtain the new credentials.
Returns:
Credentials, the obtained credential.
"""
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 = 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 create_message(sender, to, cc, subject, message_text):
"""Create a message for an email.
Args:
sender: Email address of the sender.
to: Email address of the receiver.
subject: The subject of the email message.
message_text: The text of the email message.
Returns:
An object containing a base64url encoded email object.
"""
print(sender + ', ' + to + ', ' + subject + ', ' + message_text)
message = MIMEText(message_text)
message['to'] = to
message['from'] = sender
message['subject'] = subject
message['cc'] = cc
pprint(message)
return {'raw': base64.urlsafe_b64encode(message.as_string())}
def send_message(service, user_id, message_in):
"""Send an email message.
Args:
service: Authorized Gmail API service instance.
user_id: User's email address. The special value "me"
can be used to indicate the authenticated user.
message: Message to be sent.
Returns:
Sent Message.
"""
pprint(message_in)
try:
message = (service.users().messages().send(userId=user_id, body=message_in).execute())
pprint(message)
print ('Message Id: %s' % message['id'])
return message
except errors.HttpError, error:
print ('An error occurred: %s' % error)
def main(cli):
"""Shows basic usage of the Gmail API.
Creates a Gmail API service object and outputs a list of label names
of the user's Gmail account.
"""
credentials = get_credentials()
http = credentials.authorize(httplib2.Http())
service = discovery.build('gmail', 'v1', http=http)
email_msg = create_message(cli.addr_from, cli.addr_to, cli.addr_cc, cli.subject, cli.message)
msg_out = service.users().messages().send(userId = 'me', body = email_msg).execute()
pprint(msg_out)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-m', '--message', help = 'The message to send in the email', default='<MESSAGE = unfinished>')
parser.add_argument('-t', '--addr_to', help = 'the list of comma separated emails to send', default='[email protected]')
parser.add_argument('-s', '--subject', help = 'the email subject', default='<SUBJECT = undefined>')
parser.add_argument('-c', '--addr_cc', help = 'email CC's', default='')
parser.add_argument('-f', '--addr_from', help = 'Email address to send from', default='[email protected]')
cli = parser.parse_args()
pprint(dir(cli))
main(cli)
Tried as I might, with this code, and variations on it, I could not get html formatted code, nor could I get simple escape characters to create carriage returns where they needed to be.
Here’s what didn’t work
Trying the following didn’t work either:
- modifying
line 69
to add additional message dictionary parameters… ie.{'raw': base64.urlsafe_b64encode(message.as_string()), 'payload': {'mimeType': 'text/html'}}
- As documented here in the GMail API Docs
- Adding a variety of escaped backslashes into the message text:
n
… ie:\n
nor\n
and just rendered as those exact characters- Adding
<br> </br> <br/>
did not add new lines and just rendered as those exact characters - Adding
r
did not add new lines and just rendered as that exact character
Reasons this is not a duplicate question
- This SO link deals with multipart/signed messages
- I am uninterested in multipart messaging because it is an inelegant solution. I want the WHOLE message to be
HTML
. - Additionally, this link has no accepted answer, and the one answer present is a non-answer as it simply states that they are sending a problem statement to Google.
- I am uninterested in multipart messaging because it is an inelegant solution. I want the WHOLE message to be
- This one deals with c#, which I am not interested in since I’m writing in python
- This one is for Rails
After doing a lot of digging around, I started looking in to the python side of the message handling, and noticed that a python object is actually constructing the message to be sent for base64 encoding into the gmail-api message object constructor.
See line 63 from above:
message = MIMEText(message_text)
The one trick that finally worked for me, after all the attempts to modify the header values and payload dict (which is a member of the message
object), was to set (line 63
):
message = MIMEText(message_text, 'html')
<– add the'html'
as the second parameter of the MIMEText object constructor
The default code supplied by Google for their gmail API only tells you how to send plain text emails, but they hide how they’re doing that.
ala…
message = MIMEText(message_text)
I had to look up the python class email.mime.text.MIMEText
object.
That’s where you’ll see this definition of the constructor for the MIMEText object:
- class email.mime.text.MIMEText(_text[, _subtype[, _charset]])
We want to explicitly pass it a value to the_subtype
. In this case, we want to pass:'html'
as the_subtype
.
Now, you won’t have anymore unexpected word wrapping applied to your messages by Google, or the Python mime.text.MIMEText
object
The Fixed Code
def create_message(sender, to, cc, subject, message_text):
"""Create a message for an email.
Args:
sender: Email address of the sender.
to: Email address of the receiver.
subject: The subject of the email message.
message_text: The text of the email message.
Returns:
An object containing a base64url encoded email object.
"""
print(sender + ', ' + to + ', ' + subject + ', ' + message_text)
message = MIMEText(message_text,'html')
message['to'] = to
message['from'] = sender
message['subject'] = subject
message['cc'] = cc
pprint(message)
return {'raw': base64.urlsafe_b64encode(message.as_string())}
Try this:
def CreateMessage(emailSubject, emailTo, emailFrom, message_body, emailCc, html_content=None):
try:
message = MIMEMultipart('alternative')
message['to'] = emailTo
message['from'] = emailFrom
message['subject'] = emailSubject
message['Cc'] = emailCc
body_mime = MIMEText(message_body, 'plain')
message.attach(body_mime)
if html_content:
html_mime = MIMEText(html_content, 'html')
message.attach(html_mime)
return {
'raw': base64.urlsafe_b64encode(
bytes(
message.as_string(),
"utf-8")).decode("utf-8")}
except Exception as e:
print('Error in CreateMessage()', e)
return '400'
The accepted answer works, but in my journey down this rabbit hole, I found the right place to put the payload from the original question, so here’s a full example. I’m using a service account with domain-wide delegation:
import base64
from googleapiclient.discovery import build
from google.oauth2 import service_account
from email.message import EmailMessage
SCOPES = ['https://www.googleapis.com/auth/gmail.send']
CREDS = service_account.Credentials.from_service_account_file(
serviceAcct, scopes=SCOPES, subject=senderEmail)
with build('gmail', 'v1', credentials=CREDS) as service:
msg = EmailMessage()
content="Message body in <b>html</b> format!"
msg['To'] = recipientEmail
msg['From'] = senderEmail
msg['Subject'] = 'Gmail API test'
# Use this for plain text
# msg.set_content(content)
# Otherwise, use this for html
msg.add_header('Content-Type','text/html')
msg.set_payload(content)
encodedMsg = base64.urlsafe_b64encode(msg.as_bytes()).decode()
try:
sendMsg = service.users().messages().send(
userId=senderEmail,
body={ 'raw': encodedMsg }
).execute()
print('Msg id:', sendMsg['id'])
except Exception as e:
print('Error:', e)