I sent my first batch of cold outreach emails yesterday. Every one of them landed in the inbox — not a single one hit spam or the Promotions tab in Gmail.
That is not how it usually goes. Most founders send their first batch, don't notice anything is wrong, then two weeks later realize their domain reputation is already scorched and they're invisible to Gmail forever.
Here is the actual setup I used. It is all unglamorous infrastructure work. None of it is clever.
1. SPF, DKIM, and DMARC — all three, not just DKIM
These three records tell Gmail and Yahoo that you, the domain owner, authorize this server to send on your behalf. In 2026 they are effectively required for anything higher than a handful of messages per day.
# SPF (TXT record on @ or root)
v=spf1 include:_spf.resend.com ~all
# DKIM (TXT record on <selector>._domainkey)
# Your provider gives you this. Paste the full public key exactly as they hand it to you.
# DMARC (TXT record on _dmarc)
v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com```
Most guides stop at DKIM. **DMARC is the one that moves you from "Promotions" to "Primary"** in Gmail. `p=quarantine` means "if SPF or DKIM fail, send it to spam." That is what you want — it protects your domain against spoofing, and Gmail rewards domains that publish a policy.
## 2. List-Unsubscribe and List-Unsubscribe-Post headers
Gmail and Yahoo made these mandatory for bulk senders (5000+/day) in February 2024. But Gmail also penalizes smaller senders who don't include them — it's a "quality signal" either way.
```js
const headers = {
'List-Unsubscribe': `<mailto:unsubscribe@yourdomain.com>, <https://yourdomain.com/unsubscribe?email=${encoded}>`,
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
}
await resend.emails.send({
from: 'You <you@yourdomain.com>', to: recipient,
subject,
html,
text, // see next point
headers,
})
The One-Click variant lets Gmail show an unsubscribe link in the email client itself. Offer it. You want people who don't want to hear from you to have a zero-friction way to opt out — otherwise they hit "Report spam" and wreck your reputation for everyone else.
3. Always send a plain-text alternative alongside HTML
HTML-only messages get a small spam-score bump just for existing. Most spam filters expect a multipart/alternative body with a text version. Sending only HTML is a signal of a lazy sender.
export function htmlToPlainText(html: string): string {
return html
.replace(/<style[\s\S]*?<\/style>/gi, '')
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<\/(p|div|h[1-6]|li|tr|br)>/gi, '\n')
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/ /gi, ' ')
.replace(/&/gi, '&')
.replace(/[ \t]+\n/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim()
}
Pass it as text: alongside your html: and forget about it. Five minutes of work, measurable deliverability win.
4. Separate subdomain for cold outreach
This is the one most people miss. If you send cold outreach from you@yourdomain.com, a reputation hit from cold email tanks your transactional mail too — welcome emails, password resets, receipts. All of it starts landing in spam.
The fix:
-
mail.yourdomain.com— cold outreach only. New subdomain, new DKIM, new reputation to build. -
yourdomain.com— transactional mail only. Welcome, receipts, password resets, notifications.
Set up both in your sending provider (Resend, Postmark, AWS SES, whatever). If the cold subdomain gets flagged, your product's critical emails still land. The incremental DNS work takes 15 minutes.
5. Warm up slowly. Do NOT blast 500 emails day one.
Fresh domains have no reputation. Send 500 emails from a new domain in a single day and every major provider's reputation system flags you as a spammer on arrival.
The ramp that actually works:
- Day 1-5: 20/day
- Week 2: 30-50/day
- Week 3: 50-75/day
- Week 4+: 100+/day
I schedule mine via a Vercel Cron that reads from a queue, dedupes against a "already sent" table in Postgres, and caps the daily count:
// /api/cron/send-outreach
export async function POST(req: NextRequest) {
const auth = req.headers.get('authorization')
if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response('unauthorized', { status: 401 })
}
const today = daysSince(START_DATE)
const cap = rampSchedule(today) // 20, 20, 20, 20, 20, 30, 40, 50, 75, 100, 100...
const toSend = await queue.next(cap)
for (const contact of toSend) {
await resend.emails.send({ ...buildEmail(contact), headers })
await db.insert({ email: contact.email, status: 'sent', sent_at: new Date() })
await sleep(2000) // throttle within the batch
}
}
A unique index on email in the sent-log table means the same contact can never get a duplicate. Cheap insurance against bugs that would make you look unprofessional.
6. Quiet subject lines. No discount codes.
Gmail's classifier is keyword-aware. "50% OFF" in the subject, exclamation marks, ALL CAPS, emoji — all of that nudges your score toward Promotions.
Write subjects like you would to a colleague:
- ❌ "🚀 HUGE news about your hiring process!!"
- ✅ "Question about engineering hiring at Acme"
7. One physical mailing address in the footer
CAN-SPAM in the US requires a real postal address on commercial email. Skipping this is illegal AND it tanks your score. A PO Box is fine.
What this got me
11 emails sent as a warm-up batch. 0 bounces, 0 spam complaints, delivery confirmed to the inbox (not Promotions) on Gmail, Outlook, and Proton.
The next batches ramp from there via the cron above.
I'm building Evaluator — AI-powered technical assessments that generate a custom interview from your role description, then score submissions across code quality, problem solving, system design, communication, and debugging. Questions regenerate each run so candidates can't look them up.
Free tier is 10 full cycles/month, no credit card. If your team hires engineers, give it a look.
Top comments (0)