Image Image generated by AI

Introduction

In the world of email communication, SMTP (Simple Mail Transfer Protocol) is the backbone that allows emails to be sent from one server to another. While Python’s smtplib library offers an easy way to send emails, diving deeper into how SMTP works can give you a greater understanding of the underlying mechanics. In this post, we’ll build a simple SMTP client from scratch using Python’s socket and ssl libraries. Along the way, we’ll break down each concept to ensure you fully grasp what’s happening behind the scenes.


What Are Sockets? Link to heading

Sockets are the foundation of network communication in computer science. Think of a socket as an endpoint for sending and receiving data between two machines over a network. When you make a phone call, you need two phones connected by a network; in the same way, sockets connect computers for data exchange.

  • Stream Sockets (TCP): This type of socket provides a reliable, connection-oriented service, meaning that data sent over a stream socket arrives in the same order it was sent. This is crucial for applications like sending emails, where the integrity of the data must be preserved.

  • Datagram Sockets (UDP): In contrast, datagram sockets send data packets individually, without guaranteeing order or reliability. These are faster but less reliable and are typically used in real-time applications like video conferencing.

In our SMTP client, we’ll use stream sockets (TCP) because they ensure that our email data is transmitted reliably.


What Is SMTP? Link to heading

SMTP (Simple Mail Transfer Protocol) is a protocol used to send emails from one server to another. Protocols like SMTP define the rules for how data should be structured, transmitted, and received between machines. When you send an email, your client (e.g., Gmail, Outlook) communicates with an SMTP server, which then relays your message to the recipient’s email server.


Building the SMTP Client Link to heading

Now that we understand the basics, let’s start building our SMTP client. This client will connect to Gmail’s SMTP server, authenticate with your credentials, and send an email.

Step 1: Creating and Connecting the Socket Link to heading

The first step in building our SMTP client is to create a socket and connect it to Gmail’s SMTP server. Here’s how we do it:

import socket
import ssl
import base64
import os
import certifi
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

def smtp_client():
    smtp_server = 'smtp.gmail.com'
    smtp_port = 587

    # Create a socket object
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Connect to the SMTP server
    client_socket.connect((smtp_server, smtp_port))

    print("Connected to SMTP server.")

Explanation:

  • Socket Creation: We start by creating a socket using socket.AF_INET, which specifies IPv4 as the network protocol, and socket.SOCK_STREAM, which specifies that we are using TCP for reliable communication.

  • Connection: The connect() method connects the socket to the specified server (smtp.gmail.com) and port (587). Port 587 is commonly used for SMTP communications with TLS/SSL encryption.

By running this code, you establish a connection with Gmail’s SMTP server, opening a communication channel to send data.


Step 2: Communicating with the Server Link to heading

Once connected, we need to communicate with the server using specific SMTP commands. These commands follow a strict format that the server expects.

def send_command(sock, command):
    print(f'Sending: {command}')
    sock.sendall((command + '\r\n').encode())
    response = sock.recv(1024).decode()
    print(f'Received: {response}')
    return response

# Send EHLO command
send_command(client_socket, 'EHLO smtp.gmail.com')

Explanation:

  • Sending Commands: The send_command() function sends a command to the server. The command is a string, followed by \r\n, which represents a newline in network communication. This format is required by the SMTP protocol.

  • Encoding: The command string is converted to bytes using encode(), as sockets transmit data as byte sequences, not as plain text.

  • Receiving Response: The recv() method waits for a response from the server. The number 1024 specifies the maximum amount of data to receive in one go (in bytes).

  • EHLO Command: This command tells the server that the client is ready to start an SMTP session. It also provides the server with the client’s capabilities (e.g., STARTTLS, which we’ll use next).


Step 3: Securing the Connection Link to heading

In today’s internet, security is paramount. We need to ensure that the data (like your email content and credentials) sent to the server is encrypted. This is where TLS (Transport Layer Security) comes into play. TLS is the successor to SSL (Secure Sockets Layer) and provides a secure channel over which data can be transmitted.

# Send STARTTLS command
send_command(client_socket, 'STARTTLS')

# Create SSL context and load trusted certificates
context = ssl.create_default_context(cafile=certifi.where())

# Wrap the socket with SSL for encryption
secure_socket = context.wrap_socket(client_socket, server_hostname=smtp_server)

Explanation:

  • STARTTLS Command: This command is sent to the server to initiate a switch from a plain text communication channel to a secure, encrypted one using TLS.

  • SSL Context with Certifi: The ssl.create_default_context(cafile=certifi.where()) function creates an SSL context with trusted certificates loaded from the certifi package. certifi is a Python package that provides a collection of trusted root certificates, which helps avoid issues like the CERTIFICATE_VERIFY_FAILED error by verifying the server’s certificate.

  • Wrap Socket: We then wrap our existing socket (client_socket) with the SSL context. This converts our socket into a secure socket (secure_socket), which encrypts all data sent through it.


Step 4: Obtaining Google App Password Link to heading

Before we can authenticate with the Gmail SMTP server, you’ll need to generate a Google App Password. This is necessary because Google requires you to enable 2FA (Two-Factor Authentication) for added security, and once enabled, you won’t be able to use your regular password in the script. Instead, you’ll need an App Password.

Steps to Obtain a Google App Password:

  1. Enable 2-Step Verification

    • Go to Enable 2-Step Verification.
    • Sign in to your Google Account if prompted.
    • Follow the on-screen instructions to set up 2-Step Verification.
  2. Create an App Password

    • Go to Create App Passwords.
    • Sign in to your Google Account if prompted.
    • Select the app and device from the dropdown menus.
    • Click “Generate” to get your 16-digit app password.
  3. Store the App Password Securely:

    • Copy this password and store it securely in your .env file as the GMAIL_PASSWORD variable.
    • Replace your regular password with this App Password in your script.

Note: Never share your App Password publicly or include it directly in your code.


Step 5: Authenticating with the Server Link to heading

Before sending an email, we need to prove to the SMTP server that we are who we claim to be. This is done through authentication using your Gmail credentials.

username = os.getenv('GMAIL_USERNAME')
password = os.getenv('GMAIL_PASSWORD')

send_command(secure_socket, 'AUTH LOGIN')
send_command(secure_socket, base64.b64encode(username.encode()).decode())
send_command(secure_socket, base64.b64encode(password.encode()).decode())

Explanation:

  • Environment Variables: We use os.getenv() to securely load your Gmail credentials (username and password) from environment variables stored in a .env file. This is a safer practice than hard-coding credentials in your script.

  • AUTH LOGIN Command: This command tells the server that the client wants to authenticate using a username and password.

  • Base64 Encoding: SMTP requires the username and password to be encoded in base64 before sending. Base64 is a method of encoding binary data as text, which is necessary for transmitting it over protocols like SMTP.

  • Sending Credentials: The base64-encoded username and password are sent to the server. If the server accepts them, it will allow you to send emails.


Step 6: Sending the Email Link to heading

With authentication successful, we can now compose and send the email.

from_address = username
to_address = 'recipient@example.com'
subject = 'Test Email'
message = 'This is a test email sent from a custom SMTP client.'

send_command(secure_socket, f'MAIL FROM:<{from_address}>')
send_command(secure_socket, f'RCPT TO:<{to_address}>')
send_command(secure_socket, 'DATA')
send_command(secure_socket, f'Subject: {subject}\r\nTo: {to_address}\r\n\r\n{message}\r\n.')

Explanation:

  • MAIL FROM Command: Specifies the sender’s email address. This is the address that will appear in the “From” field of the recipient’s inbox.

  • RCPT TO Command: Specifies the recipient’s email address.

  • DATA Command: Tells the server that the following lines contain the email’s content (headers and body).

  • Email Composition: The email’s subject, recipient, and body are sent as part of the data section. The \r\n. sequence marks the end of the message, signaling the server that it’s time to send the email.


Step 7: Closing the Connection Link to heading

After the email is sent, it’s important to close the connection gracefully.

send_command(secure_socket, 'QUIT')
secure_socket.close()

Explanation:

  • QUIT Command: This command ends the SMTP session and tells the server to close the connection.
  • Close Socket: The close() method closes the socket, freeing up system resources.

Full Code Link to heading

Here’s the complete code for the SMTP client:

import socket
import ssl
import base64
import os
import certifi
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

def send_command(sock, command):
    print(f'Sending: {command}')
    sock.sendall((command + '\r\n').encode())
    response = sock.recv(1024).decode()


    print(f'Received: {response}')
    return response

def smtp_client():
    smtp_server = 'smtp.gmail.com'
    smtp_port = 587

    # Create a socket object
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Connect to the SMTP server
    client_socket.connect((smtp_server, smtp_port))
    print("Connected to SMTP server.")

    # Send EHLO command
    send_command(client_socket, 'EHLO smtp.gmail.com')

    # Send STARTTLS command
    send_command(client_socket, 'STARTTLS')

    # Create SSL context and load trusted certificates
    context = ssl.create_default_context(cafile=certifi.where())

    # Wrap the socket with SSL for encryption
    secure_socket = context.wrap_socket(client_socket, server_hostname=smtp_server)

    # Authenticate with the server
    username = os.getenv('GMAIL_USERNAME')
    password = os.getenv('GMAIL_PASSWORD')

    send_command(secure_socket, 'AUTH LOGIN')
    send_command(secure_socket, base64.b64encode(username.encode()).decode())
    send_command(secure_socket, base64.b64encode(password.encode()).decode())

    # Send the email
    from_address = username
    to_address = 'recipient@example.com'
    subject = 'Test Email'
    message = 'This is a test email sent from a custom SMTP client.'

    send_command(secure_socket, f'MAIL FROM:<{from_address}>')
    send_command(secure_socket, f'RCPT TO:<{to_address}>')
    send_command(secure_socket, 'DATA')
    send_command(secure_socket, f'Subject: {subject}\r\nTo: {to_address}\r\n\r\n{message}\r\n.')

    # Close the connection
    send_command(secure_socket, 'QUIT')
    secure_socket.close()

if __name__ == "__main__":
    smtp_client()

.env file

GMAIL_USERNAME=example@gmail.com
GMAIL_PASSWORD=app_password

Conclusion

By following this guide, you’ve gained a deep understanding of how to create a simple SMTP client in Python using sockets and SSL/TLS encryption. You’ve learned about the key concepts involved, such as sockets, SMTP commands, SSL/TLS, and the importance of securing data transmission. This foundational knowledge not only helps you understand how emails are sent but also provides a stepping stone to more advanced topics in network programming.