Python script to send personalized cold emails via SMTP from a CSV file. Perfect for job hunting outreach.
- Template-based emails: Choose templates based on
is_recruitercolumn (1 = recruiter.md, 0 = default.md) - Markdown templates: Write templates in Markdown format, automatically converted to HTML emails
- Personalization: Use placeholders like
{{name}},{{company}},{{job_link}},{{position_name}}in templates - Custom templates: Override template selection with a single custom template file
- Attachments: Attach files (e.g., resume) to all emails
- Rate limiting: Configurable delay between emails to avoid spam filters
Set your SMTP credentials (recommended):
export SMTP_USER="your-email@gmail.com"
export SMTP_PASSWORD="your-app-password"
# Optional (defaults shown):
export SMTP_HOST="smtp.gmail.com"
export SMTP_PORT="587"
export RESUME_PATH="PATH"Or use the .env file:
source .env- Gmail: Use an App Password, not your normal password
- Outlook/Hotmail: Use
smtp.office365.com, port587, and your Microsoft account
- Prepare your CSV file (see format below)
- Edit templates in
templates/default.txtandtemplates/recruiter.txt - Run the script:
python send_mail.pyThe script will prompt you for:
- CSV file path
- Custom template file (optional; press Enter to use default/recruiter templates)
- Delay between emails in seconds (default: 60)
Your CSV must have these columns:
| Column | Required | Description |
|---|---|---|
email |
Yes | Recipient email address |
name |
Yes | Recipient name (used in {{name}}) |
company |
Yes | Company name (used in {{company}}) |
is_recruiter |
Yes | 1 for recruiter template, 0 for default template |
job_link |
No | Job posting URL (used in {{job_link}}) |
position_name |
No | Job title (used in {{position_name}}) |
Example CSV (contacts.example.csv):
email,name,company,is_recruiter,job_link,position_name
recruiter1@company.com,Jane Smith,Acme Inc,1,https://careers.company.com/job/123,Senior Software Engineer
hiring@startup.io,Alex Lee,Startup.io,0,https://startup.io/careers/eng,Backend DeveloperNote: You can add any other columns to your CSV and use them as placeholders (e.g., {{custom_column}}).
Templates are located in the templates/ directory:
templates/default.md: Used whenis_recruiter=0(or missing)templates/recruiter.md: Used whenis_recruiter=1
Template Format (Markdown):
- First line = Email subject (plain text)
- Remaining lines = Email body (Markdown format, automatically converted to HTML)
Placeholders (case-insensitive):
{{name}}→ Recipient name from CSV{{company}}→ Company name from CSV{{job_link}}→ Job link from CSV (empty if missing){{position_name}}→ Position name from CSV (empty if missing){{email}}→ Recipient email- Any other CSV column →
{{column_name}}
Markdown Formatting: Templates use Markdown syntax, which is automatically converted to HTML for emails.
Supported Markdown:
**text**or__text__→ Bold text*text*or_text_→ Italic text[link text](url)→ Hyperlink# Heading→ Large heading## Heading→ Medium heading### Heading→ Small heading- Empty line → Paragraph break
Example Template (templates/default.md):
Introduction – Shashank Kumar Pandey | Software Developer
Hi {{name}},
I hope you are doing well. My name is **Shashank Kumar Pandey** and I am a **SDE 2** at Landmark Group, having an experience of more than 3 years.
I came to know that **{{company}}** is currently hiring for Software Engineering positions.
If possible, can you please refer the following position :
{{job_link}}
I have attached my resume for your reference.
Regards
Shashank Kumar PandeyYou can edit the templates as you wish.
Note: All templates are automatically converted from Markdown to HTML, so emails are always sent as HTML with proper formatting.
When prompted for "Custom template file", you can provide a path to a template file. This template will be used for all rows, ignoring the is_recruiter column.
- Reads the CSV file
- For each row:
- Determines template:
recruiter.txtifis_recruiter=1, elsedefault.txt(or custom template if provided) - Replaces placeholders (
{{name}},{{company}}, etc.) with values from the CSV row - Sends the email via SMTP
- Waits for the specified delay before processing the next row
- Determines template:
- Python 3.9+
- Standard library only (no external dependencies)
"SMTP username and password required"
- Make sure you've set
SMTP_USERandSMTP_PASSWORDenvironment variables
"CSV must have a column named 'email'"
- Check that your CSV has an
emailcolumn (case-sensitive)
Emails not sending
- Verify SMTP credentials
- Check firewall/network settings
- For Gmail, ensure you're using an App Password, not your regular password
Free to use for personal and commercial purposes.