feat: admin frontend - React + Vite, auth pages, user management, roles, permissions, webhooks, devices, logs
This commit is contained in:
185
frontend/admin/scripts/mock-smtp-capture.mjs
Normal file
185
frontend/admin/scripts/mock-smtp-capture.mjs
Normal file
@@ -0,0 +1,185 @@
|
||||
import process from 'node:process'
|
||||
import path from 'node:path'
|
||||
import net from 'node:net'
|
||||
import { appendFile, mkdir, writeFile } from 'node:fs/promises'
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = new Map()
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const value = argv[index]
|
||||
if (!value.startsWith('--')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const key = value.slice(2)
|
||||
const nextValue = argv[index + 1]
|
||||
if (nextValue && !nextValue.startsWith('--')) {
|
||||
args.set(key, nextValue)
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
args.set(key, 'true')
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv.slice(2))
|
||||
const port = Number(args.get('port') ?? process.env.SMTP_CAPTURE_PORT ?? 2525)
|
||||
const outputPath = path.resolve(args.get('output') ?? process.env.SMTP_CAPTURE_OUTPUT ?? './smtp-capture.jsonl')
|
||||
|
||||
if (!Number.isInteger(port) || port <= 0) {
|
||||
throw new Error(`Invalid SMTP capture port: ${port}`)
|
||||
}
|
||||
|
||||
await mkdir(path.dirname(outputPath), { recursive: true })
|
||||
await writeFile(outputPath, '', 'utf8')
|
||||
|
||||
let writeQueue = Promise.resolve()
|
||||
|
||||
function queueMessageWrite(message) {
|
||||
writeQueue = writeQueue.then(() => appendFile(outputPath, `${JSON.stringify(message)}\n`, 'utf8'))
|
||||
return writeQueue
|
||||
}
|
||||
|
||||
function createSessionState() {
|
||||
return {
|
||||
buffer: '',
|
||||
dataMode: false,
|
||||
mailFrom: '',
|
||||
rcptTo: [],
|
||||
data: '',
|
||||
}
|
||||
}
|
||||
|
||||
const server = net.createServer((socket) => {
|
||||
socket.setEncoding('utf8')
|
||||
let session = createSessionState()
|
||||
|
||||
const reply = (line) => {
|
||||
socket.write(`${line}\r\n`)
|
||||
}
|
||||
|
||||
const resetMessageState = () => {
|
||||
session.dataMode = false
|
||||
session.mailFrom = ''
|
||||
session.rcptTo = []
|
||||
session.data = ''
|
||||
}
|
||||
|
||||
const flushBuffer = async () => {
|
||||
while (true) {
|
||||
if (session.dataMode) {
|
||||
const messageTerminatorIndex = session.buffer.indexOf('\r\n.\r\n')
|
||||
if (messageTerminatorIndex === -1) {
|
||||
session.data += session.buffer
|
||||
session.buffer = ''
|
||||
return
|
||||
}
|
||||
|
||||
session.data += session.buffer.slice(0, messageTerminatorIndex)
|
||||
session.buffer = session.buffer.slice(messageTerminatorIndex + 5)
|
||||
|
||||
const capturedMessage = {
|
||||
timestamp: new Date().toISOString(),
|
||||
mailFrom: session.mailFrom,
|
||||
rcptTo: session.rcptTo,
|
||||
data: session.data.replace(/\r\n\.\./g, '\r\n.'),
|
||||
}
|
||||
|
||||
await queueMessageWrite(capturedMessage)
|
||||
resetMessageState()
|
||||
reply('250 OK')
|
||||
continue
|
||||
}
|
||||
|
||||
const lineEndIndex = session.buffer.indexOf('\r\n')
|
||||
if (lineEndIndex === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const line = session.buffer.slice(0, lineEndIndex)
|
||||
session.buffer = session.buffer.slice(lineEndIndex + 2)
|
||||
const normalized = line.toUpperCase()
|
||||
|
||||
if (normalized.startsWith('EHLO')) {
|
||||
socket.write('250-localhost\r\n250 OK\r\n')
|
||||
continue
|
||||
}
|
||||
|
||||
if (normalized.startsWith('HELO')) {
|
||||
reply('250 OK')
|
||||
continue
|
||||
}
|
||||
|
||||
if (normalized.startsWith('MAIL FROM:')) {
|
||||
resetMessageState()
|
||||
session.mailFrom = line.slice('MAIL FROM:'.length).trim()
|
||||
reply('250 OK')
|
||||
continue
|
||||
}
|
||||
|
||||
if (normalized.startsWith('RCPT TO:')) {
|
||||
session.rcptTo.push(line.slice('RCPT TO:'.length).trim())
|
||||
reply('250 OK')
|
||||
continue
|
||||
}
|
||||
|
||||
if (normalized === 'DATA') {
|
||||
session.dataMode = true
|
||||
session.data = ''
|
||||
reply('354 End data with <CR><LF>.<CR><LF>')
|
||||
continue
|
||||
}
|
||||
|
||||
if (normalized === 'RSET') {
|
||||
resetMessageState()
|
||||
reply('250 OK')
|
||||
continue
|
||||
}
|
||||
|
||||
if (normalized === 'NOOP') {
|
||||
reply('250 OK')
|
||||
continue
|
||||
}
|
||||
|
||||
if (normalized === 'QUIT') {
|
||||
reply('221 Bye')
|
||||
socket.end()
|
||||
return
|
||||
}
|
||||
|
||||
reply('250 OK')
|
||||
}
|
||||
}
|
||||
|
||||
socket.on('data', (chunk) => {
|
||||
session.buffer += chunk
|
||||
void flushBuffer().catch((error) => {
|
||||
console.error(error?.stack ?? String(error))
|
||||
socket.destroy(error)
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('error', () => {})
|
||||
reply('220 localhost ESMTP ready')
|
||||
})
|
||||
|
||||
server.listen(port, '127.0.0.1', () => {
|
||||
console.log(`SMTP capture listening on 127.0.0.1:${port}`)
|
||||
})
|
||||
|
||||
async function shutdown() {
|
||||
server.close()
|
||||
await writeQueue.catch(() => {})
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
void shutdown().finally(() => process.exit(0))
|
||||
})
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
void shutdown().finally(() => process.exit(0))
|
||||
})
|
||||
Reference in New Issue
Block a user