How to Receive Emails in Node.js (Without a Mail Server)
Receive incoming emails in Node.js using webhooks and inbound email parsing. Express handler with signature verification, reply threading, and routing examples.
To receive emails in Node.js, use an inbound parsing service that forwards incoming emails to your webhook as JSON. No SMTP server, no IMAP polling, no mail infrastructure.
Setup:
- MX record →
inbound.sendpigeon.dev - Express route handles webhook POSTs
- Verify signature, process email
No mail server needed. Works with Express, Fastify, Hono, Koa — anything that handles POST requests.
Why Not Run Your Own SMTP Server?
You can receive email in Node.js with packages like smtp-server or Haraka. But then you're managing:
- TLS certificates for SMTP
- Spam and virus filtering
- SPF/DKIM/DMARC validation
- Connection handling and backpressure
- Uptime monitoring for a mail server
An inbound parsing service handles all of this. You get clean JSON at a webhook URL.
Setup
1. Add the MX Record
Point your domain (or subdomain) to the parsing service:
| Type | Priority | Value |
|---|---|---|
| MX | 10 | inbound.sendpigeon.dev |
Use a subdomain like inbound.yourdomain.com to keep inbound parsing separate from your team's mailboxes.
2. Configure the Webhook
Set your webhook URL in the SendPigeon dashboard or via API:
curl -X PUT https://api.sendpigeon.dev/v1/domains/{domainId}/inbound \
-H "Authorization: Bearer sp_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "webhookEnabled": true, "webhookUrl": "https://yourapp.com/api/inbound" }'
3. Handle the Webhook
import express from "express";
import { createHmac } from "crypto";
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.SENDPIGEON_WEBHOOK_SECRET!;
function verifySignature(payload: string, signature: string, timestamp: string): boolean {
const expected = createHmac("sha256", WEBHOOK_SECRET)
.update(`${timestamp}.${payload}`)
.digest("hex");
return expected === signature;
}
app.post("/api/inbound", (req, res) => {
const signature = req.headers["x-webhook-signature"] as string;
const timestamp = req.headers["x-webhook-timestamp"] as string;
const payload = JSON.stringify(req.body);
if (!verifySignature(payload, signature, timestamp)) {
return res.status(401).send("Invalid signature");
}
const { event, data } = req.body;
if (event === "email.received") {
const { id, from, to, subject, text, html } = data;
console.log(`Email from ${from}: ${subject}`);
// Process asynchronously — don't block the response
processEmail(data).catch(console.error);
}
res.status(200).send("OK");
});
app.listen(3000);
Always verify the signature. Without it, anyone who discovers your webhook URL can inject fake emails into your system.
Routing by Recipient Address
Use address-based routing to direct emails to different handlers:
async function processEmail(data: InboundEmail) {
const { to, from, subject, text, html, id } = data;
// support@yourdomain.com → ticket system
if (to.startsWith("support@")) {
return createTicket({ from, subject, body: text || html });
}
// reply+{token}@yourdomain.com → thread reply
const replyMatch = to.match(/^reply\+(.+)@/);
if (replyMatch) {
return addReply(replyMatch[1], { from, body: text || html });
}
// invoice-{id}@yourdomain.com → document processing
const invoiceMatch = to.match(/^invoice-(\w+)@/);
if (invoiceMatch) {
return processInvoiceReply(invoiceMatch[1], data);
}
console.log(`Unrouted email to ${to}`);
}
Replying with Threading
Reply to inbound emails so the response appears in the sender's original thread:
async function replyToInbound(emailId: string, responseHtml: string) {
const res = await fetch(
`https://api.sendpigeon.dev/v1/inbound/emails/${emailId}/reply`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENDPIGEON_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ html: responseHtml }),
}
);
return res.json();
}
Threading headers (In-Reply-To, References) are set automatically.
IMAP Polling vs. Webhooks
| Aspect | IMAP Polling | Inbound Webhooks |
|---|---|---|
| Latency | Seconds to minutes (depends on poll interval) | Instant |
| Connections | Persistent IMAP connection per mailbox | Standard HTTP |
| Complexity | High (TLS, auth, connection management) | Low (one POST handler) |
| Reliability | Connection drops, timeout handling | Retries with backoff |
| Scaling | One connection per mailbox | One endpoint for all |
| Infrastructure | IMAP-capable mailbox | MX record + webhook URL |
Webhooks win on every axis unless you need to read from an existing mailbox you don't control.
Handling Attachments
The webhook payload includes rawUrl — a presigned URL to the original .eml file:
import { simpleParser } from "mailparser";
async function extractAttachments(rawUrl: string) {
const res = await fetch(rawUrl);
const rawEmail = await res.text();
const parsed = await simpleParser(rawEmail);
for (const attachment of parsed.attachments) {
console.log(`${attachment.filename} (${attachment.contentType}, ${attachment.size} bytes)`);
// Save to S3, process, etc.
}
return parsed.attachments;
}
Error Handling
app.post("/api/inbound", async (req, res) => {
// Verify signature first...
try {
await processEmail(req.body.data);
} catch (error) {
// Log the error but still return 200
// Returning 4xx/5xx triggers retries — only do this if you want a retry
console.error("Failed to process inbound email:", error);
}
// Always return 200 to acknowledge receipt
res.status(200).send("OK");
});
SendPigeon retries failed deliveries 3 times with exponential backoff. Return 200 to acknowledge receipt. Only return 4xx/5xx if you genuinely want a retry.
Production Checklist
- Webhook URL is HTTPS
- Signature verification in place
- Handler returns 200 quickly (queue heavy work)
- MX record configured on domain/subdomain
- Error handling doesn't block the response
- Tested with SendPigeon's webhook tester
Next Steps
- Read the full inbound email parsing guide
- Set up inbound email in the docs
- Learn how to send email in Node.js with the same SDK
- Receive emails in Next.js with App Router
- Build an AI email agent powered by inbound parsing