// Node.js 22+. Run: node email-mock-server.mjs // Local protocol fixture, not a mail server. No SMTP or provider SDK is used. import http from 'node:http'; import { randomUUID, createHash } from 'node:crypto'; const messages = new Map(); const keys = new Map(); const server = http.createServer(async (request, response) => { const reply = (status, value) => { response.writeHead(status, { 'content-type': 'application/json' }); response.end(JSON.stringify(value)); }; if (request.method === 'POST' && request.url === '/api/v1/emails') { let body = ''; for await (const part of request) { body += part; if (Buffer.byteLength(body) > 16384) return reply(413, { error: 'Too large' }); } let payload; try { payload = JSON.parse(body); } catch { return reply(400, { error: 'Invalid JSON' }); } const key = request.headers['idempotency-key']; if (typeof key !== 'string' || !key || key.length > 120 || !payload || typeof payload.to !== 'string' || typeof payload.subject !== 'string' || typeof payload.text !== 'string') return reply(400, { error: 'Key, to, subject and text required' }); const fingerprint = createHash('sha256').update(body).digest('hex'); const old = keys.get(key); if (old && old.fingerprint !== fingerprint) return reply(409, { error: 'Idempotency conflict' }); if (old) return reply(202, messages.get(old.id)); const id = randomUUID(); const message = { id, status: 'queued', mode: 'demo' }; messages.set(id, message); keys.set(key, { id, fingerprint }); return reply(202, message); } if (request.method === 'GET' && request.url?.startsWith('/api/v1/emails/')) { const message = messages.get(request.url.split('/').pop()); return reply(message ? 200 : 404, message || { error: 'Not found' }); } reply(404, { error: 'Not found' }); }); server.requestTimeout = 5000; server.headersTimeout = 5000; // Set EMAIL_FIXTURE_PORT=0 to let the operating system choose a free port. const port = Number(process.env.EMAIL_FIXTURE_PORT ?? 3027); server.listen(port, '127.0.0.1', () => console.log(`Email fixture: http://127.0.0.1:${server.address().port} (simulated; memory only)`));