Introduction
In this post, I will show you how to send an HTML email to a list of users using Python with Google API libraries.
Use cases:
You have to send an email to you or the Helpdesk, Admin mail group individually when some triggers occur. (Monitoring systems like Zabbix)
You have to send an email to the list of users individually for some reason (marketing, notification, announcement, etc.)
Your application needs some function to send the email.
Or for studying.
Send Mail to the list of Recipients
Prerequisites
Libraries
google-api-python-client
google-auth-httplib2
google-auth-oauthlib
base64
concurrent.futures
time
Install libraries as the below command
pip3 install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
Import the necessary libraries to the Python script.
import base64
import csv #read the list of user in CSV file
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
Create OAuth 2.0 credentials
- Go to the Google API Console (https://console.developers.google.com/) and create a new project by clicking on the project dropdown menu at the top of the page and selecting “New Project”. Enter a name for your project and click “Create”.
Once the project is created, you will be redirected to the project dashboard. Make sure your newly created project is selected from the project dropdown menu at the top.
Enable the Gmail API for your project.
Click on the “Enable APIs and Services” button to go to the library page.
In the search bar, type “Gm scail API” and select the “Gmail API” from the search results.
On the Gmail API page, click the “Enable” button to enable the API for your project.
Go to the Credentials tab and create credentials for an OAuth 2.0 client ID.
Download the client secrets JSON file, which contains the client ID and client secret.
Scripts
We will define some small functions through the script and test the code in a single script. You can import the functions to your application or use the script to run through some triggers later.
- gmail_credentials: this function is used to set up OAuth2 credentials with Google Authorized Server.
OAuth 2.0 is an open standard protocol that allows third-party applications to access user data from a service (like Google, or Microsoft Azure) on behalf of the user without requiring the user to share their credentials.
Read more: What is OAuth2 and How it works?
def gmail_credentials():
# Set up OAuth2 credentials
try:
creds = Credentials.from_authorized_user_file('token.json')
except FileNotFoundError: #-- token.json file does NOT exist --#
#-- generate token by authorizing via browser (1st time only, I hope so :D) --#
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', #credentials JSON file
['https://www.googleapis.com/auth/gmail.send']
)
creds = flow.run_local_server(port=0)
#-- token.json exists --#
if creds and creds.valid:
pass
#-- token is expired--#
elif creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
# Save the credentials as token
with open('token.json', 'w') as token_file:
token_file.write(creds.to_json())
# return the creds
return creds
The “creds” variable will be created by reading the token.json file via the Credentials function from the module google.oauth2.credentials.(Line 4)
The very first time you run the scripts, you will only have the credentials.json file which is created via Google API Console.
Thus, the token.json file might not exist, and the FileNotFoundError exception might occur, the try-except block will solve this problem. (lines 3-5).
In the “except” block, we need to authenticate and authorize the application and then obtain the token.json file using credentials.json.
The credentials.json will be downloaded and saved in the same directory with the running script. token.json will be saved the same in this example. For security reasons, you need to consider how to store them in your application structure.
gmail_compose: this function will generate the email message content in a single object (dictionary) encoded with base64.
Object components:
Subject
From
To
Body
Content-Type
Attachments
MIME version
MIME stands for Multipurpose Internet Mail Extension.
Read more: What is MIME?
def gmail_compose(mail_subject, email_recipient, mail_body):
message = {
'raw': base64.urlsafe_b64encode(
f'MIME-Version: 1.0\n'
f'Content-Type: text/html; charset="UTF-8"\n'
f"From: itbase.tv@gmail.com\n"
f"To: {email_recipient}\n"
f"Subject: {mail_subject}\n\n"
f"{mail_body}"
.encode("utf-8")
).decode("utf-8")
}
return message
The function receives the mail_subject, email_recipient, and mail_body as input.
Inside the function, a dictionary named message is created.
The raw key of the message dictionary is assigned a value that is the Base64 encoded representation of the email content.
The email content is formatted using f-strings and includes the necessary MIME-Version, Content-Type, sender, recipient, subject, and body information. In this example, I use HTML format, so the Content-Type will be ‘text/html’.
The email content is encoded using UTF-8 and then encoded with Base64 using the base64.urlsafe_b64encode function.
The encoded email content is then decoded to a UTF-8 string using the .decode(“utf-8”) method.
The dictionary “message” will be returned.
- gmail_send: this function will build the mail service using the input credentials and the message which is returned in the above gmail_compose function.
def gmail_send(creds, message):
# Send the email
service = build('gmail', 'v1', credentials=creds)
try:
service.users().messages().send(userId='me', body=message).execute()
print('Email sent successfully.')
except Exception as e:
print('An error occurred while sending the email:', str(e))
Service has been built by build() function in googleapiclient.discovery module. The send() method uses input userId and body to execute the mail send. Note that the userId=’me’ this time will use your email id as the sender.
if __name__ == "__main__":
#-Mail Subject-#
mail_subject = "[itbaseTV Newsletter] Traditional IPSec vs. SDWAN"
#-Build HTML content in single line with "space" as separater-#
mail_body = open('content.html', 'r').read()
mail_body = mail_body.replace('\n',' ')
#--create mail services--#
creds = gmail_credentials()
#--Read line by line in csv, each line includes one user's mail address, and send mail to them.--#
with open('user_mail_lists.csv', 'r') as csv_file:
reader = csv.DictReader(csv_file)
for user in reader:
email_recipient = user['user_email']
#SEND MAIL#
mail_content = gmail_compose(mail_subject, email_recipient, mail_body)
gmail_send(creds, mail_content)
##############
## OPTIONAL ##
##############
#-- You can use below block for multiple threads send mail to optimize the process in case you have--#
#-- the long list of users --#
import time
import concurrent.futures #for multi threads
with open('user_mail_lists.csv', 'r') as csv_file:
reader = csv.DictReader(csv_file)
recipients = [user['user_email'] for user in reader]
# Create a thread pool executor
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = []
for recipient in recipients:
# Compose the email
mail_content = gmail_compose(mail_subject, recipient, mail_body)
# Submit the email sending task to the executor
future = executor.submit(gmail_send, creds, mail_content)
futures.append(future)
time.sleep(1) #some limitation of Google API, could not send a lot of email in short time.
# Wait for all tasks to complete
concurrent.futures.wait(futures)
Let’s run the test by the above block of script.
Define the mail_subject as a string and mail_body as and single line string including HTML format code. (in content.html)
Look into content.html, you can easily create the HTML using UI online free tool
<!-- ####### MY EMAIL CONTENT AS HTML #########-->
<h1 style="text-align: center;">[Cisco SDWAN]</h1>
<h1 style="text-align: center;"><span style="color: #ff6600;">Traditional</span> IPSec vs. <span style="color: #00ff00;">SDWAN</span></h1>
<p><a href="https://www.itbase.tv/traditional-ipsec-vs-sdwan/" target="_blank"><img style="display: block; margin-left: auto; margin-right: auto;" src="https://www.itbase.tv/wp-content/uploads/2023/05/traditional-ipsec-1024x480.png" alt="IPSec" width="667" height="312" /></a></p>
<h1 class="wp-block-heading" style="text-align: center;">Introduction</h1>
<p style="text-align: center;">Cisco SDWAN’s Data plane uses IPSec as a secure communication method, the question is that</p>
<p style="text-align: center;">What is the difference between IPSec running on an SDWAN Data plane and Traditional IPSec?</p>
<h1 class="wp-block-heading" style="text-align: center;"><span id="Traditional_IPSec" class="ez-toc-section"></span><strong>Traditional IPSec</strong></h1>
<p style="text-align: center;">ISAKMP is the negotiation protocol that lets two hosts agree on how to build an IPsec security association (SA).</p>
<p style="text-align: center;">ISAKMP separates negotiation into two phases: <strong>Phase 1</strong> and <strong>Phase 2</strong>.</p>
<p style="text-align: center;"><strong><a href="https://www.itbase.tv/traditional-ipsec-vs-sdwan/">Read more</a></strong></p>
<p style="text-align: center;"> </p>
<hr />
<p style="text-align: center;"><a href="https://www.linkedin.com/in/nam-nguyen95/" target="_blank"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhbkcZ2V9bXNv8OPU8of3K0jPtonBBPVxk63O0AdShu4N3mLEnKSjTT_mvep1NqaP81OQJzUUwsNfU4s1LqKTioWVo2gHy-SbQ8e-qEsDDVJG5ZZSsg4FTZCBstCBGTIzXZkGv_oR_sXSiZnkgfwIZe-ZSZNFoUHI9wPZBIbKJ3q3RyW2HotMu247CH/s512/linkedin%20icon.png" alt="" width="28" height="28" /> </a><a href="https://www.youtube.com/@itbazetv" target="_blank" rel="noopener"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgz21KQlna0QoFbf6g7nT1MnDYkc55o_nhPPTyF4EF_daU9VAeXAOy5eqJ0myS3582elSPFlNnFaDNfz2GuPVzBFif1ew5Oy2lzQDfSBqsQ53MFgSgGumOfFBwedwzdauAv0QwFVf0E7o1heA1IoBtwtM0rDW24hCPCykh7cOjGDLnzpntQezjnUiUG/w72-h72/youtube.png" alt="" width="30" height="30" /></a></p>
It will look like this, the pretty email content, right?
[Cisco SDWAN]
Traditional IPSec vs. SDWAN
Introduction
Cisco SDWAN’s Data plane uses IPSec as a secure communication method, the question is that
What is the difference between IPSec running on an SDWAN Data plane and Traditional IPSec?
Traditional IPSec
ISAKMP is the negotiation protocol that lets two hosts agree on how to build an IPsec security association (SA).
ISAKMP separates negotiation into two phases: Phase 1 and Phase 2.
Comeback to the code, I use the CSV module to read the list of users and get their email by the CSV Header user_email.
And then each time read the line, I perform to call functions gmail_compose and gmail_send.
Note: Below is the List of User CSV content which “comma” as a separator.
user_email,display_name
info@itbase.tv,Test1
itbase.tv@gmail.com,Test2
Validate
(SendMail) [www.itbase.tv] Roger@~/GmailAutomation/
$ python gmail-api-send-mail.py
Email sent successfully.
Email sent successfully.
The emails were sent to all users, one of the examples below.
Conclusion
I hope you enjoy the blogs and have an overview understanding of how to create your script to send emails for your purpose.
The full script and related things will be pushed to Github.