Getting Started
I'll assume you are a Python programmer; or, are at least familiar with the Python programming language and already have it installed and running on the command line.
The tools we will be using in this tutorial are:
- Python
- telnet
- Gmail
Python3
This tutorial will be utilizing the Python programming language. And the version we will be using is Python version three (3).
You can see if you have Python 3 installed by executing the python binary in a shell:
python --version
It should display something like "Python 3.10.4" with a three (3) as the first number which means that you are running Python version 3.
Note: Sometimes the binary is named "python3", note the "3" on the end.
Client / Server
To send an e-mail we need to use a mail client which sends messages to a mail server which distributes them to the computer designated with the recipient's address.
Local SMTP Server
To get started let's spin up a testing server locally that we can use to make sure our client is working properly. We will configure it to show us the messages it receives only; and, not actually send off the e-mails.
To start our e-mail server run the following command in a terminal (shell).
python -m smtpd --nosetuid --class DebuggingServer localhost:8025
This command will run Python and import the smtpd module as a non-root user to create a new debugging server which will print the received messages to standard out and then discard them.
Here is a breakdown of the above command:
- python = the Python binary (executable program)
- -m smtpd = tell Python we want to use the smtpd module
- --nosetuid = required if not running as root
- --class DebuggingServer = the concrete SMTP proxy class
- localhost:8025 = server listener address and port
Note: Once we are done with the server we will stop it with Ctrl-C (Cmd-C on Mac).
Deprecation Warning
The smtpd module has been deprecated since 2017 and depending on your version of Python may issues warnings like:
- /usr/lib/python3.10/smtpd.py:105: DeprecationWarning: The asyncore module is deprecated and will be removed in Python 3.12. The recommended replacement is asyncio
The server will still work fine for our purposes but the technology used is now a bit old.
Note: If you got an error that the port is busy then you can use another port, for example 8125. If you do change the port number for the server then make sure your clients are also using that port number. We will setup our clients later.
Upgrade
It is not needed; but, if you would like to run the recommended replacement for smtpd and you have pip on your system then you can install aiosmtpd:
pip install aiosmtpd
Note: If you have the smtpd server running then stop it now with Ctrl-C (Cmd-C on Mac).
Then run the aiosmtpd server as:
python -m aiosmtpd --nosetuid -l localhost:8025
Telnet Client
Let's test to make sure that our e-mail server is listening. For this we will use the networking client telnet.
Note: If you don't have telnet installed then you can install it or you can skip ahead to the Python Client section below.
Step 1: Start telnet client
Open a separate terminal and enter the following command:
telnet localhost 8025
Note: If you get a "Connection refused" error then make sure you are using the correct hostname and port.
You should get connected to our e-mail server and receive some messages similar to but probably not exactly like the following:
- Trying ::1...
Connected to localhost.
Escape character is '^]'.
220 Python SMTP proxy version 0.3
Now let's try to simulate an e-mail with the server.
Note: Remember that we have the server configured to not actually send any mail just yet.
Step 2: Identify yourself
First we will identify ourselves with the server. Typically we use the hostname of the machine we are currently using:
HELO abstract.example.com
The server may respond with some informational messages or perhaps nothing.
Note: Since we are not not going to use any extended commands for authenticating or security we will not use the ELHO command but instead the HELO command as above.
Step 3: Configure transfer addresses
We will configure two (2) addresses: the sender and the recipient.
Use the MAIL command to setup our sender:
MAIL FROM: johnny.goode@example.com
You should get a message back saying that the command was executed ok. Your server may respond slightly differently but it should start with "250" and end with "ok":
- 250 OK
Now let's setup the recipient with the RCPT command:
RCPT TO: peter.pumpking@example.com
Again you should get back an ok:
- 250 OK
Note: It is possible to send to multiple recipients by using the RCPT command multiple times.
Step 4: Enter data
Now use the DATA command to tell the server that we want to start typing our message:
DATA
It should give us a response like:
- 354 End data with <CR><LF>.<CR><LF>
Note: The particular 354 message above is meant to inform us that when we're done typing our message that we should:
- hit enter (Carriage Return / Line Feed),
- type a period and
- then hit enter again.
The data we are going to type in should contain three parts:
- headers,
- message body,
- terminator character.
The headers are what the recipient's e-mail client will see and typically contain at least:
- from email address,
- to email address,
- subject line,
- date.
Note: Most servers will verify matching addresses that we entered in the sender address via the RCPT command and the from address in the header.
Enter the Headers
From: "J. Goode" <johnny.goode@example.com>To: "Mr. Pumpking" <peter.pumpking@example.com>Subject: Hello World!Date: Mon, 30 May 2022 08:09:00 -0400
Enter the Message Body
RFC821 defined SMTP messages to only use 7-bit characters so you can only use ASCII characters, basically just what you see on a standard keyboard; in other words, no fancy Unicode symbols. We'll get around this limitation later using Content-Transfer-Encoding.

In the message body below notice the blank line at the top and the period at the bottom:
Hello World, I am writing to tell you that I am alive!Yours very truly,Johnny B. Goode.
Again you should get back an ok:
- 250 OK
After you enter the DATA command and type the headers, the blank line, the body and the terminating dot; as soon as you hit enter after the period telnet will send the payload to the server.
Note: The ending period must be on a line all by itself, i.e. no leading or trailing spaces.
Step 5: Check the server
If we go back to our other terminal we should see that our SMTP server echoed our message as strings of bytes:
- ---------- MESSAGE FOLLOWS ----------
b'From: "J. Goode" <johnny.goode@example.com>'
b'To: "Mr. Pumpking" <peter.pumpking@example.com>'
b'Subject: Hello World!'
b'Date: Mon, 30 May 2022 08:09:00 -0400'
b'X-Peer: ::1'
b''
b'Hello World,'
b' I am writing to tell you that I am alive!'
b''
b'Yours very truly,'
b'Johnny B. Goode'
------------ END MESSAGE ------------
Step 6: Quit telnet
Now back in our client terminal we can quit telnet with the QUIT command:
QUIT
The entire SMTP server setup and test message should look something like:

Python Client
The Python standard library contains the smtplib module for sending e-mail using the Simple Mail Transfer Protocol (SMTP). We will use the email module to package our mail and the smtplib module to send it.
Step 1: Import the modules
Create a script file called "local.py".
The Style Guide for Python Code, a.k.a PEP-8, states:
- Imports are always put at the top of the file...
Enter the following lines into our script file:
Step 2: Create the message
Instantiate an object to hold our e-mail message:
Enter in our headers:
Configure the message content:
Step 3: Create the transport
Create an object to communicate with our SMTP server. Note that we are passing an argument for the hostname (localhost) and port (8025) of our SMTP server:
Use the send_message method to send our message object to the server:
Instruct our server object to terminate the connection:
Step 4: Run the script
Save the script file and go to our client terminal.
Note: Make sure you have the normal operating system prompt. If you are instead still in the telnet app then exit with Ctrl-C (Cmd-C on Mac).
Run the script:
Step 5: Check the server
If we go back to our server terminal we should see that it echoed our message:
- ---------- MESSAGE FOLLOWS ----------
b'Subject: Hello Python World!'
b'From: J. B. Goode <jgoode@example.com>'
b'To: Pump King <peter.pumpking@example.com>'
b'Content-Type: text/plain; charset="utf-8"'
b'Content-Transfer-Encoding: 7bit'
b'MIME-Version: 1.0'
b'X-Peer: ::1'
b''
b'Hello World,'
b' I am writing to tell you that I am alive!'
b''
b'Yours very truly,'
b'Johnny B. Goode'
------------ END MESSAGE ------------
Full Version
The full version of the script should look like this:
The terminal session should look something like:

We are done with our SMTP server so we can stop it with Ctrl-C (Cmd-C on Mac).
The Real Deal: Gmail
So far in this tutorial, we have started a local SMTP email service and tested it with a telnet client as well as a Python client. Now let's create another Python script to actually send e-mail.
The rest of this tutorial will require you to have a Google account for gmail.com.
Even if you have a Gmail account for personal use it might be a good idea to setup a new one for the purposes of this tutorial. You don't want to accidentally expose your real account password. And in order to allow 3rd-party apps like our Python script to access Gmail, security configuration is necessary.
Google Security
We will be sending e-mail with Gmail. So the first thing we need to do is setup Google security.
On May 30th, 2022 Gmail removed the setting to allow "less secure" applications to access an account with just username and password.

What this means is that you have to enable some other means of account security.
One way to do it is to use API Authentication with OAuth2 which Google documents here.
Another way, which I will describe here, is to turn on two-factor authentication.
Two-factor Authentication
In order to allow our Python client script to access Gmail accounts we will enable two-factor authentication (2FA) which Google calls "2-step verification".
With 2FA you add an extra layer of security to your account in case your password is stolen. After you set it up, you’ll sign in to your account in two steps using:
- something you know, like your password and
- something you have, like your phone.
To enable 2FA:
- Open your Google Account.
- In the navigation panel, select Security.
- Under “Signing in to Google,” select 2-Step Verification


Click the blue GET STARTED button:

Login using your password. Make sure you are using the right account.

Enter your phone number and click SEND:

Wait for the confirmation code to arrive and enter it in:

Click the TURN ON button:

2FA should now be enabled:

App Passwords
Now we will generate a special password that our app can use as the 2-step verification.
Back in the Signing in to Google panel select "App passwords" to add a new password:

Under the "Select app" drop-down select "Other":

Give your app a name and click GENERATE:

Write down (copy to clipboard) your generated password to use in the app:

We should now have our generated app password:

Script: Plain Text
Create a file called "gmail.py" and enter the following script:
The code above creates a secure connection with Gmail’s SMTP server using Secure Sockets Layer encryption (SSL) and automatically upgrades it to TLS. Passing in the context from the create_default_context() function will load the system’s trusted CA certificates, enable certificate validation and hostname checking and try to choose reasonably secure protocol and cipher settings.
Notice how the message variable actually contains a newline as the first character.
Note: Be sure to fill in your: password, sender and receiver strings.
Save the file and go to your terminal and run the script:
You shouldn't get any output in the terminal but go to your e-mail reader, for example gmail.com in a browser and verify that you got the e-mail.
You may notice that the from & to-addresses are missing and the subject is blank. You may have also noted that they were not blank when we sent test messages to our local SMTP server. So what's different?
The answer lies in the headers. But where are the headers?
Remember the message we sent via telnet?
The lines before the first double "new-line" characters, the so-called blank line, are the headers:
And everything after the first blank line is the message body:
The ending dot is something that we used to tell telnet that we're done typing our message.
Here is the line of our script that actually sends the mail once we've logged in:
So the sendmail function uses the server and receiver fields to route the message to the target computer. But apparently e-mail clients rely on the headers for their display information.
So let's add some headers. Edit gmail.py and update the message variable to:
Notice the backslash \ as the first character of our string which means no new-line if that backslash is the last character.
Save the file and go to your terminal and run the script again:
Now when Peter Pumpking gets this e-mail he will see the subject line and his name & address as well as who the mail was from.

But wait a minute.
What did you have for the sender and receiver?
In the From field most e-mail clients will take the name from the header but the address from the sender. In the To field it takes the whole thing from the header.
Script: HTML
We can use MIME to send HTML in our messages. Here's how.
Import two new modules from the email.mime package:
After our password and transport variable definitions, setup a new multi-part message object:
By specifying the "alternative" subtype we dictate that each of the attached parts is an alternative version of the same information. User agents should recognize that the content of the various parts are interchangeable.
Create a variable to hold our message with HTML content:
Now we will create a variable to hold an alternative plain-text version of our message in case the recipient's e-mail client is not able to render HTML:
From the W3C Multipart Protocol:
In general, user agents that compose multipart/alternative entities should place the body parts in increasing order of preference, that is, with the preferred format last. For fancy text, the sending user agent should put the plainest format first and the richest format last. Receiving user agents should pick and display the last format they are capable of displaying.
NOTE: From an implementor's perspective, it might seem more sensible to reverse this ordering, and have the plainest alternative last. However, placing the plainest alternative first is the friendliest possible option when mutlipart/alternative entities are viewed using a non-MIME- compliant mail reader. While this approach does impose some burden on compliant mail readers, interoperability with older mail readers was deemed to be more important in this case.
Connect with server and send the message:
Here is the full version:
Script: Attachments - Sending email with an attachment
We have already used attachments when we sent the HTML message but we specified the multipart subtype to be "alternative" which instructs the e-mail client to render only one part, the "best" part. Now we will specify the "mixed" subtype to say that each part should be rendered and in addition, each part might be of a different type, for example, one part text and another part a binary image.
Import one new module from the email.mime package:
After our transport variable definitions let's update the subject field:
After the message creation let's attach a simple plain text message:
If we have an image called "mail.jpg" in the images directory then let's attach it to our message:
Save the file and run it again and check your mail.
You should get something like:

The full version should look like:
Wrapping Up
In this tutorial, we covered setting up and testing a local SMTP server as well as sending emails with Python. We used Gmail to send HTML and images; well, one image at least.
FAQ
Are there any good libraries that make sending emails with Gmail easier?
Yagmail is a GMAIL/SMTP client that aims to make it as simple as possible to send emails. It has a simple to use API and it is actively maintained. You can find the code at https://pypi.org/project/yagmail/.
What does a Transactional E-mail Service do?
It is a service that will send e-mail when certain transaction events occur. For example you can set one up to automatically send a customer an invoice or receipt when they buy something from you online. Examples include: Sendinblue, SendGrid, Amazon SES, Mailgun, Mailjet, and Postmark.