catch-all email receiver: SMTP + REST API for verification codes
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
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));
|
||||
Reference in New Issue
Block a user