A self-running email outreach pipeline designed to run as an always-on task on PythonAnywhere. Discovers contacts, sends personalised emails, follows up automatically, monitors for positive replies, and sends a weekly progress report.
This is a skeleton. The architecture and logic are provided. The targeting, segmentation, and messaging are yours to build.
Phase 1 -- Fixed targets Scrapes a hand-picked list of target sites for contact email addresses. Each site is revisited weekly to catch new contacts.
Phase 2 -- Expanding search Works through a list of segments (locations, industries, topics -- whatever suits your use case) using DuckDuckGo, searching across categories you define. Cycles back to the beginning once complete.
Always running
| Frequency | Action |
|---|---|
| Every 60s | Send one outreach email (within sending hours) |
| Every 60s | Send follow-ups that are due |
| Every 5 ticks | Scrape next fixed target |
| Every 10 ticks | Run next segment/category search |
| Every 2 hours | Check inbox, alert on positive replies |
| Every Sunday | Send weekly progress report |
pip install requests beautifulsoup4 lxml python-dotenv duckduckgo-searchcp .env.example .envFill in every value in .env. At minimum: SMTP credentials, sender name,
website URL, and the address where alerts should go.
There are four things you must fill in before the script is useful:
FIXED_TARGETS
A list of specific sites you want to scrape for contacts. Format:
("domain.com", "Outlet name", "why they're relevant").
SEGMENTS
A list of whatever grouping makes sense for your outreach -- geographic areas,
industries, niches, topics. The pipeline works through these one by one.
CATEGORIES
The types of outlet to search for within each segment, with DuckDuckGo query
templates. Use {segment} as a placeholder.
INITIAL_BODY and FOLLOWUP_BODY
Your actual pitch emails. Use {outlet} and {beat} in the initial email,
{outlet} and {sent_date} in the follow-up.
python outreach.py run --dry-runThis runs the full pipeline in preview mode. Check the log to confirm everything looks correct before sending any real emails.
- Upload
outreach.pyand your.envfile to your PythonAnywhere home directory - Go to the Tasks tab
- Under Always-on tasks, set the command to:
python3 /home/yourusername/outreach.py run - Click Create
The log icon next to the task shows live output.
python outreach.py run Start the pipeline
python outreach.py run --dry-run Full preview, no emails sent
python outreach.py test Re-send test emails and exit
python outreach.py summary Print dashboard and exit
python outreach.py import <file.csv> Import contacts from CSV
python outreach.py import my_contacts.csvRequired column: email. Optional: outlet, beat, first_name, last_name.
Deliverability. Sending from a dedicated domain rather than a personal Gmail address significantly improves deliverability. Low-cost SMTP hosting for custom domains is widely available.
Rate limiting. The default of one email per minute is intentionally
conservative. Do not reduce EMAIL_INTERVAL_SECONDS below 30.
DuckDuckGo. The duckduckgo-search library may occasionally hit rate
limits. The script handles this gracefully with a wait and retry.
The database (outreach.db) is excluded from the repo by .gitignore.
It contains real contact data and should never be committed.
MIT