DEV Community

Brad
Brad

Posted on

Build a Python Email Automation System: Send 100 Personalized Emails in 5 Minutes

Build a Python Email Automation System: Send 100 Personalized Emails in 5 Minutes

Manual email campaigns are slow and error-prone. Here's how to build an email automation system with Python that sends personalized emails at scale.

Basic Email Sender with Gmail

import smtplib
import ssl
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
import os

class EmailSender:
    def __init__(self, email: str, password: str):
        """Initialize with Gmail credentials.
        For Gmail: enable 2FA and create an App Password at
        https://myaccount.google.com/apppasswords
        """
        self.email = email
        self.password = password
        self.smtp_server = "smtp.gmail.com"
        self.port = 587

    def send_email(
        self,
        to_email: str,
        subject: str,
        body_text: str,
        body_html: str = None,
        attachments: list = None
    ) -> bool:
        """Send a single email with optional HTML and attachments."""

        message = MIMEMultipart("alternative")
        message["Subject"] = subject
        message["From"] = self.email
        message["To"] = to_email

        # Add plain text part
        message.attach(MIMEText(body_text, "plain"))

        # Add HTML part if provided
        if body_html:
            message.attach(MIMEText(body_html, "html"))

        # Add attachments if provided
        if attachments:
            for file_path in attachments:
                with open(file_path, "rb") as f:
                    part = MIMEBase("application", "octet-stream")
                    part.set_payload(f.read())

                encoders.encode_base64(part)
                part.add_header(
                    "Content-Disposition",
                    f"attachment; filename= {os.path.basename(file_path)}"
                )
                message.attach(part)

        # Send
        context = ssl.create_default_context()
        with smtplib.SMTP(self.smtp_server, self.port) as server:
            server.ehlo()
            server.starttls(context=context)
            server.login(self.email, self.password)
            server.sendmail(self.email, to_email, message.as_string())

        return True
Enter fullscreen mode Exit fullscreen mode

Personalized Campaign Sender

import csv
import time
from typing import Generator

def load_recipients(csv_path: str) -> Generator:
    """Load recipient data from CSV file."""
    with open(csv_path, 'r') as f:
        reader = csv.DictReader(f)
        for row in reader:
            yield row

def send_campaign(
    sender: EmailSender,
    csv_file: str,
    subject_template: str,
    body_template: str,
    delay_seconds: float = 1.0
) -> dict:
    """Send personalized emails to a list of recipients."""

    results = {"sent": 0, "failed": 0, "errors": []}

    for recipient in load_recipients(csv_file):
        # Personalize subject and body
        subject = subject_template.format(**recipient)
        body = body_template.format(**recipient)

        try:
            sender.send_email(
                to_email=recipient["email"],
                subject=subject,
                body_text=body
            )
            results["sent"] += 1
            print(f"✓ Sent to {recipient['email']}")

        except Exception as e:
            results["failed"] += 1
            results["errors"].append({"email": recipient["email"], "error": str(e)})
            print(f"✗ Failed to send to {recipient['email']}: {e}")

        # Rate limiting to avoid spam filters
        time.sleep(delay_seconds)

    return results

# Usage
sender = EmailSender("your@gmail.com", "app_password_here")

# recipients.csv format: name,email,company,role
results = send_campaign(
    sender=sender,
    csv_file="recipients.csv",
    subject_template="Hey {name}, quick question about {company}",
    body_template="""Hi {name},

I noticed {company} is in the {role} space and thought you might find this useful.

[Your message here]

Best,
[Your name]
"""
)

print(f"Campaign complete: {results['sent']} sent, {results['failed']} failed")
Enter fullscreen mode Exit fullscreen mode

HTML Email Template

Professional-looking emails with HTML:

HTML_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<style>
  body {{ font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; }}
  .header {{ background: #007bff; color: white; padding: 20px; }}
  .content {{ padding: 20px; }}
  .cta {{ background: #28a745; color: white; padding: 12px 24px;
          text-decoration: none; border-radius: 5px; display: inline-block; }}
</style>
</head>
<body>
  <div class="header">
    <h1>Hello, {name}!</h1>
  </div>
  <div class="content">
    <p>Thank you for your interest in {product}.</p>
    <p>{message}</p>
    <a href="{cta_url}" class="cta">{cta_text}</a>
  </div>
</body>
</html>
"""

# Personalize the template for each recipient
html_body = HTML_TEMPLATE.format(
    name="John",
    product="Our Service",
    message="We'd love to show you how we can help.",
    cta_url="https://example.com",
    cta_text="Learn More"
)
Enter fullscreen mode Exit fullscreen mode

Track Open Rates

Add a tracking pixel to know who opened your emails:

import uuid

def add_tracking_pixel(html_body: str, tracking_server: str, email_id: str) -> str:
    """Add an invisible tracking pixel to HTML email."""

    pixel_url = f"{tracking_server}/track/{email_id}.png"
    pixel_html = f'<img src="{pixel_url}" width="1" height="1" style="display:none;">'

    # Insert before closing </body> tag
    return html_body.replace("</body>", f"{pixel_html}</body>")

# Each email gets a unique tracking ID
for recipient in recipients:
    email_id = str(uuid.uuid4())
    tracked_html = add_tracking_pixel(html_body, "https://track.yourdomain.com", email_id)
    # Log email_id -> recipient mapping in your database
Enter fullscreen mode Exit fullscreen mode

Real-World Performance

This system can process approximately 100 emails per 5 minutes when using 3-second delays (recommended to avoid spam folders).

For higher volume, consider using SendGrid, Mailgun, or AWS SES APIs instead of SMTP directly.

Want the Complete Automation Toolkit?

This email script is part of a larger automation toolkit I use for business.

👉 Get 50+ Python automation scripts — including email automation, web scrapers, invoice generators, data processors, and more.

Each script is documented and ready to customize for your needs.

Top comments (0)