diff --git a/.gitignore b/.gitignore index 41f94ea..c7080e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules config.json mails -cert \ No newline at end of file +cert +dist \ No newline at end of file diff --git a/dist/Logger.d.ts b/dist/Logger.d.ts deleted file mode 100644 index d751143..0000000 --- a/dist/Logger.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -declare const COLOR: { - GRAY: string; - DARK: string; - RED: string; - GREEN: string; - YELLOW: string; - PINK: string; - TEAL: string; - WHITE: string; -}; -export default class Logger { - private readonly actor; - private readonly color; - constructor(actor: string, color: keyof typeof COLOR); - log(...message: string[]): void; - error(...message: string[]): void; - warn(...message: string[]): void; - debug(...message: string[]): void; - trace(...message: string[]): void; -} -export {}; diff --git a/dist/Logger.js b/dist/Logger.js deleted file mode 100644 index 1fd5aa6..0000000 --- a/dist/Logger.js +++ /dev/null @@ -1,48 +0,0 @@ -const ESC = "\u001b"; -const RESET = ESC + "[m"; -const GRAY = ESC + "[37m"; -const DARK = ESC + "[90m"; -const RED = ESC + "[91m"; -const GREEN = ESC + "[92m"; -const YELLOW = ESC + "[93m"; -const PINK = ESC + "[95m"; -const TEAL = ESC + "[96m"; -const WHITE = ESC + "[97m"; -const COLOR = { GRAY, DARK, RED, GREEN, YELLOW, PINK, TEAL, WHITE }; -/* eslint no-console: "off" */ -export default class Logger { - actor; - color; - constructor(actor, color) { - this.actor = actor; - this.color = color; - if (!global.debug) - this.debug = () => { }; // only enable debug logs when debugging is on - } - log(...message) { - console.log(`${WHITE}[${DARK}${time()}${WHITE}] [${COLOR[this.color]}${this.actor}${WHITE}]${GRAY}`, message.join(" ")); - } - error(...message) { - console.log(`${WHITE}[${DARK}${time()}${WHITE}] [${COLOR[this.color]}${this.actor}${WHITE}]${RED}`, message.join(" ")); - } - warn(...message) { - console.warn(`${WHITE}[${DARK}${time()}${WHITE}] [${COLOR[this.color]}${this.actor}${WHITE}]${YELLOW}`, message.join(" ")); - } - debug(...message) { - console.debug(`${WHITE}[${DARK}${time()}${WHITE}] [${COLOR[this.color]}${this.actor}${WHITE}]${TEAL}`, message.join(" ")); - } - trace(...message) { - console.trace(`${WHITE}[${DARK}${time()}${WHITE}] [${COLOR[this.color]}${this.actor}${WHITE}]${WHITE}`, message.join(" ")); - } -} -/* eslint prefer-template: "off" */ -function time() { - const d = new Date(); - const year = d.getUTCFullYear(); - const month = (d.getUTCMonth() + "").padStart(2, "0"); - const day = (d.getUTCDate() + "").padStart(2, "0"); - const hours = (d.getUTCHours() + "").padStart(2, "0"); - const mins = (d.getUTCMinutes() + "").padStart(2, "0"); - const secs = (d.getUTCSeconds() + "").padStart(2, "0"); - return `${year}-${month}-${day} ${hours}:${mins}:${secs}`; -} diff --git a/dist/config.d.ts b/dist/config.d.ts deleted file mode 100644 index d9e61ac..0000000 --- a/dist/config.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -type ConfigValue = string | number | boolean; -export default function getConfig(key: string, defaultValue?: T): T; -export {}; diff --git a/dist/config.js b/dist/config.js deleted file mode 100644 index fa59406..0000000 --- a/dist/config.js +++ /dev/null @@ -1,71 +0,0 @@ -import Logger from "./Logger.js"; -import { existsSync } from "fs"; -import { readFile } from "fs/promises"; -const config = existsSync("config.json") ? JSON.parse(await readFile("config.json", "utf-8")) : {}; -const logger = new Logger("config", "PINK"); -export default function getConfig(key, defaultValue) { - let value; - let error; - try { - value = searchJsonKey(key); - // console.log(`json: [${searchJsonKey(key)}]`) - } - catch (e) { - error = e; - } - value = value ?? getEnvVar(key) ?? defaultValue; - // console.log("config:", key, value) - if (value != undefined) { - if (error) - logger.warn(error); - if (defaultValue != undefined && typeof value != typeof defaultValue) { - // throw error when types don't match - throw new Error(`Config type of ${key} does not match default value (${typeof defaultValue}).`); - } - return value; - } - throw error || new Error(`Config key ${key} not found`); -} -function getEnvVar(key) { - const value = process.env[key] ?? process.env[key.replaceAll(".", "_")]; - if (!value) - return undefined; - return parseStringValue(value); -} -function parseStringValue(value) { - // boolean - if (value == "true") - return true; - else if (value == "false") - return false; - // number - else if (!isNaN(Number(value))) - return Number(value); - // string - return value; -} -function searchJsonKey(key) { - const path = key.split("."); - // console.log("[getConfig]", key, path) - let value = config[path[0]]; - if (!value) { - if (path.length > 1) - throw `Config key ${key} not found (${path[0]} is invalid: ${value})`; - return undefined; - } - for (let i = 1; i < path.length; i++) { - if (typeof value != "object" || value == null) - throw `Config key ${key} not found (${path.slice(0, i).join(".")} is ${typeof value})`; - if (!(path[i] in value)) - throw `Config key ${key} not found (${path[i]} does not exist on ${path.slice(0, i).join(".")})`; - value = value[path[i]]; - } - if (isValid(value)) - return value; - if (typeof value == "object") - logger.warn(`Invalid config value (${key}):`, value instanceof Array ? "[...]" : "{...}"); - return undefined; -} -function isValid(value) { - return ["string", "number", "boolean"].includes(typeof value); -} diff --git a/dist/main.d.ts b/dist/main.d.ts deleted file mode 100644 index 1013027..0000000 --- a/dist/main.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { Sequelize } from "sequelize-typescript"; -export declare const sql: Sequelize; diff --git a/dist/main.js b/dist/main.js deleted file mode 100644 index 8215a3e..0000000 --- a/dist/main.js +++ /dev/null @@ -1,40 +0,0 @@ -import Mail from "./models/Mail.js"; -import POP3Server from "./pop3/POP3Server.js"; -import SMTPServer from "./smtp/SMTPServer.js"; -import { Sequelize } from "sequelize-typescript"; -import User from "./models/User.js"; -import getConfig from "./config.js"; -import { readFile } from "node:fs/promises"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -global.debug = getConfig("debug", false); -export const sql = new Sequelize({ - database: getConfig("db.database"), - dialect: getConfig("db.dialect"), - username: getConfig("db.username"), - password: getConfig("db.password"), - models: [User, Mail] -}); -// await sql.sync({ alter: true }) -// await User.create({ -// name: "Cfp", -// username: "cfp", -// password: "1234" -// }) -secure: if (getConfig("pop3s.enabled", false) || getConfig("smtps.enabled", false)) { - let tlsCert, tlsKey; - try { - tlsKey = await readFile(getConfig("tls.key", "cert/privkey.pem")); - tlsCert = await readFile(getConfig("tls.cert", "cert/fullchain.pem")); - } - catch (ignore) { - break secure; - } - if (getConfig("pop3s.enabled", false)) - new POP3Server(getConfig("pop3s.port", 995), true, tlsKey, tlsCert); // Port 110 for regular POP3, 995 for POP3S - if (getConfig("smtps.enabled", false)) - new SMTPServer(getConfig("smtps.port", 465), true, tlsKey, tlsCert); // Port 25 for regular SMTP, 465 for SMTPS -} -if (getConfig("smtp.enabled", true)) - new SMTPServer(getConfig("smtp.port", 25), false); // Port 25 for regular SMTP, 465 for SMTPS -if (getConfig("pop3.enabled", true)) - new POP3Server(getConfig("pop3.port", 110), false); // Port 110 for regular POP3, 995 for POP3S diff --git a/dist/models/Mail.d.ts b/dist/models/Mail.d.ts deleted file mode 100644 index 8fa2e3a..0000000 --- a/dist/models/Mail.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Model } from "sequelize-typescript"; -import User from "./User.js"; -export default class Mail extends Model { - uuid: string; - from: string; - to: string; - content: string; - userUuid: string; - user: User; -} diff --git a/dist/models/Mail.js b/dist/models/Mail.js deleted file mode 100644 index 6c3f2f7..0000000 --- a/dist/models/Mail.js +++ /dev/null @@ -1,45 +0,0 @@ -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -import { DataTypes } from "sequelize"; -import { AllowNull, BelongsTo, Column, ForeignKey, Model, PrimaryKey, Table, Unique } from "sequelize-typescript"; -import User from "./User.js"; -let Mail = class Mail extends Model { -}; -__decorate([ - AllowNull(false), - Unique, - PrimaryKey, - Column({ - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4 - }) -], Mail.prototype, "uuid", void 0); -__decorate([ - AllowNull(false), - Column(DataTypes.STRING) -], Mail.prototype, "from", void 0); -__decorate([ - AllowNull(false), - Column(DataTypes.STRING) -], Mail.prototype, "to", void 0); -__decorate([ - AllowNull(false), - Column(DataTypes.STRING) -], Mail.prototype, "content", void 0); -__decorate([ - ForeignKey(() => User) -], Mail.prototype, "userUuid", void 0); -__decorate([ - BelongsTo(() => User) -], Mail.prototype, "user", void 0); -Mail = __decorate([ - Table({ - paranoid: true, - tableName: "mails" - }) -], Mail); -export default Mail; diff --git a/dist/models/User.d.ts b/dist/models/User.d.ts deleted file mode 100644 index df5f7d8..0000000 --- a/dist/models/User.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Model } from "sequelize-typescript"; -import Mail from "./Mail.js"; -export default class User extends Model { - name: string; - username: string; - password: string; - mails: Mail[]; - getMail(id: number): Promise; -} diff --git a/dist/models/User.js b/dist/models/User.js deleted file mode 100644 index b35e5a7..0000000 --- a/dist/models/User.js +++ /dev/null @@ -1,50 +0,0 @@ -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -import { DataTypes } from "sequelize"; -import { AllowNull, Column, HasMany, Model, PrimaryKey, Table, Unique } from "sequelize-typescript"; -import Mail from "./Mail.js"; -import { createHash } from "node:crypto"; -let User = class User extends Model { - async getMail(id) { - const mails = await this.$get("mails"); - if (!mails) - return undefined; - return mails[id]; - } -}; -__decorate([ - AllowNull(false), - Unique, - Column(DataTypes.STRING) -], User.prototype, "name", void 0); -__decorate([ - AllowNull(false), - Unique, - PrimaryKey, - Column(DataTypes.STRING) -], User.prototype, "username", void 0); -__decorate([ - AllowNull(false), - Column({ - type: DataTypes.STRING, - set(value) { - const hash = createHash("sha256"); - hash.update(String(value)); - this.setDataValue("password", hash.digest("hex")); - } - }) -], User.prototype, "password", void 0); -__decorate([ - HasMany(() => Mail) -], User.prototype, "mails", void 0); -User = __decorate([ - Table({ - paranoid: true, - tableName: "users" - }) -], User); -export default User; diff --git a/dist/pop3/POP3Server.d.ts b/dist/pop3/POP3Server.d.ts deleted file mode 100644 index 7df2eaa..0000000 --- a/dist/pop3/POP3Server.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/// -/// -import net from "net"; -export default class POP3Server { - server: net.Server; - useTLS: boolean; - constructor(port: number, useTLS: boolean, key?: Buffer, cert?: Buffer); - connection(sock: net.Socket): void; -} diff --git a/dist/pop3/POP3Server.js b/dist/pop3/POP3Server.js deleted file mode 100644 index 682793c..0000000 --- a/dist/pop3/POP3Server.js +++ /dev/null @@ -1,135 +0,0 @@ -import net from "net"; -import tls from "tls"; -import User from "../models/User.js"; -import { createHash } from "node:crypto"; -import Logger from "../Logger.js"; -import getConfig from "../config.js"; -import { readFile } from "fs/promises"; -const logger = new Logger("POP3", "YELLOW"); -export default class POP3Server { - server; - useTLS; - constructor(port, useTLS, key, cert) { - this.useTLS = useTLS; - if (useTLS && (!key || !cert)) - throw new Error("TLS key or certificate not provided"); - this.server = useTLS ? tls.createServer({ - key, - cert - }, this.connection) : net.createServer(); - this.server.listen(port, () => { - logger.log(`Server listening on port ${port}`); - }); - if (!useTLS) - this.server.on("connection", this.connection); - } - connection(sock) { - logger.log("Client connected"); - sock.write("+OK POP3 server ready\r\n"); - let username = ""; - let user; - const markedForDeletion = []; - sock.on("data", async (data) => { - const msg = data.toString(); - logger.log(`Received data: ${msg}`); - const args = msg.split(" ").slice(1); - if (msg.startsWith("CAPA")) { // list capabilities - sock.write("+OK Capability list follows\r\nUSER\r\n.\r\n"); - } - else if (msg.startsWith("USER")) { // client gives username - if (args.length < 1) - return void sock.write("-ERR Invalid username or password\r\n"); - username = args[0].trim().toLowerCase(); - if (username.includes("@")) { - if (!username.endsWith(`@${getConfig("host", "localhost")}`)) - return void sock.write("-ERR Invalid username or password\r\n"); - username = username.substring(0, username.lastIndexOf("@")); - } - if (username.startsWith("\"") && username.endsWith("\"")) - username = username.substring(1, username.length - 1); - else if (username.includes("@")) { - // this is not allowed - return void sock.write("-ERR Invalid username or password\r\n"); - } - const _user = await User.findOne({ where: { username } }); - if (!_user) - return void sock.write("-ERR Invalid username or password\r\n"); - user = _user; - sock.write("+OK\r\n"); - } - else if (msg.startsWith("PASS")) { // client gives password - if (args.length < 1) - return void sock.write("-ERR Invalid username or password\r\n"); - const password = args[0].trim(); - const hash = createHash("sha256"); - hash.update(password); - const hashedPassword = hash.digest("hex"); - if (user.password == hashedPassword) - sock.write("+OK Logged in\r\n"); - else - sock.write("-ERR Invalid username or password\r\n"); - } - else if (msg.startsWith("STAT")) { // get number of messages and total size - const mails = await user.$count("mails"); - sock.write(`+OK ${mails}\r\n`); - } - else if (msg.startsWith("QUIT")) { - sock.write("+OK Bye\r\n"); - sock.end(); - } - else if (msg.startsWith("LIST")) { - const mails = await user.$get("mails"); - sock.write(`+OK ${mails.length} messages\r\n`); - for (let i = 0; i < mails.length; i++) - sock.write(`${i}\r\n`); - sock.write(".\r\n"); - } - else if (msg.startsWith("RETR")) { - if (args.length < 1) - return void sock.write("-ERR No message specified\r\n"); - const mail = await user.getMail(parseInt(args[0].trim())); - if (!mail) - return void sock.write("-ERR No such message\r\n"); - const content = await readFile(`mails/${mail.content}.txt`, "utf-8"); - sock.write(`+OK\r\n${content}\r\n.\r\n`); - } - else if (msg.startsWith("TOP")) { - if (args.length < 2) - return void sock.write("-ERR No message specified\r\n"); - const mail = await user.getMail(parseInt(args[0].trim())); - if (!mail) - return void sock.write("-ERR No such message\r\n"); - const content = await readFile(`mails/${mail.content}.txt`, "utf-8"); - const lines = content.split("\r\n"); - const top = lines.slice(0, 10).join("\r\n"); - sock.write(`+OK\r\n${top}\r\n.\r\n`); - } - else if (msg.startsWith("UIDL")) { // get unique id of message - const mails = await user.$get("mails"); - for (let i = 0; i < mails.length; i++) - sock.write(`${i} ${mails[i].uuid}\r\n`); - sock.write(".\r\n"); - } - else if (msg.startsWith("DELE")) { - if (args.length < 1) - return void sock.write("-ERR No message specified\r\n"); - const mail = await user.getMail(parseInt(args[0].trim())); - if (!mail) - return void sock.write("-ERR No such message\r\n"); - await mail.destroy(); - sock.write("+OK\r\n"); - } - else if (msg.startsWith("NOOP")) { // this is used to keep the connection alive - sock.write("+OK\r\n"); - } - else if (msg.startsWith("RSET")) { - for (const mail of markedForDeletion) - await mail.restore(); - sock.write("+OK\r\n"); - } - }); - sock.addListener("close", () => { - logger.log("Client disconnected"); - }); - } -} diff --git a/dist/smtp/SMTP.d.ts b/dist/smtp/SMTP.d.ts deleted file mode 100644 index 1e98fcb..0000000 --- a/dist/smtp/SMTP.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default class SMTP { - static handleNewMail(info: { - from: string; - to: string[]; - content: string; - }): Promise; -} diff --git a/dist/smtp/SMTP.js b/dist/smtp/SMTP.js deleted file mode 100644 index 08c0a6f..0000000 --- a/dist/smtp/SMTP.js +++ /dev/null @@ -1,43 +0,0 @@ -import { mkdir, writeFile } from "fs/promises"; -import getConfig from "../config.js"; -import Logger from "../Logger.js"; -import User from "../models/User.js"; -import crypto from "node:crypto"; -const logger = new Logger("SMTP", "GREEN"); -export default class SMTP { - static async handleNewMail(info) { - const id = crypto.randomUUID(); - await mkdir(`mails/`, { recursive: true }); - await writeFile(`mails/${id}.txt`, info.content); - logger.log(`Saved mail to mails/${id}.txt`); - const serverName = getConfig("host"); - if (info.from.endsWith(`@${serverName}`)) { - logger.log("Mail is from this server."); - if (!info.to.every(email => email.endsWith(`@${serverName}`))) { // if not all recipients are on this server - logger.log("Not all recipients are on this server. Will forward mail to other servers."); - logger.error("Forwarding mails to other servers is not implemented yet."); - // TODO: forward mail to other servers using SMTPClient - return; - } - logger.log("All recipients are on this server."); - } - else if (!info.to.every(email => email.endsWith(`@${serverName}`))) - logger.warn("Not all recipients are from this server. Will NOT forward mail to other servers."); - const recipients = info.to.filter(email => email.endsWith(`@${serverName}`)); - for (const rec of recipients) { - logger.log(`Forwarding mail to ${rec}`); - const user = await User.findOne({ where: { username: rec.substring(0, rec.lastIndexOf("@")) } }); - if (!user) { - logger.error(`User ${rec} does not exist.`); - // Since we verify the recipients at the RCPT TO command, we should never get here, but you never know - continue; - } - await user.$create("mail", { - from: info.from, - to: rec, - content: id - }); - logger.log(`Forwarded mail to ${rec}`); - } - } -} diff --git a/dist/smtp/SMTPClient.d.ts b/dist/smtp/SMTPClient.d.ts deleted file mode 100644 index a835bfd..0000000 --- a/dist/smtp/SMTPClient.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default class SMTPClient { - static sendMessage(host: string, port: number, from: string, to: string, content: string, useTLS: boolean): Promise; -} diff --git a/dist/smtp/SMTPClient.js b/dist/smtp/SMTPClient.js deleted file mode 100644 index 700636d..0000000 --- a/dist/smtp/SMTPClient.js +++ /dev/null @@ -1,23 +0,0 @@ -import net from "net"; -import tls from "tls"; -import getConfig from "../config.js"; -import Logger from "../Logger.js"; -const logger = new Logger("SMTPClient", "TEAL"); -export default class SMTPClient { - static async sendMessage(host, port, from, to, content, useTLS) { - // const sock = net.createConnection(port, host) - const sock = useTLS ? tls.connect(port, host) : net.createConnection(port, host); - sock.on("data", (data) => { - logger.log(`Received data: ${data.toString()}`); - }); - sock.on("connect", async () => { - logger.log("Connected to server"); - sock.write(`EHLO ${getConfig("host", "localhost")}\r\n`); - sock.write(`MAIL FROM:<${from}>\r\n`); - sock.write(`RCPT TO:<${to}>\r\n`); - sock.write("DATA\r\n"); - sock.write(`${content}\r\n.\r\n`); - sock.write("QUIT\r\n"); - }); - } -} diff --git a/dist/smtp/SMTPServer.d.ts b/dist/smtp/SMTPServer.d.ts deleted file mode 100644 index 2b2e241..0000000 --- a/dist/smtp/SMTPServer.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/// -/// -import net from "net"; -export default class SMTPServer { - server: net.Server; - useTLS: boolean; - constructor(port: number, useTLS: boolean, key?: Buffer, cert?: Buffer); - connection(sock: net.Socket): void; -} diff --git a/dist/smtp/SMTPServer.js b/dist/smtp/SMTPServer.js deleted file mode 100644 index d30bbcf..0000000 --- a/dist/smtp/SMTPServer.js +++ /dev/null @@ -1,121 +0,0 @@ -import net from "net"; -import tls from "tls"; -import getConfig from "../config.js"; -import User from "../models/User.js"; -import sendStatus from "./status.js"; -import Logger from "../Logger.js"; -import SMTP from "./SMTP.js"; -const logger = new Logger("SMTPServer", "GREEN"); -export default class SMTPServer { - server; - useTLS; - constructor(port, useTLS, key, cert) { - this.useTLS = useTLS; - if (useTLS && (!key || !cert)) - throw new Error("TLS key or certificate not provided"); - this.server = useTLS ? tls.createServer({ - key, - cert - }, this.connection) : net.createServer(); - this.server.listen(port, () => { - logger.log(`Server listening on port ${port}`); - }); - if (!useTLS) - this.server.on("connection", this.connection); - } - connection(sock) { - const status = sendStatus(sock); - logger.log("Client connected"); - status(220, { message: getConfig("smtp.header", "SMTP Server ready") }); - let receivingData = false; - let info = { - from: "", - to: [], - content: "" - }; - sock.on("data", async (data) => { - const msg = data.toString(); - // TODO implement regular HELO greeting - if (receivingData) { - logger.log(`Received message content: ${msg}`); - info.content += msg; - if (msg.endsWith(".\r\n")) { - receivingData = false; - info.content = info.content.substring(0, info.content.length - 3).replaceAll("\r\n", "\n"); - await SMTP.handleNewMail(info); - status(250); - logger.log("No longer receiving data -----------------------------------"); - return; - } - return; - } - logger.log(`Received data: ${msg}`); - if (msg.startsWith("EHLO")) { - sock.write(`250-${getConfig("host", "localhost")}\r\n`); - // We dont have any smtp extensions yet - status(250, { message: "HELP" }); // was: 250 HELP - } - else if (msg.startsWith("MAIL FROM:")) { - // The spec says we should reset the state if the client sends MAIL FROM again - info = { - from: "", - to: [], - content: "" - }; - const email = msg.split(":")[1].split(">")[0].replace("<", ""); - logger.log(`MAIL FROM: ${email}`); - info.from = email; - status(250); - } - else if (msg.startsWith("RCPT TO:")) { - if (info.from == "") { - // The spec says we should return 503 if the client has not sent MAIL FROM yet - status(503); - return; - } - const email = msg.split(":")[1].split(">")[0].replace("<", ""); - const username = email.split("@")[0]; - const domain = email.split("@")[1]; - if (domain != getConfig("host")) { - // The spec says we MAY forward the message ourselves, but simply returning 550 is fine, and the client should handle it - status(550); - return; - } - const user = await User.findOne({ where: { username } }); - if (!user) { - status(550); - return; - } - info.to.push(email); - logger.log(`RCPT TO: ${email}`); - status(250); - } - else if (msg.startsWith("DATA")) { - // The spec says we should return either 503 or 554 if the client has not sent MAIL FROM or RCPT TO yet - // We will send 554 because it is more specific - if (info.from == "" || info.to.length == 0) { - status(554, { message: "No valid recipients" }); - return; - } - receivingData = true; - logger.log("Now receiving data -----------------------------------"); - status(354); - } - else if (msg.startsWith("QUIT")) { - status(221, "2.0.0"); - sock.end(); - } - else if (msg.startsWith("VRFY")) { - // This command is used to verify if a user exists, but that can be a security risk + it is also done with RCPT TO anyway - status(502); - } - else if (msg.startsWith("EXPN")) - status(502); - else - status(502); - }); - sock.on("close", () => { - logger.log("Client disconnected"); - }); - } -} diff --git a/dist/smtp/status.d.ts b/dist/smtp/status.d.ts deleted file mode 100644 index 3575dfc..0000000 --- a/dist/smtp/status.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/// -import net from "net"; -export type EnhancedStatusSubject = { - code: number; - name: string; -}; -export interface StatusCode { - code: number; - message?: string; - ok: boolean; -} -export interface EnhancedStatusCode extends StatusCode { - class: number; - subject: number; - detail: number; -} -type EnhancedCode = `${bigint}.${bigint}.${bigint}`; -type StatusOptions = { - message?: string; - enhancedCode?: EnhancedCode; - args?: string[]; -}; -export default function sendStatus(socket: net.Socket): (code: number, options?: StatusOptions | EnhancedCode) => void; -export declare function status(code: number, options?: StatusOptions | EnhancedCode): string; -export {}; diff --git a/dist/smtp/status.js b/dist/smtp/status.js deleted file mode 100644 index 2c00270..0000000 --- a/dist/smtp/status.js +++ /dev/null @@ -1,99 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const STATUS_SUBJECTS = [ - { code: 0, name: "Other or Undefined" }, - { code: 1, name: "Addressing" }, - { code: 2, name: "Mailbox" }, - { code: 3, name: "Mail System" }, - { code: 4, name: "Network and Routing" }, - { code: 5, name: "Mail Delivery" }, - { code: 6, name: "Message Content or Media" }, - { code: 7, name: "Security or Policy" } -]; -const ok = true; -const STATUS_CODES = { - // OK - 211: { code: 211, ok }, - 214: { code: 214, ok }, - 220: { code: 220, ok, message: "% Service ready" }, - 221: { code: 221, ok, message: "% Service closing transmission channel" }, - 240: { code: 240, ok, message: "QUIT" }, - 250: { code: 250, ok, message: "OK" }, - 251: { code: 251, ok, message: "User not local; will forward" }, - 252: { code: 252, ok }, - // intermediate OK - 334: { code: 334, ok }, - 354: { code: 354, ok, message: "Start mail input; end with ." }, - // transient NOT OK - 421: { code: 421, ok: false, message: "Service not available, closing transmission channel" }, - 450: { code: 450, ok: false, message: "Requested mail action not taken: mailbox unavailable" }, - 451: { code: 451, ok: false, message: "Requested action aborted: local error in processing" }, - 452: { code: 452, ok: false, message: "Requested action not taken: insufficient system storage" }, - 455: { code: 455, ok: false, message: "Server unable to accommodate parameters" }, - // permanent NOT OK - 500: { code: 500, ok: false, message: "Syntax error, command unrecognized" }, - 501: { code: 501, ok: false, message: "Syntax error in parameters or arguments" }, - 502: { code: 502, ok: false, message: "Command not implemented" }, - 503: { code: 503, ok: false, message: "Bad sequence of commands" }, - 504: { code: 504, ok: false, message: "Command parameter is not implemented" }, - 521: { code: 521, ok: false, message: "Server does not accept mail" }, - 523: { code: 523, ok: false, message: "Encryption Needed" }, - 550: { code: 550, ok: false, message: "Requested action not taken: mailbox unavailable" }, - 551: { code: 551, ok: false, message: "User not local; please try %" }, - 552: { code: 552, ok: false, message: "Requested mail action aborted: exceeded storage allocation" }, - 553: { code: 553, ok: false, message: "Requested action not taken: mailbox name not allowed" }, - 554: { code: 554, ok: false, message: "Transaction has failed" }, - 556: { code: 556, ok: false, message: "Domain does not accept mail" } -}; -const ENHANCED_STATUS_CODES = { - "221 2.0.0": { code: 221, ok, message: "Goodbye", class: 2, subject: 0, detail: 0 }, - "235 2.7.0": { code: 235, ok, message: "Authentication succeeded", class: 2, subject: 7, detail: 0 }, - "432 4.7.12": { code: 432, ok: false, message: "A password transition is needed", class: 4, subject: 7, detail: 12 }, - "451 4.4.1": { code: 451, ok: false, message: "IMAP server unavailable", class: 4, subject: 4, detail: 1 }, - "454 4.7.0": { code: 454, ok: false, message: "Temporary authentication failure", class: 4, subject: 7, detail: 0 }, - "500 5.5.6": { code: 500, ok: false, message: "Authentication Exchange line is too long", class: 5, subject: 5, detail: 6 }, - "501 5.5.2": { code: 501, ok: false, message: "Cannot Base64-decode Client responses", class: 5, subject: 5, detail: 2 }, - "501 5.7.0": { code: 501, ok: false, message: "Client initiated Authentication Exchange", class: 5, subject: 7, detail: 0 }, - "504 5.5.4": { code: 504, ok: false, message: "Unrecognized authentication type", class: 5, subject: 5, detail: 4 }, - "530 5.7.0": { code: 530, ok: false, message: "Authentication required", class: 5, subject: 7, detail: 0 }, - "534 5.7.9": { code: 534, ok: false, message: "Authentication mechanism is too weak", class: 5, subject: 7, detail: 9 }, - "535 5.7.8": { code: 534, ok: false, message: "Authentication credentials invalid", class: 5, subject: 7, detail: 8 }, - "538 5.7.11": { code: 538, ok: false, message: "Encryption required for requested authentication mechanism", class: 5, subject: 7, detail: 11 }, - "554 5.3.4": { code: 554, ok: false, message: "Message too big for system", class: 5, subject: 7, detail: 8 } -}; -export default function sendStatus(socket) { - return (code, options) => { - socket.write(status(code, options)); - }; -} -export function status(code, options) { - if (typeof options == "string") - options = { enhancedCode: options }; - let statusCode = STATUS_CODES[code]; - let enhanced = false; - if (options && options.enhancedCode) { - if (`${code} ${options.enhancedCode}` in ENHANCED_STATUS_CODES) { - statusCode = ENHANCED_STATUS_CODES[`${code} ${options.enhancedCode}`]; - enhanced = true; - } - } - if (!(code in STATUS_CODES) && !enhanced) - return `${code} ${options?.message}\r\n`; - let message; - if (statusCode.message?.includes("%") && options?.args) - message = replaceArgs(statusCode, options.args, options?.message); - else - message = options?.message || statusCode.message || ""; - if (enhanced) { - const esc = statusCode; - return `${esc.code} ${esc.class}.${esc.subject}.${esc.detail} ${message}\r\n`; - } - return `${statusCode.code} ${message}\r\n`; -} -function replaceArgs(statusCode, args, msg) { - if (!("message" in statusCode)) - return msg || ""; - const message = statusCode.message; - for (let i = 0; message.includes("%"); i++) - message.replace("%", args[i] ?? msg); - return message; -} diff --git a/dist/test/main.d.ts b/dist/test/main.d.ts deleted file mode 100644 index cb0ff5c..0000000 --- a/dist/test/main.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/dist/test/main.js b/dist/test/main.js deleted file mode 100644 index 7d6b5b9..0000000 --- a/dist/test/main.js +++ /dev/null @@ -1,34 +0,0 @@ -import Logger from "../Logger.js"; -import { status } from "../smtp/status.js"; -const ESC = "\u001b"; -const RED = `${ESC}[91m`; -const YELLOW = `${ESC}[93m`; -const BLUE = `${ESC}[94m`; -const GREEN = `${ESC}[92m`; -let errors = 0; -const logger = new Logger("test", "TEAL"); -logger.debug("debug log"); -logger.log("info log"); -logger.warn("warning log"); -logger.error("error log"); -const smtpStatusTestCases = [ - [451, "451 Requested action aborted: local error in processing\r\n"], - [521, "521 Server does not accept mail\r\n"], - [550, "550 Requested action not taken: mailbox unavailable\r\n"], - ["530 5.7.0", "530 5.7.0 Authentication required\r\n"], - ["538 5.7.11", "538 5.7.11 Encryption required for requested authentication mechanism\r\n"] -]; -for (const e of smtpStatusTestCases) { - const args = (`${e[0]}`).split(" "); - const statusMsg = status(Number(args[0]), args[1]); - logger.log(`SMTP ${e[0]}: [${statusMsg.substring(0, statusMsg.length - 2)}]`); - if (statusMsg != e[1]) { - errors++; - logger.error(`SMTP ${e[0]} does not match test case`); - } -} -const totalCases = smtpStatusTestCases.length; -const errorColor = errors ? RED : GREEN; -logger.log(`${BLUE}------------------------------------------------`); -logger.log(`${BLUE}Test cases completed: ${errorColor}${errors}${BLUE} out of ${YELLOW}${totalCases}${BLUE} failed`); -process.exit(errors ? 1 : 0);