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.
208 lines
5.4 KiB
JavaScript
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
|
|
} |