Jawab/services/smtp/actions.js
azures04 234af0e032 Add logger module, services, and package files
Introduce a new logger (modules/logger.js) that prints colorized console output, writes timestamped logs to a file in a logs/ directory, strips ANSI colors for file output, and handles process exit/SIGINT cleanup. Add service stubs (services/security.js, services/storage.js, services/smtp/actions.js, services/smtp/authenticate.js) and a test.js. Add package.json and package-lock.json with runtime deps (colors, dotenv, zod) and dev tooling (eslint, nodemon) to support the new code.
2026-03-06 11:21:23 +01:00

208 lines
5.4 KiB
JavaScript

const auth = require("./authenticate")
const storage = require("../storage")
const security = require("../security")
const codes = {
220: "Service ready",
221: "Closing transmission channel",
235: "2.7.0 Authentication successful",
250: "Requested mail action okay, completed",
354: "Start mail input; end with <CR><LF>.<CR><LF>",
421: "Service not available, closing transmission channel",
451: "Requested action aborted: local error in processing",
500: "Syntax error, command unrecognized",
501: "Syntax error in parameters or arguments",
503: "Bad sequence of commands",
530: "5.7.0 Authentication required",
535: "5.7.8 Authentication credentials invalid",
550: "Requested action not taken: mailbox unavailable",
552: "Requested mail action aborted: exceeded storage allocation"
}
const features = ["Jawab Mail Server", "AUTH PLAIN", "SIZE 10485760", "HELP"]
function reply(socket, code, extra = null) {
const message = codes[code]
if (Array.isArray(extra)) {
for (let i = 0; i < extra.length - 1; i++) {
socket.write(`${code}-${extra[i]}\r\n`);
}
return socket.write(`${code} ${extra[extra.length - 1]}\r\n`)
}
const responseText = extra ? `${message} ${extra}` : message
return socket.write(`${code} ${responseText}\r\n`)
}
function handleCommand(socket, session, line) {
if (!security.isLineSafe(line)) {
return reply(socket, 500, "Line too long")
}
const parts = line.split(" ")
const command = parts[0].toUpperCase()
const args = parts.slice(1).join(" ")
if (session.isCollectingData) {
return handleDataStreaming(socket, session, line)
}
switch (command) {
case "EHLO":
case "HELO":
handleHelo(socket, session, args)
break
case "AUTH":
handleAuth(socket, session, args)
break
case "MAIL":
handleMailFrom(socket, session, args)
break
case "RCPT":
handleRcptTo(socket, session, args)
break
case "DATA":
handleDataStart(socket, session)
break
case "RSET":
handleReset(socket, session)
break
case "QUIT":
handleQuit(socket)
break
case "NOOP":
reply(socket, 250)
break
default:
reply(socket, 500)
}
}
function handleAuth(socket, session, args) {
const [mechanism, payload] = args.split(" ")
if (mechanism !== "PLAIN" || !payload) {
return reply(socket, 501)
}
const { identity, secret } = auth.decodeSASL(payload)
if (auth.authenticate(identity, secret)) {
session.authenticated = true
session.user = identity
return reply(socket, 235)
} else {
return reply(socket, 535)
}
}
function handleMailFrom(socket, session, args) {
if (!session.authenticated) {
return reply(socket, 530)
}
const emailMatch = args.match(/FROM:\s*<(.+?)>/i)
if (!emailMatch) {
return reply(socket, 501)
}
const email = emailMatch[1]
if (!security.validateEmail(email)) {
return reply(socket, 501)
}
session.from = email
session.sizeChecker = security.createSizeChecker()
return reply(socket, "250")
}
function handleRcptTo(socket, session, args) {
if (!session.from) {
return reply(socket, 503)
}
const emailMatch = args.match(/TO:\s*<(.+?)>/i)
if (!emailMatch) {
return reply(socket, 501)
}
const email = emailMatch[1]
if (!security.validateEmail(email)) {
return reply(socket, 501)
}
session.to = email
return reply(socket, "250")
}
function handleDataStart(socket, session) {
if (!session.from || !session.to) return reply(socket, 503)
const { writeStream } = storage.createMailWriteStream()
session.isCollectingData = true
session.currentWriteStream = writeStream
return reply(socket, 354)
}
function handleDataStart(socket, session) {
if (!session.from || !session.to) return reply(socket, 503)
const { writeStream } = storage.createMailWriteStream()
session.currentWriteStream = writeStream
session.sizeChecker = security.createSizeChecker()
session.isCollectingData = true
return reply(socket, 354)
}
function handleDataStreaming(socket, session, line) {
if (line.trim() === ".") {
session.isCollectingData = false
session.currentWriteStream.end()
session.currentWriteStream = null
return reply(socket, 250)
}
if (session.sizeChecker && !session.sizeChecker(line.length)) {
session.isCollectingData = false
session.currentWriteStream.destroy()
return reply(socket, 552)
}
return session.currentWriteStream.write(line + "\n")
}
function handleReset(socket, session) {
if (session.currentWriteStream) {
session.currentWriteStream.destroy()
}
session.from = null
session.to = null
session.isCollectingData = false
return reply(socket, 250)
}
function handleQuit(socket) {
reply(socket, 221)
return socket.end()
}
function handleHelo(socket, session, args) {
if (!args) {
return reply(socket, 501)
}
session.helo = args
session.hasHelo = true
return reply(socket, 250, features)
}
module.exports = {
handleCommand
}