A Python-based tool for sending personalized HTML emails to recipients from a CSV file using Microsoft Graph API (Office
365). Leverages concurrent.futures.ThreadPoolExecutor to drastically sending a large number of emails.
- Template-based emails: Use Jinja2 templates with CSV data for personalization
- Concurrent sending: Send multiple emails simultaneously with configurable thread limits
- Dry-run mode: Test templates without actually sending emails
- Email validation: Optional regex filtering for recipient email addresses
- Comprehensive logging: File and console logging with configurable levels
- Failure tracking: Automatic CSV export of failed email attempts
- Python >=3.12
- Microsoft Graph API credentials (Entra ID Enterprise Application - see below)
- Virtual environment (managed with
virtualenv)
- Enterprise application with OAuth credentials, and permission to use the SendMail Graph API endpoint
- An existing user account to source the email from
3rd party dependencies are listed in requirements.txt.
This package is also dependent on pymsgraph-mail, which I also developed. This package comes bundled into this repository, though.
- Clone the repository and navigate to the project directory
- Create and activate a virtual environment:
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate- Install dependencies:
pip install -r requirements.txtCreate a .env file in the project root with the following format:
# MANDATORY VARIABLES
MSGRAPH_TENANT_ID=your-microsoft-tenant-id
MSGRAPH_CLIENT_ID=your-client-id
MSGRAPH_CLIENT_SECRET=your-client-secret
# OPTIONAL VARIABLES
EMAILS_MUST_MATCH_REGEX=.+@yourcompany\.com
Note: The script will look for a a .env file in the current directory, unless specified otherwise with the --env-file-name parameter.
MSGRAPH_TENANT_ID(Required): Your Microsoft Azure tenant IDMSGRAPH_CLIENT_ID(Required): Your registered application's client IDMSGRAPH_CLIENT_SECRET(Required): Your application's client secretEMAILS_MUST_MATCH_REGEX(Optional): Regex pattern to filter recipient emails. Only emails matching this pattern will receive emails.
Your CSV file must contain an email_address column. Additional columns can be used for template variables.
Example CSV structure:
email_address,first_name,last_name,department,location
user@company.com,John,Doe,IT,New York
Additional columns will be passed to the Jinja2 template for rendering.
Use Jinja2 syntax to create personalized templates. Variable names must match CSV column headers exactly.
Example template:
<!DOCTYPE html>
<html>
<head>
<title>Is this your location?</title>
</head>
<body>
<h1>Hello {{first_name}} {{Last}}</h1>
<p>Department: {{department}}</p>
<p>You're listed as living in: {{location}}</p>
</body>
</html>Please review the Jinja2 documentation for more details on template syntax.
Note: This package uses Jinja2==3.1.6. At the time of writing, this is the stable version. Functionality and documentation may differ in future versions of Jinja2.
python send_emails.py --template_path html_templates/my_template.html --csv_path csv/recipients.csv --subject "Important Request"| Parameter | Short | Required | Default | Description |
|---|---|---|---|---|
--template_path |
--t |
✅ | - | Path to HTML template file |
--csv_path |
--c |
✅ | - | Path to CSV file containing recipients |
--subject |
--s |
✅ | - | Email subject line |
--email |
--e |
✅ | - | Sender email address |
--name |
--n |
✅ | - | Sender display name |
--dry-run |
--d |
❌ | False | Test mode - render templates but don't send emails |
--log_level |
--l |
❌ | INFO | Logging verbosity (INFO, WARNING, DEBUG, TRACE) |
--env-file-name |
--env |
❌ | .env | Name of environment file to load |
--max-concurrent-threads |
--mct |
❌ | 10 | Maximum concurrent email sending threads |
python send_emails.py \
--template_path html_templates/security_alert.html \
--csv_path csv/test_users.csv \
--subject "Monthly Security Update" \
--email "security@company.com" \
--name "Security Team"python send_emails.py \
--template_path html_templates/newsletter.html \
--csv_path csv/subscribers.csv \
--subject "Weekly Newsletter" \
--email "security@company.com" \
--name "Security Team"
--dry-run \
--log_level DEBUGpython send_emails.py \
--template_path html_templates/alert.html \
--csv_path csv/recipients.csv \
--subject "System Maintenance Notice" \
--email "security@company.com" \
--name "Security Team"
--env-file-name .env.production- Console logging: Real-time progress and status updates
- File logging: Detailed logs saved to
./logs/send_emails_py_YYYY-MM-DD.log - Failure tracking: Failed emails exported to
email_failures_YYYY-MM-DD_HH-MM-SS.csv
Note: Will create /logs directory in the current directory if it does not already exist.
generic_emailer/
├── csv/ # CSV files with recipient data
├── html_templates/ # Jinja2 HTML email templates
├── logs/ # Generated log files
├── src/ # Source code modules
├── .env # Environment configuration
├── send_emails.py # Main script
└── README.md # This file
- Never commit
.envfiles to version control - Use application-specific passwords for Office 365
- Consider using Azure Key Vault for production deployments
- The
EMAILS_MUST_MATCH_REGEXhelps prevent accidentally sending to external domains
- Authentication errors: Verify your Microsoft Graph API credentials in
.env - Template rendering issues: Ensure CSV column names exactly match template variables
- Email delivery failures: Check the generated failure CSV for specific error details
- Permission denied: Ensure your Azure app registration has the necessary Mail.Send permissions