115 lines
3.5 KiB
JavaScript
115 lines
3.5 KiB
JavaScript
const { SMTPServer } = require("smtp-server");
|
|
const { simpleParser } = require("mailparser");
|
|
const express = require("express");
|
|
|
|
const SMTP_PORT = parseInt(process.env.SMTP_PORT || "25", 10);
|
|
const API_PORT = parseInt(process.env.API_PORT || "3025", 10);
|
|
const API_KEY = process.env.API_KEY || "";
|
|
const MAX_AGE_MS = 3600_000; // 1 saat
|
|
|
|
const store = new Map();
|
|
|
|
function extractCode(text) {
|
|
if (!text) return null;
|
|
const m = text.match(/\b(\d{4,8})\b/);
|
|
return m ? m[1] : null;
|
|
}
|
|
|
|
function cleanup() {
|
|
const now = Date.now();
|
|
for (const [addr, mails] of store) {
|
|
const fresh = mails.filter((m) => now - m.ts < MAX_AGE_MS);
|
|
if (fresh.length) store.set(addr, fresh);
|
|
else store.delete(addr);
|
|
}
|
|
}
|
|
setInterval(cleanup, 60_000);
|
|
|
|
const smtp = new SMTPServer({
|
|
secure: false,
|
|
authOptional: true,
|
|
disabledCommands: ["STARTTLS"],
|
|
onData(stream, session, cb) {
|
|
const chunks = [];
|
|
stream.on("data", (c) => chunks.push(c));
|
|
stream.on("end", async () => {
|
|
try {
|
|
const raw = Buffer.concat(chunks);
|
|
const parsed = await simpleParser(raw);
|
|
const recipients = session.envelope.rcptTo.map((r) =>
|
|
r.address.toLowerCase()
|
|
);
|
|
const entry = {
|
|
ts: Date.now(),
|
|
from: parsed.from?.text || "",
|
|
subject: parsed.subject || "",
|
|
text: parsed.text || "",
|
|
html: parsed.html || "",
|
|
code: extractCode(parsed.text) || extractCode(parsed.subject),
|
|
};
|
|
for (const addr of recipients) {
|
|
if (!store.has(addr)) store.set(addr, []);
|
|
store.get(addr).push(entry);
|
|
if (store.get(addr).length > 50) store.get(addr).shift();
|
|
}
|
|
console.log(
|
|
`[MAIL] ${entry.from} -> ${recipients.join(",")} subj="${entry.subject}" code=${entry.code || "-"}`
|
|
);
|
|
} catch (e) {
|
|
console.error("[MAIL] parse error:", e.message);
|
|
}
|
|
cb();
|
|
});
|
|
},
|
|
});
|
|
|
|
const app = express();
|
|
|
|
app.use((req, res, next) => {
|
|
if (API_KEY && req.headers["x-api-key"] !== API_KEY) {
|
|
return res.status(401).json({ error: "unauthorized" });
|
|
}
|
|
next();
|
|
});
|
|
|
|
app.get("/health", (_req, res) => {
|
|
res.json({ status: "ok", stored: store.size });
|
|
});
|
|
|
|
app.get("/mail/:address", (req, res) => {
|
|
const addr = req.params.address.toLowerCase();
|
|
const mails = store.get(addr) || [];
|
|
const limit = parseInt(req.query.limit || "10", 10);
|
|
res.json({ address: addr, count: mails.length, mails: mails.slice(-limit) });
|
|
});
|
|
|
|
app.get("/code/:address", (req, res) => {
|
|
const addr = req.params.address.toLowerCase();
|
|
const mails = store.get(addr) || [];
|
|
const last = [...mails].reverse().find((m) => m.code);
|
|
if (last) {
|
|
res.json({ address: addr, code: last.code, subject: last.subject, age_s: Math.round((Date.now() - last.ts) / 1000) });
|
|
} else {
|
|
res.json({ address: addr, code: null, message: "no code found" });
|
|
}
|
|
});
|
|
|
|
app.get("/codes", (_req, res) => {
|
|
const result = {};
|
|
for (const [addr, mails] of store) {
|
|
const last = [...mails].reverse().find((m) => m.code);
|
|
if (last) result[addr] = { code: last.code, age_s: Math.round((Date.now() - last.ts) / 1000) };
|
|
}
|
|
res.json(result);
|
|
});
|
|
|
|
app.delete("/mail/:address", (req, res) => {
|
|
store.delete(req.params.address.toLowerCase());
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
smtp.listen(SMTP_PORT, () => console.log(`[SMTP] listening on :${SMTP_PORT}`));
|
|
app.listen(API_PORT, () => console.log(`[API] listening on :${API_PORT}`));
|
|
|
|
smtp.on("error", (e) => console.error("[SMTP] error:", e.message));
|