Sign In
Cold Outreach

What steps show how to do email automation?

How to Do Email Automation with Python and Gmail

Email automation can significantly streamline your communication, freeing up valuable time and resources. This article provides a practical guide to automating email sending using Python and Gmail. We’ll focus specifically on sending personalized welcome emails to new subscribers through a dedicated Gmail account. You’ll learn how to configure your Gmail account for programmatic access, build the Python script, and then explore methods for trigger-based sending, making it a complete solution for welcome email automation. By the end of this article, you’ll have the knowledge and tools to implement an efficient email automation system tailored to your needs.

Setting Up Gmail for Automation

Before you can start automating emails with Python, you need to configure your Gmail account to allow programmatic access. Google prioritizes security, so you can’t just use your standard password directly in your script. There are two primary methods for allowing external access: enabling “less secure app access” (which is strongly discouraged for security reasons and is being deprecated) or, the preferred method, creating an “App Password”. We will focus on the more secure “App Password” approach.

Example 1: Creating an App Password

  • Enable 2-Step Verification: You must first enable 2-Step Verification on your Google account. Go to your Google Account settings, then Security, and follow the prompts to set up 2-Step Verification.
  • Generate an App Password: Once 2-Step Verification is enabled, go back to the Security settings and look for “App passwords”. Click on it and sign in again.
  • Select the App and Device: In the “Select app” dropdown, choose “Mail”. In the “Select device” dropdown, choose “Other (Custom name)”. Give it a descriptive name like “Python Email Script”.
  • Generate the Password: Click “Generate”. Google will provide a 16-character App Password. Important: Copy this password and store it securely. You will only see it once.
  • Store the Password Securely: Never hardcode the App Password directly into your Python script. Use environment variables or a secure configuration file.

The App Password is specifically for your Python script and is separate from your main Gmail password. If the script is compromised, you can revoke the App Password without affecting the security of your main Gmail account. This is far safer than enabling “less secure app access,” which makes your entire account vulnerable.

Example 2: Setting Environment Variables (Linux/macOS)

A common approach is to set the App Password as an environment variable. This allows your script to access the password without it being directly embedded in the code.

# In your terminal (e.g., bash, zsh)
export GMAIL_APP_PASSWORD="your_app_password"
export GMAIL_USERNAME="your_gmail_address@gmail.com"

Replace your_app_password with the actual App Password you generated, and your_gmail_address@gmail.com with the Gmail address you’ll be sending emails from. These variables will only be set for the current terminal session. To make them permanent, you’ll need to add these lines to your shell’s configuration file (e.g., .bashrc, .zshrc, .profile).

Example 3: Setting Environment Variables (Windows)

On Windows, you can set environment variables through the System Properties dialog.

  • Search for “Environment Variables” in the Start Menu.
  • Click on “Edit the system environment variables”.
  • Click the “Environment Variables…” button.
  • Under “User variables” or “System variables”, click “New…”.
  • Enter “GMAIL_APP_PASSWORD” as the Variable name and your App Password as the Variable value.
  • Repeat for “GMAIL_USERNAME” with your Gmail address as the value.
  • Click “OK” on all dialogs to save the changes. You may need to restart your command prompt or IDE for the changes to take effect.

Once the environment variables are set, your Python script can retrieve them using the os module.

Expert Tip: Always prioritize security when handling sensitive information like passwords. Using App Passwords and environment variables is a significant step in protecting your Gmail account and your code.

Writing the Python Script

With your Gmail account configured, the next step is to write the Python script to send emails. We’ll use the smtplib and email libraries, which are part of Python’s standard library. This eliminates the need to install external packages for basic email functionality.

Example 1: Basic Email Sending Script

import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

def send_email(recipient_email, subject, body):
    sender_email = os.environ.get("GMAIL_USERNAME")
    sender_password = os.environ.get("GMAIL_APP_PASSWORD")

    message = MIMEMultipart()
    message["From"] = sender_email
    message["To"] = recipient_email
    message["Subject"] = subject

    message.attach(MIMEText(body, "plain")) # or "html"

    try:
        with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server: # Use port 465 for SMTP_SSL
            server.login(sender_email, sender_password)
            server.sendmail(sender_email, recipient_email, message.as_string())
        print("Email sent successfully!")
    except Exception as e:
        print(f"Error sending email: {e}")

if __name__ == "__main__":
    recipient = "test.recipient@example.com" # Replace with the recipient's email
    subject = "Welcome to our service!"
    body = "Thank you for subscribing! We're excited to have you."
    send_email(recipient, subject, body)

Explanation:

  • Import necessary libraries: os for accessing environment variables, smtplib for SMTP communication, and email modules for constructing email messages.
  • Retrieve credentials: The script retrieves the sender’s email address and App Password from the environment variables set in the previous step.
  • Create the email message: MIMEMultipart is used to create a multipart email, allowing for plain text or HTML content, and attachments. We add the “From”, “To”, and “Subject” headers.
  • Attach the email body: The MIMEText object is used to create the body of the email. The second argument to MIMEText specifies the content type (“plain” or “html”).
  • Connect to the Gmail SMTP server: smtplib.SMTP_SSL("smtp.gmail.com", 465) establishes a secure connection to Gmail’s SMTP server over SSL on port 465 (older implementations used 587 with STARTTLS).
  • Login to the server: The script logs in to the Gmail server using the sender’s email address and App Password.
  • Send the email: server.sendmail(sender_email, recipient_email, message.as_string()) sends the email to the specified recipient. The message.as_string() method converts the email object into a string format suitable for sending.
  • Error handling: The try...except block catches potential exceptions during the email sending process and prints an error message.

Example 2: Sending HTML Emails

To send HTML-formatted emails, change the content type in the MIMEText object.

    html_body = """
    <html>
    <body>
        <h1>Welcome to Our Service!</h1>
        <p>Thank you for subscribing! We're excited to have you.</p>
        <p>Click <a href="https://example.com">here</a> to get started.</p>
    </body>
    </html>
    """
    message.attach(MIMEText(html_body, "html"))

Replace the original message.attach(MIMEText(body, "plain")) line with the above code. Make sure to create valid HTML for your email content.

Example 3: Adding a Reply-To Address

You can specify a “Reply-To” address to ensure replies are sent to a different email address than the sender.

message["Reply-To"] = "support@example.com"

Add this line after setting the “To” and “Subject” headers in the send_email function. Replace support@example.com with the desired reply-to address.

This basic script provides a foundation for sending emails using Python and Gmail. In the next sections, we’ll explore how to personalize these emails and trigger them automatically.

Personalizing Emails with Templates

Sending generic emails is ineffective. Personalization significantly increases engagement. We can achieve personalization by using templates with placeholders that are dynamically replaced with the subscriber’s information.

Example 1: Using String Formatting for Personalization

One straightforward approach is to use Python’s string formatting capabilities.

def send_personalized_email(recipient_email, subject, template, data):
    sender_email = os.environ.get("GMAIL_USERNAME")
    sender_password = os.environ.get("GMAIL_APP_PASSWORD")

    message = MIMEMultipart()
    message["From"] = sender_email
    message["To"] = recipient_email
    message["Subject"] = subject

    body = template.format(data)
    message.attach(MIMEText(body, "html")) # Or "plain" if your template is plain text

    try:
        with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
            server.login(sender_email, sender_password)
            server.sendmail(sender_email, recipient_email, message.as_string())
        print("Personalized email sent successfully!")
    except Exception as e:
        print(f"Error sending personalized email: {e}")

if __name__ == "__main__":
    recipient = "test.recipient@example.com"
    subject = "Welcome, {name}!"
    template = """
    <html>
    <body>
        <h1>Welcome, {name}!</h1>
        <p>Thank you for subscribing, {name}! We're excited to have you on board.</p>
        <p>Your username is: {username}</p>
    </body>
    </html>
    """
    data = {"name": "John Doe", "username": "johndoe123"}
    send_personalized_email(recipient, subject, template, data)

Explanation:

  • A template string contains placeholders enclosed in curly braces {}.
  • The data dictionary holds the values to replace the placeholders.
  • The template.format(data) method performs the replacement. The data syntax unpacks the dictionary into keyword arguments for the format method.

Example 2: Loading Templates from Files

For more complex templates, it’s better to store them in separate files. This keeps your code cleaner and makes it easier to edit the templates.

import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

def load_template(template_path):
    with open(template_path, "r") as f:
        return f.read()

def send_personalized_email(recipient_email, subject, template_path, data):
    sender_email = os.environ.get("GMAIL_USERNAME")
    sender_password = os.environ.get("GMAIL_APP_PASSWORD")

    message = MIMEMultipart()
    message["From"] = sender_email
    message["To"] = recipient_email
    message["Subject"] = subject

    template = load_template(template_path)
    body = template.format(data)
    message.attach(MIMEText(body, "html"))

    try:
        with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
            server.login(sender_email, sender_password)
            server.sendmail(sender_email, recipient_email, message.as_string())
        print("Personalized email sent successfully!")
    except Exception as e:
        print(f"Error sending personalized email: {e}")

if __name__ == "__main__":
    recipient = "test.recipient@example.com"
    subject = "Welcome, {name}!"
    template_path = "welcome_template.html" # Path to your template file

    # Create the template file (welcome_template.html)
    # <html><body><h1>Welcome, {name}!</h1><p>...</p></body></html>

    data = {"name": "John Doe", "username": "johndoe123"}
    send_personalized_email(recipient, subject, template_path, data)

Explanation:

  • The load_template function reads the template content from the specified file.
  • The template_path variable specifies the path to the template file.
  • The rest of the code is similar to the previous example, but uses the loaded template.

Create a file named welcome_template.html in the same directory as your Python script, and add your HTML template to it.

Example 3: Using Jinja2 Templating Engine (More Advanced)

For more advanced templating needs, consider using a dedicated templating engine like Jinja2. Jinja2 provides features like template inheritance, filters, and control structures.

# Install Jinja2: pip install Jinja2

import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from jinja2 import Environment, FileSystemLoader

def send_personalized_email(recipient_email, subject, template_path, data):
    sender_email = os.environ.get("GMAIL_USERNAME")
    sender_password = os.environ.get("GMAIL_APP_PASSWORD")

    message = MIMEMultipart()
    message["From"] = sender_email
    message["To"] = recipient_email
    message["Subject"] = subject

    # Configure Jinja2 environment
    env = Environment(loader=FileSystemLoader('.'))  # Load templates from the current directory
    template = env.get_template(template_path)
    body = template.render(data) # Render the template with the data
    message.attach(MIMEText(body, "html"))

    try:
        with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
            server.login(sender_email, sender_password)
            server.sendmail(sender_email, recipient_email, message.as_string())
        print("Personalized email sent successfully!")
    except Exception as e:
        print(f"Error sending personalized email: {e}")

if __name__ == "__main__":
    recipient = "test.recipient@example.com"
    subject = "Welcome, {{ name }}!" # Jinja2 syntax
    template_path = "welcome_template.html" # Path to your template file

    # Example welcome_template.html for Jinja2
    # <html><body><h1>Welcome, {{ name }}!</h1><p>...</p></body></html>

    data = {"name": "John Doe", "username": "johndoe123"}
    send_personalized_email(recipient, subject, template_path, data)

Explanation:

  • You need to install Jinja2 using pip install Jinja2.
  • The Environment and FileSystemLoader classes are used to configure the Jinja2 environment. The FileSystemLoader('.') tells Jinja2 to load templates from the current directory.
  • The env.get_template(template_path) method loads the template from the specified file.
  • The template.render(data) method renders the template with the provided data.
  • Note the use of {{ name }} syntax in the template file for Jinja2 placeholders.

By using templates and placeholders, you can easily personalize your emails and create a more engaging experience for your subscribers.

Triggering Emails on New Subscriber

Now that we can send personalized emails, the next step is to automate the sending process when a new subscriber joins your list. There are several ways to achieve this, depending on how you manage your subscribers. We will explore two common approaches: using a simple file-based system and integrating with a database.

Example 1: File-Based Trigger (Simple but Limited)

This method involves monitoring a file for new email addresses. When a new email address is added to the file, the script triggers the welcome email.

import os
import time
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

def send_welcome_email(recipient_email):
    #  (Reuse send_personalized_email function from previous section, with hardcoded welcome email subject/template)
    sender_email = os.environ.get("GMAIL_USERNAME")
    sender_password = os.environ.get("GMAIL_APP_PASSWORD")

    message = MIMEMultipart()
    message["From"] = sender_email
    message["To"] = recipient_email
    message["Subject"] = "Welcome to Our Newsletter!"

    body = f"Welcome {recipient_email}!" # simplistic, replace with templating
    message.attach(MIMEText(body, "plain"))


    try:
        with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
            server.login(sender_email, sender_password)
            server.sendmail(sender_email, recipient_email, message.as_string())
        print(f"Welcome email sent to {recipient_email}!")
    except Exception as e:
        print(f"Error sending welcome email to {recipient_email}: {e}")



def monitor_new_subscribers(filepath):
    # Get the initial file size
    try:
        initial_size = os.path.getsize(filepath)
    except FileNotFoundError:
        print(f"File not found: {filepath}")
        return

    while True:
        try:
            current_size = os.path.getsize(filepath)
            if current_size > initial_size:
                # The file has grown, indicating a new subscriber
                with open(filepath, "r") as f:
                    # Read only the new lines added since last check.
                    f.seek(initial_size) # move file pointer to the previous size.
                    new_lines = f.readlines()
                    for line in new_lines:
                        email = line.strip() # remove whitespace/newlines
                        if email: # check not empty
                            send_welcome_email(email)
                # Update initial size for next check
                initial_size = current_size
        except FileNotFoundError:
            print(f"File not found: {filepath}")
            return

        time.sleep(5) # Check every 5 seconds



if __name__ == "__main__":
    subscriber_file = "subscribers.txt"

    # Create the file if it doesn't exist
    if not os.path.exists(subscriber_file):
        open(subscriber_file, "w").close()

    monitor_new_subscribers(subscriber_file)

Explanation:

  • The monitor_new_subscribers function continuously monitors the subscribers.txt file.
  • It compares the current file size to the previous size. If the file has grown, it assumes a new subscriber has been added.
  • It reads the newly added line(s) from the file, extracts the email address, and calls the send_welcome_email function.
  • The time.sleep(5) function pauses the script for 5 seconds before checking the file again.
  • Error handling is included to manage the case where the file is not found.

To use this script, create a file named subscribers.txt in the same directory as your Python script. Add one email address per line to this file. The script will automatically send a welcome email to each new email address added to the file.

Caveats: This method is very simplistic and has limitations. It’s not suitable for high-volume subscriber lists or concurrent access. It’s also prone to errors if the file is modified in unexpected ways.

Example 2: Database Trigger (More Robust)

A more robust solution is to use a database to store subscriber information and trigger emails when a new record is added. This example assumes you’re using a database like PostgreSQL, but the concepts apply to other databases as well. We will use `psycopg2`, a popular PostgreSQL adapter for Python.

# Install psycopg2: pip install psycopg2-binary

import os
import time
import psycopg2
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart


DATABASE_URL = os.environ.get("DATABASE_URL") # e.g., "postgresql://user:password@host:port/database"


def send_welcome_email(recipient_email):
    sender_email = os.environ.get("GMAIL_USERNAME")
    sender_password = os.environ.get("GMAIL_APP_PASSWORD")

    message = MIMEMultipart()
    message["From"] = sender_email
    message["To"] = recipient_email
    message["Subject"] = "Welcome to Our Newsletter!"

    body = f"Welcome {recipient_email}!" # Simplistic: use templating
    message.attach(MIMEText(body, "plain"))

    try:
        with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
            server.login(sender_email, sender_password)
            server.sendmail(sender_email, recipient_email, message.as_string())
        print(f"Welcome email sent to {recipient_email}!")
    except Exception as e:
        print(f"Error sending welcome email to {recipient_email}: {e}")


def monitor_new_subscribers_db():
    conn = None  # Initialize conn outside the try block
    try:
        conn = psycopg2.connect(DATABASE_URL)
        cur = conn.cursor()

        # Get the maximum ID from the subscribers table to track new additions.
        cur.execute("SELECT COALESCE(MAX(id), 0) FROM subscribers;")
        last_id = cur.fetchone()[0]


        while True:
            try:
                cur.execute("SELECT id, email FROM subscribers WHERE id > %s;", (last_id,))
                new_subscribers = cur.fetchall()

                for subscriber in new_subscribers:
                    subscriber_id, email = subscriber
                    send_welcome_email(email) # Send the welcome email.
                    last_id = subscriber_id      # Update our last known ID.
                conn.commit() # Commit transactions after processing subscribers


            except Exception as e:
                print(f"Database error: {e}")

            time.sleep(5)


    except psycopg2.Error as e:
        print(f"Error connecting to the database: {e}")

    finally:
        if conn:
            cur.close() # Close the cursor safely.
            conn.close()   # Close the connection safely.
            print("Database connection closed.")




if __name__ == "__main__":

    # You need to set DATABASE_URL environment variable.
    # e.g., export DATABASE_URL="postgresql://user:password@host:port/database"

    monitor_new_subscribers_db()

Explanation:

  • You need to install psycopg2-binary using pip install psycopg2-binary.
  • Set the DATABASE_URL environment variable to your PostgreSQL connection string. This string typically includes the username, password, host, port, and database name. Example: postgresql://user:password@host:5432/mydatabase
  • The monitor_new_subscribers_db function connects to the PostgreSQL database.
  • It retrieves the maximum ID from the subscribers table. This is used to track new subscribers added since the last check.
  • It continuously queries the database for new subscribers with an ID greater than the last known ID.
  • For each new subscriber, it calls the send_welcome_email function.
  • The time.sleep(5) function pauses the script for 5 seconds before querying the database again.
  • We properly close the cursor and connection in a finally block to avoid resource leaks.

Before running this script, you need to create a subscribers table in your PostgreSQL database with at least an id (integer, primary key) and an email (text) column. You also need to populate the `DATABASE_URL` environment variable.

Comparison of Triggering Methods

FeatureFile-Based TriggerDatabase Trigger
ScalabilityLowHigh
ReliabilityLowHigh
ComplexityLowMedium
Real-time responsivenessGoodGood
Suitable Use CasesSmall, simple lists; development/testingLarge lists; production environments

The database trigger method is significantly more robust and scalable than the file-based method, making it the preferred choice for production environments.

Error Handling and Logging

Reliable email automation requires robust error handling and logging. Proper error handling prevents the script from crashing due to unexpected issues, while logging provides valuable insights into the script’s behavior and helps diagnose problems.

Example 1: Implementing Basic Error Handling

We’ve already used try...except blocks in the previous sections to handle potential exceptions during email sending and database operations. Let’s expand on that.

import os
import time
import logging # Import the logging module
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart


# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')


def send_welcome_email(recipient_email):
    sender_email = os.environ.get("GMAIL_USERNAME")
    sender_password = os.environ.get("GMAIL_APP_PASSWORD")

    message = MIMEMultipart()
    message["From"] = sender_email
    message["To"] = recipient_email
    message["Subject"] = "Welcome to Our Newsletter!"

    body = f"Welcome {recipient_email}!" # simplistic, replace with templating
    message.attach(MIMEText(body, "plain"))

    try:
        with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
            server.login(sender_email, sender_password)
            server.sendmail(sender_email, recipient_email, message.as_string())
        logging.info(f"Welcome email sent to {recipient_email}!") # Changed print to logging                    

Share this article