catch-all email receiver: SMTP + REST API for verification codes
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json ./
|
||||||
|
RUN npm install --production
|
||||||
|
COPY server.js ./
|
||||||
|
EXPOSE 25 3025
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "desehavuz-mail",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Catch-all email receiver with API for verification codes",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"smtp-server": "^3.13.0",
|
||||||
|
"mailparser": "^3.7.1",
|
||||||
|
"express": "^4.21.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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