A Python bulk email sender with a web UI. Upload a CSV, Excel, or PDF file, write a personalized email template, and send to hundreds of recipients via Gmail SMTP — with rate limiting, resume-on-failure, and a live log stream.
- Web UI — browser-based dashboard to configure, compose, and send without touching the terminal
- Personalized templates — use
{{Name}},{{Email}}, or any column heading as a placeholder; each recipient gets their own personalized email - Multi-format input — reads recipients from
.xlsx,.csv, or.pdf - Resume on failure — tracks sent addresses; re-running skips already-sent ones
- Rate limiting — configurable per-email delay and batch pauses to stay within Gmail limits
- Dry-run mode — preview the recipient list without sending
- Live log stream — watch sending progress in real time in the browser
- Run history — view logs from previous runs inside the UI
- Python 3.8+
- A Gmail account with an App Password (requires 2FA to be enabled)
pip install -r requirements.txtInstalls: flask, openpyxl, pdfplumber
1. Start the server:
python app.py2. Open your browser:
http://localhost:5000
3. Configure SMTP — go to the Configure tab and fill in:
- Host:
smtp.gmail.com - Port:
465 - Your Gmail address and 16-character App Password
4. Write your template — go to the Template tab. Upload your file first in the Send tab to see column buttons appear — click any column button to insert {{ColumnName}} at your cursor.
5. Send — go to the Send tab, upload your file, choose options, and click Start Sending.
| Tab | What it does |
|---|---|
| Dashboard | Sent/failed counts, list of all tracked addresses, reset button |
| Configure | Edit SMTP settings and rate limiting — saved to config.json |
| Template | Write your email body with column placeholders, saved to email_template.txt |
| Send | Upload file, choose dry-run/reset options, start sending with live log output |
| History | View the last 5 run log files |
Column headings from your file become placeholders. For a file with columns Name, Email, Company:
Hello {{Name}},
We'd love to connect with you at {{Company}}.
Best regards,
Your Name
Each recipient receives their own version with real values filled in. Unknown placeholders are silently removed. Placeholders are case-insensitive ({{name}} = {{Name}}).
# Create default config and template
python bulk_emailer.py --init
# Preview recipients without sending
python bulk_emailer.py contacts.csv --dry-run
# Send emails
python bulk_emailer.py contacts.xlsx
# Use a custom config file
python bulk_emailer.py contacts.csv --config office.json
# Clear sent history and resend to everyone
python bulk_emailer.py contacts.csv --reset| Argument | Description |
|---|---|
file |
.xlsx, .csv, or .pdf file with email addresses |
--config <path> |
Config file (default: config.json) |
--dry-run |
List recipients without sending |
--reset |
Clear sent_emails.txt and failed_emails.txt |
--init |
Scaffold config.json and email_template.txt |
"smtp_host": "smtp.gmail.com",
"smtp_port": 465,
"use_ssl": true"smtp_host": "smtp.gmail.com",
"smtp_port": 587,
"use_ssl": falseApp Password: Go to myaccount.google.com/apppasswords → generate a password for "Mail". Use that 16-character password, not your regular Gmail password.
| Setting | Default | Purpose |
|---|---|---|
delay_seconds |
2 |
Wait between each email |
batch_size |
40 |
Emails per batch |
batch_pause_seconds |
60 |
Pause between batches |
Recommended safe settings (avoids Gmail bans):
| Risk level | delay | batch_size | batch_pause |
|---|---|---|---|
| Safest | 10s | 20 | 300s |
| Safe | 5s | 25 | 180s |
| Default | 2s | 40 | 60s |
Gmail limits: ~500 emails/day (free), ~2,000/day (Workspace). Never exceed ~20 emails/minute.
| File | Description |
|---|---|
sent_emails.txt |
Every successfully sent address (one per line) |
failed_emails.txt |
Rejected or errored addresses |
run_YYYYMMDD_HHMMSS.log |
Full timestamped log per run |
uploads/ |
Uploaded recipient files (via web UI) |
| Format | Notes |
|---|---|
.csv |
First row used as column headers for template variables |
.xlsx |
First row of each sheet used as headers |
.xls |
Same as .xlsx |
.pdf |
Emails extracted by regex; no column support (no personalization) |
bulk_emailer/
├── app.py # Flask web server
├── bulk_emailer.py # Core send logic (also usable as CLI)
├── config.json # SMTP credentials and settings
├── email_template.txt # Email body template
├── requirements.txt # Python dependencies
├── templates/
│ └── index.html # Single-page web UI
└── uploads/ # Uploaded recipient files (auto-created)