___________.__ _________ .__
\_ _____/|__|______ ____ \_ ___ \| | _____ __ _ __
| __) | \_ __ \_/ __ \/ \ \/| | \__ \\ \/ \/ /
| \ | || | \/\ ___/\ \___| |__/ __ \\ /
\___ / |__||__| \___ >\______ /____(____ /\/\_/
\/ \/ \/ \/
A self-hosted email firewall that sits between your email client and your upstream IMAP/SMTP provider.
FireClaw intercepts every email your client sends or receives. It evaluates each message against a set of configurable rules and blacklist entries, then decides whether to allow, flag, quarantine, or reject it — before it ever reaches your upstream provider or your inbox.
No cloud vendor. No subscription. No data leaving your machine unless you explicitly allow it.
Your email client → FireClaw (SMTP :2525 / IMAP :1143) → Gmail / Outlook / any IMAP+SMTP provider
↓
Rule engine evaluates every message
↓
allow / flag / quarantine / reject
↓
Admin dashboard at :3000
- Why FireClaw?
- How It Works
- Quick Start
- Configuration Reference
- Rule System
- Blacklist
- Quarantine
- Admin Dashboard
- REST API
- Architecture
- Known Limitations
- Contributing
- License
Most email filtering solutions are either cloud-based (your mail goes through someone else's servers) or locked inside a mail server you have to run yourself. FireClaw is different:
| Feature | FireClaw | Cloud filters | Self-hosted mail server |
|---|---|---|---|
| Works with any existing provider | Yes | Varies | No |
| Data stays on your machine | Yes | No | Yes |
| No MX record changes required | Yes | No | No |
| Admin dashboard included | Yes | Yes | Varies |
| Regex + keyword rule engine | Yes | Varies | Varies |
| Inbound + outbound filtering | Yes | Varies | Yes |
| Zero recurring cost | Yes | No | Yes |
You keep your Gmail, Outlook, Fastmail, or any provider you already use. FireClaw acts as a transparent local proxy: your email client talks to it, it talks to your provider.
FireClaw exposes two local endpoints that mimic standard email protocols:
Outbound (SMTP firewall) — your email client sends mail to localhost:2525. The firewall parses the message, runs it through your rules, and either forwards it to your upstream SMTP server or quarantines/rejects it.
Inbound (IMAP proxy) — your email client connects to localhost:1143. The proxy authenticates locally, then maintains a live connection to your upstream IMAP server. Quarantined inbound messages are hidden from the client until you release them through the dashboard.
Inbound scanner (IMAP poller) — independently polls your upstream INBOX on a configurable interval. Any message matching a quarantine rule is moved to a dedicated mailbox on the server side, keeping it invisible to your email client entirely.
Every message passes through the same evaluator for both inbound and outbound traffic:
- Blacklist check — sender or recipient address/domain is compared against the blacklist. A match always results in
quarantine. - Rule check — each enabled rule is tested in order. Multiple rules can match; the highest-priority action wins.
Action priority (lowest → highest): allow → flag → quarantine → reject
npm install -g pnpmgit clone https://github.com/leftcurveaeon/fireclaw.git
cd fireclawpnpm installcp .env.example .envOpen .env and fill in your upstream email provider credentials. Here is a Gmail example:
# Upstream SMTP
UPSTREAM_SMTP_HOST=smtp.gmail.com
UPSTREAM_SMTP_PORT=587
UPSTREAM_SMTP_SECURE=false
UPSTREAM_SMTP_USER=you@gmail.com
UPSTREAM_SMTP_PASS=your-app-password # Use an App Password, not your account password
# Upstream IMAP
UPSTREAM_IMAP_HOST=imap.gmail.com
UPSTREAM_IMAP_PORT=993
UPSTREAM_IMAP_SECURE=true
UPSTREAM_IMAP_USER=you@gmail.com
UPSTREAM_IMAP_PASS=your-app-passwordGmail users: generate an App Password in your Google account settings. Your regular password will not work with IMAP/SMTP direct access.
pnpm devThis starts both the backend server and the Next.js admin dashboard in watch mode with hot reload.
| Service | Address |
|---|---|
| Admin dashboard | http://localhost:3000 |
| REST API | http://localhost:8787 |
| SMTP firewall | localhost:2525 |
| IMAP proxy | localhost:1143 |
Update your email client's outgoing and incoming server settings:
Outgoing mail (SMTP)
| Setting | Value |
|---|---|
| Server | localhost |
| Port | 2525 |
| Security | None |
| Authentication | None (or set SMTP_AUTH_USER/SMTP_AUTH_PASS in .env) |
Incoming mail (IMAP)
| Setting | Value |
|---|---|
| Server | localhost |
| Port | 1143 |
| Security | None |
| Authentication | None (or set IMAP_AUTH_USER/IMAP_AUTH_PASS in .env) |
Your email client will now route all traffic through FireClaw. Open http://localhost:3000 to manage rules and review quarantined messages.
All configuration lives in .env at the project root. Most settings can also be updated live from the admin dashboard without restarting.
| Variable | Default | Description |
|---|---|---|
API_PORT |
8787 |
REST API and admin backend |
SMTP_LISTEN_PORT |
2525 |
Local SMTP firewall endpoint |
IMAP_PROXY_LISTEN_PORT |
1143 |
Local IMAP proxy endpoint |
| Variable | Default | Description |
|---|---|---|
IMAP_PROXY_ENABLED |
true |
Enable the local IMAP proxy |
IMAP_POLL_ENABLED |
true |
Enable the background IMAP inbox scanner |
IMAP_POLL_INTERVAL_SEC |
45 |
How often to scan upstream INBOX (seconds) |
QUARANTINE_MAILBOX |
OC-QUARANTINE |
Mailbox name created on the upstream server for quarantined inbound messages |
| Variable | Example | Description |
|---|---|---|
UPSTREAM_SMTP_HOST |
smtp.gmail.com |
Your provider's SMTP hostname |
UPSTREAM_SMTP_PORT |
587 |
SMTP port (587 for STARTTLS, 465 for TLS) |
UPSTREAM_SMTP_SECURE |
false |
true for port 465 (TLS), false for 587 (STARTTLS) |
UPSTREAM_SMTP_USER |
you@gmail.com |
SMTP login username |
UPSTREAM_SMTP_PASS |
app-password |
SMTP password or app password |
UPSTREAM_IMAP_HOST |
imap.gmail.com |
Your provider's IMAP hostname |
UPSTREAM_IMAP_PORT |
993 |
IMAP port |
UPSTREAM_IMAP_SECURE |
true |
Use TLS for the IMAP connection |
UPSTREAM_IMAP_TLS_REJECT_UNAUTHORIZED |
true |
Set to false if your environment uses a custom certificate chain (e.g. corporate proxy) |
UPSTREAM_IMAP_REJECT_MAILBOX |
[Gmail]/Trash |
Mailbox used for hard-rejected inbound messages (provider-specific) |
UPSTREAM_IMAP_USER |
you@gmail.com |
IMAP login username |
UPSTREAM_IMAP_PASS |
app-password |
IMAP password or app password |
Leave these blank to allow unauthenticated local connections (appropriate for single-user local use). Set them if you need to restrict who can connect to the local endpoints.
| Variable | Description |
|---|---|
SMTP_AUTH_USER |
Username required by email clients connecting to the local SMTP port |
SMTP_AUTH_PASS |
Password for the above |
IMAP_AUTH_USER |
Username required by email clients connecting to the local IMAP port |
IMAP_AUTH_PASS |
Password for the above |
| Variable | Default | Description |
|---|---|---|
FRONTEND_ORIGIN |
http://localhost:3000 |
Allowed CORS origin for the API |
NEXT_PUBLIC_API_BASE |
http://localhost:8787 |
API URL used by the Next.js frontend |
Rules are the core of FireClaw. Each rule specifies what to look for in a message and what to do when it matches.
| Field | What is checked |
|---|---|
from |
Sender address |
to |
Recipient address(es) |
subject |
Email subject line |
body |
Plain-text and HTML body content |
any |
All of the above simultaneously |
| Action | What happens |
|---|---|
allow |
Message is forwarded normally (default when no rules match) |
flag |
Message is forwarded and recorded in the quarantine log with flagged-forwarded status |
quarantine |
Message is stored locally; not forwarded. Review and release from the dashboard. |
reject |
Message is rejected at the SMTP level with a 554 error. The sender's email client receives a permanent delivery failure. |
Each rule can match using either plain substring search or a full regular expression:
- Substring match — case-insensitive by default. Set
caseSensitive: trueto require exact casing. - Regex match — full JavaScript
RegExpsyntax. Invalid patterns are safely skipped (logged as errors, never crash the server).
Example: flag phishing-style keywords
{
"name": "Phishing keywords",
"field": "any",
"pattern": "\\b(verify your account|urgent action required|confirm your identity)\\b",
"isRegex": true,
"caseSensitive": false,
"action": "flag"
}Example: reject outbound mail to a specific domain
{
"name": "Block competitor domain",
"field": "to",
"pattern": "competitor.com",
"isRegex": false,
"caseSensitive": false,
"action": "reject"
}Example: quarantine financial scam patterns
{
"name": "Financial scams",
"field": "any",
"pattern": "\\b(wire transfer|bitcoin wallet|crypto investment|guaranteed returns)\\b",
"isRegex": true,
"caseSensitive": false,
"action": "quarantine"
}When multiple rules match the same message, the highest-priority action wins:
allow (0) < flag (1) < quarantine (2) < reject (3)
A message matching both a flag rule and a quarantine rule will be quarantined.
FireClaw ships with a set of example rules (all disabled by default). You can enable them from the dashboard or use them as starting templates:
- English profanity filter (common words)
- English profanity filter (obfuscated with spaces/asterisks)
- Phishing keyword detection
- Financial scam keyword detection
The blacklist lets you block individual email addresses or entire domains without writing a rule. A blacklist match always results in quarantine.
Block a specific address:
{ "value": "spam@example.com", "type": "email" }Block an entire domain:
{ "value": "spammy-domain.com", "type": "domain" }Both inbound (checked by the IMAP poller) and outbound (checked by the SMTP firewall) messages are evaluated against the blacklist. A match on either sender or recipient triggers quarantine.
Quarantined messages are stored in the local SQLite database. The full raw RFC 822 message is preserved, so nothing is lost.
| Status | Meaning |
|---|---|
quarantined |
Held, not delivered |
flagged-forwarded |
Delivered, but logged for review |
released |
Manually approved by an admin and forwarded to the original destination |
discarded |
Permanently dismissed — not forwarded |
Outbound quarantine — the SMTP firewall catches the message before it reaches your provider. Releasing it re-sends via your upstream SMTP.
Inbound quarantine — the IMAP poller detects the message in your upstream INBOX, moves it to the OC-QUARANTINE mailbox on the server, and hides it from the IMAP proxy so it never appears in your email client. Releasing an inbound message removes it from quarantine so it reappears in your client on the next IMAP sync.
The Next.js dashboard at http://localhost:3000 provides a full management interface:
- Dashboard — live status of all services (SMTP, IMAP proxy, IMAP poller) and recent quarantined messages
- Rules — create, edit, enable/disable, and delete filtering rules with a live preview
- Blacklist — manage blocked addresses and domains with optional notes
- Quarantine inbox — review held messages, read full content, and release or discard them
- Settings — update all configuration values at runtime without restarting the server
The backend exposes a REST API at http://localhost:8787. All endpoints accept and return JSON.
GET /api/health → { ok: true, timestamp }
GET /api/status → service status + current config (passwords are redacted)
GET /api/rules → list all rules
POST /api/rules → create a rule
PUT /api/rules/:id → update a rule (partial updates supported)
DELETE /api/rules/:id → delete a rule
Rule object:
{
"name": "Block Nigerian prince scams",
"field": "any",
"pattern": "nigerian prince",
"isRegex": false,
"caseSensitive": false,
"action": "quarantine",
"enabled": true
}GET /api/blacklist → list all blacklist entries
POST /api/blacklist → add an entry
DELETE /api/blacklist/:id → remove an entry
Blacklist entry object:
{ "value": "spam@example.com", "type": "email", "notes": "Known spam sender" }type is either "email" (exact address match) or "domain" (matches any address at that domain).
GET /api/quarantine → list messages (query: ?limit=100, max 500)
POST /api/quarantine/:id/release → release and forward the message
POST /api/quarantine/:id/discard → discard permanently
GET /api/config → list all config key-value pairs
PUT /api/config → update config values (flat key-value object)
GET /api/database/backup → download a full SQLite backup (.db file)
POST /api/database/clear → reset the entire database
POST /api/database/clear-quarantine → clear quarantine messages only
Test a pattern against a string without sending any mail:
POST /api/debug/test-pattern
// Request
{
"pattern": "urgent action",
"text": "This is urgent action required, please verify your account",
"isRegex": false,
"caseSensitive": false
}
// Response
{
"pattern": "urgent action",
"result": true,
"error": null
}This is a pnpm workspace monorepo with two packages:
fireclaw/
├── apps/
│ ├── server/ # Node.js + TypeScript backend
│ │ └── src/
│ │ ├── index.ts # Bootstrap: initializes DB and starts all services
│ │ ├── smtp/
│ │ │ └── smtpFirewallServer.ts # SMTP receive → parse → evaluate → forward/quarantine/reject
│ │ ├── imap/
│ │ │ ├── imapProxy.ts # IMAP proxy with local auth and upstream credential isolation
│ │ │ └── imapPoller.ts # Background INBOX scanner, moves matches to quarantine mailbox
│ │ ├── rules/
│ │ │ └── evaluator.ts # Pure rule engine: (message, rules, blacklist) → decision
│ │ ├── api/
│ │ │ └── httpServer.ts # Express REST API
│ │ ├── db/
│ │ │ ├── database.ts # SQLite init and schema migration
│ │ │ ├── repository.ts # Data access layer
│ │ │ └── defaultRules.ts # Seed rules (disabled by default)
│ │ └── types/
│ │ └── index.ts # Shared TypeScript types
│ └── web/ # Next.js 14 admin dashboard
│ ├── app/ # App Router pages
│ ├── components/ # React components
│ └── lib/ # API client, types, configuration helpers
├── .env.example # Configuration template
├── package.json # Workspace root + dev/build scripts
└── pnpm-workspace.yaml
All persistent state lives in a single SQLite file at apps/server/data/firewall.db:
| Table | Purpose |
|---|---|
config |
Runtime configuration (key-value pairs) |
rules |
Filtering rules with field, pattern, action |
blacklist |
Blocked email addresses and domains |
quarantine_messages |
Full raw message stored alongside metadata and matched rules |
sync_state |
IMAP poller cursor — tracks the last scanned message UID |
client_deleted_uids |
UIDs deleted by the IMAP client — prevents re-appearance after sync |
Credential isolation — upstream IMAP/SMTP credentials never leave the server process. Email clients authenticate against local credentials (or bypass auth entirely). A compromised email client cannot extract your upstream provider password.
No message mutation — FireClaw stores the original raw RFC 822 message and forwards it byte-for-byte. Message headers are not rewritten or injected.
Stateless rule engine — the evaluator (rules/evaluator.ts) is a pure function: (message, rules, blacklist) → decision. It carries no internal state between calls, making it straightforward to test and reason about.
ES modules throughout — the server uses "type": "module" with NodeNext module resolution. All internal imports include .js extensions as required by the TypeScript + ESM convention.
- SMTP is plaintext by default.
STARTTLSis disabled on the local SMTP endpoint. This is safe for loopback connections but should not be exposed on a network interface without TLS termination in front of it. - IMAP auth methods. The proxy supports
LOGINandAUTHENTICATE PLAIN. Some clients that requireCRAM-MD5,OAUTH2, or other mechanisms are not supported in the current version. - Inbound quarantine release. Releasing an inbound message updates the local quarantine status and moves the IMAP UID back to the client-visible set. The message reappears in the email client on the next IMAP sync — it is not pushed immediately.
- Custom TLS certificates. If your environment intercepts TLS (e.g. a corporate proxy), set
UPSTREAM_IMAP_TLS_REJECT_UNAUTHORIZED=falseas a temporary workaround. This disables certificate validation for the upstream IMAP connection.
Contributions are welcome. To get started:
# Fork the repository on GitHub, then:
git clone https://github.com/leftcurveaeon/fireclaw.git
cd fireclaw
pnpm install
# Start in development mode
pnpm dev
# Type-check both apps
pnpm typecheck- Tests — the rule evaluator (
rules/evaluator.ts) is a pure function and a natural starting point for unit tests. SMTP and IMAP flows are good candidates for integration tests with real fixture messages. - Additional rule fields — adding headers like
reply-toorccis a focused change inevaluator.tsandtypes/index.ts. - SMTP TLS — the local SMTP endpoint does not yet support STARTTLS. Adding optional certificate configuration would improve compatibility with stricter email clients.
- Docker packaging — a
Dockerfileanddocker-compose.ymlfor easy deployment. - IMAP IDLE support — replace the polling interval with push-based notifications from the upstream server.
Please open a GitHub issue and include:
- A description of what you expected vs. what happened
- Relevant log output from the server (the firewall logs every rule evaluation to stdout by default)
- Your
.envconfiguration with all credentials replaced by placeholder values
MIT — see LICENSE for details.
FireClaw is not affiliated with any email provider.
Your upstream credentials are used only to forward mail to your provider and are never transmitted anywhere else.