From a2cf87e8fd18e43514dc62b89856ac6f4da11253 Mon Sep 17 00:00:00 2001 From: yilsem Date: Mon, 24 Aug 2026 11:30:03 +0300 Subject: [PATCH] catch-all email receiver: SMTP + REST API for verification codes --- .gitignore | 2 + Dockerfile | 7 ++++ package.json | 14 +++++++ server.js | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 package.json create mode 100644 server.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..713d500 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6fb8607 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/package.json b/package.json new file mode 100644 index 0000000..231e784 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..d8d7f1a --- /dev/null +++ b/server.js @@ -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));