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 .", 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 }