DEV Community

Vemtrac Labs
Vemtrac Labs

Posted on

I Built an Automated Cold Email Sender in Python — Here's the Full Code Breakdown

sending cold emails manually is slow. i built a python script that loads prospects from a json file, personalizes each email with SEO scan data, and sends them via gmail SMTP with proper spacing. here's how.

the architecture

prospects.json -> sender.py -> gmail SMTP -> sent_log.json
Enter fullscreen mode Exit fullscreen mode

the sender reads from a batch file, checks against a sent log to avoid duplicates, sends with delays, and logs every send.

the sender script

import smtplib
import json
import time
from email.mime.text import MIMEText

def load_batch(filepath):
    with open(filepath) as f:
        return json.load(f)

def load_sent_log(filepath):
    try:
        with open(filepath) as f:
            return set(e['to'] for e in json.load(f))
    except FileNotFoundError:
        return set()

def send_batch(batch_file, sent_log_file, gmail, app_pw, max_sends=25):
    prospects = load_batch(batch_file)
    already_sent = load_sent_log(sent_log_file)

    # filter already sent
    to_send = [p for p in prospects if p['to'] not in already_sent]
    print(f'{len(to_send)} unsent out of {len(prospects)} total')

    smtp = smtplib.SMTP_SSL('smtp.gmail.com', 465)
    smtp.login(gmail, app_pw)

    sent_count = 0
    for prospect in to_send[:max_sends]:
        msg = MIMEText(prospect['body'])
        msg['Subject'] = prospect['subject']
        msg['From'] = gmail
        msg['To'] = prospect['to']

        try:
            smtp.sendmail(gmail, prospect['to'], msg.as_string())
            sent_count += 1
            print(f'  Sent {sent_count}: {prospect["to"]}')

            # log the send
            log_send(sent_log_file, prospect)

            # reconnect every 10 emails
            if sent_count % 10 == 0:
                smtp.quit()
                time.sleep(5)
                smtp = smtplib.SMTP_SSL('smtp.gmail.com', 465)
                smtp.login(gmail, app_pw)

            time.sleep(150)  # 2.5 min between sends

        except Exception as e:
            print(f'  Error: {e}')
            break

    smtp.quit()
    print(f'Done. Sent {sent_count} emails.')
Enter fullscreen mode Exit fullscreen mode

key design decisions

2.5 minute delay: gmail flags rapid sends. 150 seconds between emails keeps you under the radar for longer.

reconnect every 10: SMTP connections time out. reconnecting prevents SMTPServerDisconnected errors mid-batch.

sent log as deduplication: the log file prevents resending if the script crashes and restarts. every successful send is immediately logged.

max 25 per run: gmail's daily limit is officially 500, but cold emails get flagged much earlier. 25/day kept me going for 2 weeks before restriction.

the batch file format

[
  {
    "to": "agency@example.com",
    "subject": "example.com — quick question about your client outreach",
    "body": "Hi, I ran a quick scan on example.com..."
  }
]
Enter fullscreen mode Exit fullscreen mode

what happened

367 emails sent over 14 days. 3 replies (0.82% response rate). then gmail restricted the account. the code works — the channel has limits.

lessons for your cold email system

  1. use a dedicated sending domain, not personal gmail
  2. warm up the domain for 2 weeks before sending
  3. start at 5/day, ramp to 25/day over a week
  4. personalize with real data (SEO scans, not templates)
  5. monitor bounce rates — remove bad emails immediately

tools i built around this

the hardest part of cold email isn't the code. it's staying under email provider limits while sending enough volume to get results.

Top comments (0)