'use strict'; /** * Мок Unisender Go (goapi.unisender.ru/ru/transactional/api/v1/email/send.json). * Реальных отправок НЕТ — каждое письмо пишется в MAIL_DIR как JSON-файл. * Валидирует контракт как настоящий API (те же коды ошибок) — чтобы ловить * регрессии формы (body.text vs plaintext и т.п.) без платных писем. * * Эндпоинты: * POST /ru/transactional/api/v1/email/send.json — приём письма (X-API-KEY или api_key) * GET /health — живость */ const http = require('node:http'); const fs = require('node:fs'); const path = require('node:path'); const PORT = Number(process.env.PORT || 8080); const MAIL_DIR = process.env.MAIL_DIR || '/mail'; fs.mkdirSync(MAIL_DIR, { recursive: true }); function json(res, code, body) { const payload = JSON.stringify(body); res.writeHead(code, { 'content-type': 'application/json' }); res.end(payload); } function apiError(res, code, message) { json(res, 400, { status: 'error', code, message }); } function saveMail(payload, apiKey) { const ts = new Date().toISOString().replace(/[:.]/g, '-'); const jobId = `mock-${ts}`; const file = path.join(MAIL_DIR, `${ts}.json`); const msg = payload.message || {}; const recipients = (msg.recipients || []).map((r) => r.email); const record = { job_id: jobId, receivedAt: new Date().toISOString(), api_key: apiKey, from_email: msg.from_email || null, from_name: msg.from_name || null, subject: msg.subject || null, recipients, body: msg.body || null, raw: payload, }; fs.writeFileSync(file, JSON.stringify(record, null, 2) + '\n'); return { jobId, recipients }; } const server = http.createServer(async (req, res) => { const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`); if (req.method === 'GET' && url.pathname === '/health') return json(res, 200, { ok: true }); if (req.method === 'POST' && url.pathname === '/ru/transactional/api/v1/email/send.json') { const chunks = []; for await (const c of req) chunks.push(c); let payload; try { payload = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { return apiError(res, 111, 'JSON parsing error'); } const apiKey = req.headers['x-api-key'] || payload.api_key; if (!apiKey) return apiError(res, 101, 'API key is missing'); const msg = payload.message; if (!msg || typeof msg !== 'object') return apiError(res, 2000, "Error in 'message' field. Message object required."); if (!Array.isArray(msg.recipients) || msg.recipients.length === 0 || !msg.recipients[0].email) { return apiError(res, 2001, "Error in 'recipients' field. Recipients required."); } const body = msg.body || {}; if (!body.plaintext && !body.html) { // зеркалит реальный ответ: 400 code 1577 No message body passed return apiError(res, 1577, "Error in 'body' field. No message body passed."); } if (!msg.from_email) return apiError(res, 2002, "Error in 'from_email' field. Sender required."); const { jobId, recipients } = saveMail(payload, apiKey); console.log(`✉️ mock email: ${msg.subject || '(no subject)'} -> ${recipients.join(', ')} (${jobId})`); return json(res, 200, { status: 'success', job_id: jobId, emails: recipients, tags: [] }); } return json(res, 404, { status: 'error', code: 404, message: 'not found' }); }); server.listen(PORT, '0.0.0.0', () => { console.log(`unisender-mock on :${PORT}, mail dir: ${MAIL_DIR}`); });