Self-hosted inbox warm-up for cold-email senders. Bring your own seed pool of inboxes you already own, and let them quietly exchange real-looking emails so your sending domains build a clean reputation before your first cold campaign.
Part of the pypesdev open-source stack. Pairs naturally with coldflow: warm your domains here, then send your real campaigns there.
The single biggest deliverability blocker for new cold-email senders is cold-domain reputation. Mailbox providers (Gmail, Outlook, Yahoo) have no engagement history for your domain, so even legitimate first sends drop straight into spam.
Inbox-warmer fixes this the boring, durable way: it sends small numbers of conversational, plain-text emails between Google Workspace inboxes that you own, on a jittered schedule, so providers see your domain participating in normal-looking back-and-forth before any real outreach happens.
Most commercial warm-up tools work by joining all their users into one giant shared peer pool — your domain trades emails with strangers' domains. That's an abuse vector and a reputational risk: one bad actor in the pool can drag everyone down with them.
v1 is intentionally BYO seed pool. You connect 3–10 inboxes that you already own (different mailbox accounts on different domains, ideally on different providers). The worker only ever exchanges emails between inboxes in your own pool. We will never silently pair your domain with someone else's.
If you want the shared-pool model, this is not the tool for you (yet — see roadmap).
What works in v0.1:
- ✅ Connect Google Workspace inboxes via full OAuth (Gmail API, refresh tokens stored encrypted at rest).
- ✅ Heartbeat-style scheduled send: a cron tick picks one eligible (sender, recipient) pair from your pool and sends one short, plain-text, conversation-flavoured email.
- ✅ Per-inbox daily send target with simple pacing.
- ✅ Single-page dashboard: connected inboxes, today's send count, status badges.
- ✅ AES-256-GCM token encryption. Refresh tokens never sit in the DB in plaintext.
Not in v0.1 (planned for v0.2 — see Roadmap below):
- ❌ Reply-and-mark-as-important loop (receiver auto-replies and stars / archives).
- ❌ DMARC pass-rate / reputation signals on the dashboard.
- ❌ Per-pair scheduling (currently one send per cron tick).
- ❌ Outlook / Microsoft 365.
- ❌ AI-generated email bodies.
- Node.js 20+
- A Postgres 14+ instance (local Docker is fine)
- Google Cloud project with the Gmail API enabled
- 3–10 Google Workspace inboxes you own and can sign into
git clone git@github.com:pypesdev/inbox-warmer.git
cd inbox-warmer
pnpm install # or npm install / yarn installdocker run --name inbox-warmer-pg -d \
-e POSTGRES_PASSWORD=warmer \
-e POSTGRES_DB=inbox_warmer \
-p 5432:5432 postgres:16- Go to https://console.cloud.google.com → enable the Gmail API.
- OAuth consent screen → External, add yourself as a test user.
- Credentials → Create credentials → OAuth client ID → Web application.
- Authorized redirect URI:
http://localhost:3000/api/inboxes/oauth/callback - Save the client ID and client secret.
Copy .env.example to .env and fill in:
cp .env.example .env
# Generate the encryption key:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# Generate any random CRON_SECRET (openssl rand -hex 32 works).pnpm db:generate # generates SQL migration in ./drizzle
pnpm db:migrate # applies it to the local Postgres
pnpm dev # http://localhost:3000Click + connect Google inbox on the dashboard, authorize, and repeat for every inbox in your seed pool (minimum 2; recommended 3–10).
Locally, you can fire the tick manually to verify pairing works:
curl -X POST http://localhost:3000/api/cron/tick \
-H "Authorization: Bearer $CRON_SECRET"In production, point any scheduler at this endpoint every few minutes:
- Vercel: add
vercel.jsonwith a cron entry on/api/cron/tick(recommended interval: every 5–15 minutes). - GitHub Actions: a cron
schedule:workflow thatcurls the endpoint. - Anything else that can issue an authenticated HTTP request.
┌──────────────────────────────────────┐
│ Your seed pool (DB) │
│ inbox A · inbox B · inbox C · … │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
cron tick → │ pick eligible sender (under target) │
│ pick a different inbox as recipient │
│ pick a short, conversational body │
│ send via Gmail API, log it │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ send_log row → dashboard counters │
└──────────────────────────────────────┘
Per-inbox daily target defaults to 10 sends (DAILY_SENDS_PER_INBOX). Every cron tick performs one send. Run the cron every 5–15 minutes to spread sends across the day.
- Refresh tokens are encrypted with AES-256-GCM before they touch the database. Without
GMAIL_ENCRYPTION_KEYthey cannot be read, even with full DB access. - OAuth state is a random 16-byte value stored in a 10-minute,
HttpOnly,SameSite=Laxcookie and verified on the callback to prevent CSRF. - Cron endpoint requires a
Bearertoken fromCRON_SECRET. - The app never stores email bodies or the recipient address book — only the inbox metadata you connected and a log of which-pair-sent-which-subject-when (for pacing and the dashboard).
- v0.2 — Reply loop (receiver auto-replies, marks important, archives) + per-pair scheduling.
- v0.3 — DMARC / SPF / DKIM signal panel (consumes the
dmarc-doctorlibrary). - v0.4 — Outlook / Microsoft 365 support.
- No timeline — Shared cross-user peer pool. We may never ship this; the abuse and legal model is hard, and BYO is the safer story.
Bug reports and PRs are welcome — please open an issue first for anything beyond a small fix so we can align on scope.
MIT © Pypes LLC