93 lines
3.6 KiB
JavaScript
93 lines
3.6 KiB
JavaScript
'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}`);
|
||
});
|