A Python utility for creating personalized draft emails with calendar invitations from templates and context data. Perfect for bulk invitations, newsletters, or any personalized email campaigns.
- 📧 Template-based emails with Jinja2 templating
- 📊 Context data support from CSV or YAML files
- 📅 Calendar invitations (ICS files) automatically generated
- 🔧 IMAP integration to save drafts directly to your email client
- ✅ Test mode for validating IMAP settings
- 🔄 Backward compatibility with existing workflows
- 🚫 Email deduplication prevents multiple emails to same recipient
- ✔️ Include/exclude filtering with boolean column support
- 📱 Multi-client compatibility with proper HTML and plain text formats
Install the required Python packages:
pip install pyyaml~=6.0 markdown~=3.4 ics~=0.7 pytz~=2023.3 jinja2~=3.1-
Copy the configuration template:
cp bulkdraft.conf.example ~/.config/bulkdraft.conf -
Edit
~/.config/bulkdraft.confwith your IMAP settings:[DEFAULT] imap_server=imap.gmail.com imap_port=993 imap_username=your-email@gmail.com imap_password=your-app-password from_email=your-email@gmail.com
Note: For Gmail, use an App Password, not your regular password.
-
If the configuration file is missing, the tool will show a helpful error message:
❌ Configuration file not found! Please copy the example configuration file: cp bulkdraft.conf.example ~/.config/bulkdraft.conf Then edit ~/.config/bulkdraft.conf with your IMAP settings.
bulkdraft has two main modes: template processing and IMAP testing.
Before processing templates, test your IMAP configuration:
python main.py test "recipient@example.com" "Test Subject" "Test message content"This will create a single draft email to verify your IMAP settings are working.
# Process template with CSV context data
python main.py template example.txt --context recipients.csv
# Or using the legacy format (backward compatible)
python main.py example.txt --context recipients.csvTemplates use YAML front matter for metadata and Jinja2 templating for dynamic content:
---
event_name: "{{ event_name | default('Team Meeting') }}"
event_date: "{{ event_date | default('2023-12-15 10:00:00') }}"
event_location: "{{ event_location | default('Conference Room') }}"
timezone: "{{ timezone | default('America/New_York') }}"
subject: "{{ subject | default('Invitation: ' + event_name) }}"
---
Dear {{ first_name | default('Team Member') }},
You're invited to **{{ event_name }}** on {{ event_date }}.
**Event Details:**
- Date: {{ event_date }}
- Location: {{ event_location }}
- Timezone: {{ timezone }}
Best regards,
The Event Team
---
*This email was sent to {{ email }}*first_name,last_name,email,event_name,include
John,Doe,john@example.com,Project Kickoff,TRUE
Jane,Smith,jane@example.com,Project Kickoff,FALSE
Alice,Johnson,alice@example.com,Project Kickoff,TRUENote: The optional include column allows you to control which rows are processed. Only rows with include set to TRUE (case-insensitive) will have emails generated. Rows with FALSE, empty values, or any other text will be skipped.
Email Deduplication: The tool automatically removes duplicate email addresses (case-insensitive) to prevent multiple invitations to the same person. It shows which duplicates are skipped and provides a summary of unique recipients.
- first_name: John
last_name: Doe
email: john@example.com
event_name: Project Kickoff
- first_name: Jane
last_name: Smith
email: jane@example.com
event_name: Project KickoffTemplates support Jinja2 syntax with the following available variables:
- From YAML metadata:
event_name,event_date,event_location,timezone,subject - From context data: Any columns from your CSV/YAML file (e.g.,
first_name,last_name,email) - Filters: Use Jinja2 filters like
{{ variable | default('fallback') }} - Conditionals:
{% if variable %}...{% endif %}
- HTML emails with proper structure and CSS styling
- Plain text fallback for accessibility and older clients
- Calendar invitations (ICS files) with both attachment and inline formats
- Mobile-responsive design for email clients
The tool automatically creates ICS calendar files based on the metadata:
- Uses
event_namefor the calendar event title - Uses
event_date(format:YYYY-MM-DD HH:MM:SS) for scheduling - Uses
event_locationfor the event location - Uses
timezonefor proper timezone handling - Dual format: Both attachment and inline for maximum email client compatibility
- Email deduplication: Prevents multiple emails to the same address
- Include/exclude filtering: Boolean column support for selective sending
- Template validation: Error handling for invalid templates
- IMAP draft flags: Proper draft creation for editability in email clients
python main.py template <template_file> [--context <data_file>]
python main.py <template_file> [--context <data_file>] # Legacy formatArguments:
template_file: Path to your email template file--context: Path to CSV or YAML file with context data--csv: (Deprecated) Use--contextinstead
python main.py test <email> <subject> <message>Arguments:
email: Recipient email addresssubject: Email subject linemessage: Email message content
-
Create
invitation.txt:--- event_name: "Weekly Team Sync" event_date: "2023-12-15 10:00:00" event_location: "Zoom Meeting Room" timezone: "America/New_York" --- Hi {{ first_name }}, Join us for our {{ event_name }} on {{ event_date }}.
-
Create
team.csv:first_name,email Alice,alice@company.com Bob,bob@company.com
-
Run:
python main.py template invitation.txt --context team.csv
python main.py test "test@example.com" "Configuration Test" "Testing IMAP setup"-
Configuration File Missing
❌ Configuration file not found!- Copy
bulkdraft.conf.exampleto~/.config/bulkdraft.conf - Edit with your IMAP settings
- Copy
-
Authentication Failed
- For Gmail: Use App Passwords, not your regular password
- Enable 2-factor authentication first, then generate an App Password
-
Connection Timeout
- Verify
imap_serverandimap_portsettings - Check firewall/network restrictions
- Verify
-
Drafts Not Editable
- Tool now sets proper
\\Draftflags for editability - Check that drafts folder is correctly detected
- Tool now sets proper
-
Template Errors
- Ensure YAML front matter is properly formatted with
---separators - Check Jinja2 template syntax
- Avoid circular references in template variables
- Ensure YAML front matter is properly formatted with
-
Missing Context Variables
- Use
{{ variable | default('fallback') }}for optional variables - Verify CSV/YAML column names match template variables
- Use
-
Duplicate Emails
- Tool automatically deduplicates by email address
- Check output for "Skipping duplicate email" messages
Add error handling by running with Python's verbose mode:
python -v main.py test "email@example.com" "Test" "Message"bulkdraft includes a comprehensive test suite with both offline and online tests.
# Run all tests
python run_tests.py
# Run only offline tests (no IMAP required)
python run_tests.py --mode offline
# Run only online tests (requires IMAP config)
python run_tests.py --mode online
# Run specific test module
python run_tests.py --module test_configbulkdraft/
├── main.py # Main entry point
├── bulkdraft/ # Package modules
│ ├── __init__.py
│ ├── config.py # Configuration management
│ ├── template.py # Template processing
│ ├── context.py # Context data handling
│ ├── email_builder.py # Email construction
│ ├── calendar.py # ICS calendar creation
│ ├── imap_client.py # IMAP operations
│ └── cli.py # Command line interface
├── tests/ # Test suite
│ ├── test_*.py # Individual test modules
│ └── test_imap_online.py # Online integration tests
├── run_tests.py # Test runner
├── TESTING.md # Testing documentation
├── example.txt # Example template
├── recipients.csv # Example context data
└── bulkdraft.conf.example # Configuration template
For detailed testing information, see TESTING.md.
- Server:
imap.gmail.com:993 - Enable 2FA and create an App Password
- Use App Password in configuration
- Server:
outlook.office365.com:993 - May require app-specific authentication
- Server:
imap.mail.yahoo.com:993 - Enable "Allow apps that use less secure sign in"
- Contact your IT administrator for IMAP settings
- Typically port 993 (SSL) or 143 (non-SSL)
This project is licensed under the MIT License - see the LICENSE file for details.