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
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.')
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..."
}
]
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
- use a dedicated sending domain, not personal gmail
- warm up the domain for 2 weeks before sending
- start at 5/day, ramp to 25/day over a week
- personalize with real data (SEO scans, not templates)
- monitor bounce rates — remove bad emails immediately
tools i built around this
- SEO chrome extension — scan sites for personalization data ($9)
- agency contact sample — 50 free verified contacts
- full outreach service — managed cold email campaigns
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)