Fishin' Generator is a realistic phishing email simulator designed for corporate security awareness training. It allows security teams to generate and send safe, simulated phishing emails to employees, and tracks engagement (opens and clicks) to identify vulnerabilities in the human firewall.
Instead of just a technical exercise, this is positioned as a business solution. It demonstrates a deep understanding of social engineering - the number one attack vector for modern breaches - and provides actionable metrics for training.
- Backend: Flask (Python) - Lightweight and perfect for serving the dashboard UI and handling fast API tracking endpoints.
- Database: SQLite with Flask-SQLAlchemy (SQLAlchemy 2.0 API) - A robust, file-based relational database to store targets, templates, and tracking events.
- Frontend: HTML + Jinja2 Templates, powered by HTMX for dynamic real-time updates without page reloads, and styled with Tailwind CSS (via CDN) for a clean, modern, and professional aesthetic.
- Email Delivery: Standard Python
smtplib+email.mimefor generating and dispatching HTML emails, with a built-in "Dry Run" mode for safe local testing.
The simulator features a responsive, professional dashboard built with Tailwind CSS. Security teams can view high-level metrics (Total Campaigns, Total Targets) and see the real-time status of their tests.
The database comes seeded with 12 foundational phishing templates designed to mimic real-world attacks. These templates exploit specific emotional triggers:
- Urgency/Fear: e.g., "Credential Reset Notification" or "MFA Fatigue Bypass".
- Authority: e.g., "Executive Wire Transfer Request".
- Curiosity/Familiarity: e.g., "HR Policy Document" or "Fake File Share".
Each template features a customized, context-aware Sender Display Name stored at the template level (e.g. "Barclays Bank Security", "Zoom Support", or "Human Resources"), which dynamically pairs with your sending infrastructure to simulate high-fidelity phishing headers.
If an employee falls for the simulation and clicks a malicious link, they are safely redirected to a local phished.html landing page. This page breaks the news gently and provides immediate, constructive feedback on how they could have spotted the phishing attempt (e.g., checking the sender domain, hovering over links).
Security teams can end campaigns to stop active phishing simulations when the testing window closes.
- Suspended Tracking: When a campaign is "Ended", all subsequent clicks and opens from that campaign's emails are ignored, preventing late clicks from skewing active simulation metrics.
- Reactivation & Email Resending: If needed, campaigns can be reactivated with a single click. This changes the status back to "Active" and triggers automatic email resending to all original targets with new tracking IDs, continuing the simulation.
The core logic of the project relies on tying specific actions back to a single user through a unique identifier. Here is exactly how the lifecycle of an event is tracked:
When a campaign is launched, the Flask backend (app.py) iterates through the selected targets.
- It generates a unique UUID (
tracking_id) for that specific target. - It inserts a row into the
TrackingEventdatabase table withevent_type='Sent'. - It passes this
tracking_idto themailer.pyscript.
To know if a user opened the email, we use a tracking pixel:
mailer.pyappends an invisible HTML<img>tag to the bottom of the email:<img src="http://our-server/track/open/<tracking_id>.gif" width="1" height="1" style="display:none;" />- When the employee's email client renders the HTML, it automatically makes an HTTP
GETrequest to our server to download that image. - Our server catches that request, looks up the
tracking_id, logs an'Opened'event in the database, and returns a transparent 1x1 pixel so the user sees nothing broken.
To track if a user falls for the trap, we dynamically rewrite the links in the email:
- The templates use a placeholder
{{ tracking_url }}for their call-to-action buttons. - During email generation, Jinja replaces this with a custom URL:
http://our-server/track/click/<tracking_id>. - When the user clicks the "Reset Password" button, their browser navigates to our server.
- Our server catches the request, logs a
'Clicked'event using thetracking_id, and redirects the user to the educational training page.
(Note: The tracking_id column in the database is Indexed, rather than Unique, allowing us to store multiple events - Sent, Opened, Clicked - under the same ID for fast querying).
- Ensure you have uv installed.
- Install the dependencies:
uv sync
- Set up your environment configuration (optional):
- Copy the sample env file:
cp .env.sample .env - Fill in your SMTP settings. If using Gmail, you will need to generate a 16-character App Password from your Google App Passwords account settings (2-Step Verification must be enabled first) and use it as
SMTP_PASSwithout spaces.
- Copy the sample env file:
- Run the Flask application:
uv run app.py
- There are 2 ways to proceed:
-
Version 1 - Absolutely local with Dry Run
Open your browser and navigate to
http://localhost:5000.
Tip
Dry Run Mode: By default, if no SMTP credentials are provided via a .env file, the simulator will automatically run in "Dry Run" mode. Instead of actually sending emails, it will save the generated HTML email into a dry_run_emails/ folder and print the raw HTML to the terminal. You can simply double-click the saved .html file to view the email in your browser and test the clicking flow safely!
- Version 2 - using
serveoto have a real working demo
Note
For a temporary deployment we can use Serveo, a simple SSH-based tunneling service that exposes your local Flask app to the public internet without changing router settings or firewall rules. It creates a secure tunnel from your machine to a temporary public URL, so external users can reach http://localhost:5000 while the tunnel is active.
ssh -R 80:localhost:5000 serveo.netAfter running the command a temporary public URL will be printed to your terminal (for example: https://randomsubdomain.serveo.net).
Note
For some reason, serveo.net may not work as expected, so I recommend using Cloudflared instead. Install it on your Linux machine first:
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared focal main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt-get update && sudo apt-get install cloudflaredor on Arch Linux:
sudo pacman -S cloudflaredThen run the following command:
cloudflared tunnel --url http://localhost:5000Copy the generated URL and open it in a browser to access the dashboard remotely. It will be something like
https://randomname.trycloudflare.comand it will work for 2 hours or until you close the terminal. The link will appear after the wordsYour quick Tunnel has been created! Visit it at (it may take some time to be reachable):
Use it as follows:
- Copy the generated URL and open it in a browser to access the dashboard remotely.
- Send a test campaign (or open a saved dry-run email) and ensure the tracking pixel and rewritten links use the serveo/cloudflared URL (e.g. https://randomsubdomain.serveo.net/track/open/<tracking_id>).
- Click a tracked link from the email; the server will log the "Clicked" event and redirect to the educational landing page through the same public URL.
- Remember the tunnel (and its URL) lasts only for the SSH session - closing the terminal ends the public link.
erDiagram
TARGET ||--o{ TRACKING_EVENT : "has many"
TEMPLATE ||--o{ CAMPAIGN : "used in"
CAMPAIGN ||--o{ TRACKING_EVENT : "generates"
TARGET {
int id PK
string name
string email
}
TEMPLATE {
int id PK
string name
string sender_name
string subject
text body_html
}
CAMPAIGN {
int id PK
string name
int template_id FK
datetime created_at
string status
}
TRACKING_EVENT {
int id PK
int campaign_id FK
int target_id FK
string tracking_id
string event_type
datetime timestamp
}
| Column | Type | Constraints |
|---|---|---|
| id | Integer | Primary Key |
| name | String(100) | Not Null |
| String(120) | Unique, Not Null |
| Column | Type | Constraints |
|---|---|---|
| id | Integer | Primary Key |
| name | String(100) | Not Null |
| sender_name | String(100) | Nullable |
| subject | String(200) | Not Null |
| body_html | Text | Not Null |
| Column | Type | Constraints |
|---|---|---|
| id | Integer | Primary Key |
| name | String(100) | Not Null |
| template_id | Integer | Foreign Key |
| created_at | DateTime | Not Null |
| status | String(20) | Not Null |
| Column | Type | Constraints |
|---|---|---|
| id | Integer | Primary Key |
| campaign_id | Integer | Foreign Key |
| target_id | Integer | Foreign Key |
| tracking_id | String(36) | Indexed |
| event_type | String(20) | Not Null |
| timestamp | DateTime | Not Null |
To transition this simulator from a local training exercise to a production-ready cloud service, the following roadmap features are planned:
- User Authentication & Authorization: Securing the administration dashboard behind a secure login flow using Flask-Login and cryptographically hashed passwords.
- Abuse Prevention Safeguards: Restricting outbound campaigns to verified recipient domains, and adding rate limiting to campaign creation to prevent spam abuse.
- Decoupled SMTP Settings: Migrating email credentials out of environment variables and into user settings, allowing administrators to utilize their own secure email infrastructure.
For a detailed step-by-step walkthrough on how to implement and deploy these updates, refer to the Deployment and Email Security Roadmap.
