diff --git a/dist/imap/IMAPServer.d.ts b/dist/imap/IMAPServer.d.ts new file mode 100644 index 0000000..c0406c0 --- /dev/null +++ b/dist/imap/IMAPServer.d.ts @@ -0,0 +1,7 @@ +/// +import net from "net"; +export default class IMAPServer { + server: net.Server; + constructor(port: number); + connection(sock: net.Socket): void; +} diff --git a/dist/imap/IMAPServer.js b/dist/imap/IMAPServer.js new file mode 100644 index 0000000..ba587f2 --- /dev/null +++ b/dist/imap/IMAPServer.js @@ -0,0 +1,109 @@ +import net from "net"; +import { createHash } from "node:crypto"; +import User from "../models/User.js"; +import Logger from "../Logger.js"; +import getConfig from "../config.js"; +const logger = new Logger("IMAP", "TEAL"); +export default class IMAPServer { + server; + constructor(port) { + this.server = net.createServer(); + this.server.listen(port, () => { + logger.log("Server listening on port " + port); + }); + this.server.on("connection", this.connection); + } + connection(sock) { + logger.log("Client connected"); + sock.write("* OK IMAP4rev1 Service Ready\r\n"); + let user; + sock.on("data", async (data) => { + const msg = data.toString().trim(); + logger.log("Received data: " + msg); + const tag = msg.split(" ")[0]; + const rest = msg.split(" ").slice(1).join(" "); + const cmd = rest.split(" ")[0].toLowerCase(); + const args = (rest.match(/(".*?"|[^"\s]+)+(?=\s*|\s*$)/g) || []).filter(Boolean).splice(1); + logger.debug("Tag: " + tag); + logger.debug("Command: " + cmd); + logger.debug("Arguments: " + args.join(" ")); + if (cmd === "login") { + let username = args[0].trim().toLowerCase(); + if (username.includes("@")) { + if (!username.endsWith("@" + getConfig("host", "localhost"))) { + return void sock.write(tag + " NO 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("@")) { + return void sock.write(tag + " NO Invalid username or password\r\n"); + } + let _user = await User.findOne({ where: { username: username } }); + if (!_user) + return void sock.write(tag + " NO Invalid username or password\r\n"); + user = _user; + let password = args[1]; + const hash = createHash("sha256"); + hash.update(password); + const hashedPassword = hash.digest("hex"); + if (user.password != hashedPassword) { + return void sock.write(tag + " NO Invalid username or password\r\n"); + } + sock.write(tag + " OK Logged in\r\n"); + } + else if (cmd === "select") { + const mailbox = args[0]; + if (mailbox == "INBOX") { + sock.write("* 18 EXISTS\r\n"); + sock.write("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n"); + sock.write("* 2 RECENT\r\n"); + sock.write("* OK [UNSEEN 1] Message 1 is the first unseen message\r\n"); + sock.write("* OK [UIDVALIDITY 1] UIDs valid\r\n"); + sock.write(tag + " OK [READ-WRITE] SELECT completed\r\n"); + } + else { + sock.write(tag + " NO [NONEXISTENT] SELECT failed\r\n"); + } + } + else if (cmd === "fetch") { + // fetch 1 body[...] + const id = args[0]; // 1 + const body = args[1]; // body[...] + const mails = await user.$get("mails"); + const mail = mails[parseInt(id) - 1]; + if (!mail) + return void sock.write(tag + " NO [NONEXISTENT] FETCH failed\r\n"); + if (body == "body[]") { + sock.write("* 1 FETCH (BODY[] {" + mail.content.length + "}\r\n"); + sock.write(mail.content + ")\r\n"); + sock.write(tag + " OK FETCH completed\r\n"); + } + } + else if (cmd === "logout") { + sock.write("* BYE IMAP4rev1 Server logging out\r\n"); + sock.write(tag + " OK LOGOUT completed\r\n"); + sock.end(); + } + else if (cmd === "capability") { + sock.write("* CAPABILITY IMAP4rev1\r\n"); + sock.write(tag + " OK CAPABILITY completed\r\n"); + } + else if (cmd === "list") { + sock.write("* LIST (\\HasNoChildren) \"/\" \"INBOX\"\r\n"); + sock.write(tag + " OK LIST completed\r\n"); + } + else if (cmd === "noop") { + sock.write(tag + " OK NOOP completed\r\n"); + } + else if (cmd === "uid") { + sock.write(tag + " OK UID completed\r\n"); + } + }); + sock.addListener("close", () => { + logger.log("Client disconnected"); + }); + } +} diff --git a/dist/main.js b/dist/main.js index 5b1e807..b839b0f 100644 --- a/dist/main.js +++ b/dist/main.js @@ -5,6 +5,7 @@ import User from "./models/User.js"; import Mail from "./models/Mail.js"; import getConfig from "./config.js"; import { readFile } from "node:fs/promises"; +import IMAPServer from "./imap/IMAPServer.js"; export const sql = new Sequelize({ database: getConfig("db_database"), dialect: getConfig("db_dialect"), @@ -34,3 +35,4 @@ if (getConfig("enable_smtp", true)) new SMTPServer(getConfig("smtp_port", 25)); // Port 25 for regular SMTP, 465 for SMTPS if (getConfig("enable_pop3", true)) new POP3Server(getConfig("pop3_port", 110), false); // Port 110 for regular POP3, 995 for POP3S +new IMAPServer(143); diff --git a/dist/models/Mail.d.ts b/dist/models/Mail.d.ts index 8fa2e3a..a7c5a2d 100644 --- a/dist/models/Mail.d.ts +++ b/dist/models/Mail.d.ts @@ -5,6 +5,7 @@ export default class Mail extends Model { from: string; to: string; content: string; + seen: boolean; userUuid: string; user: User; } diff --git a/dist/models/Mail.js b/dist/models/Mail.js index 6c3f2f7..1795104 100644 --- a/dist/models/Mail.js +++ b/dist/models/Mail.js @@ -30,6 +30,13 @@ __decorate([ AllowNull(false), Column(DataTypes.STRING) ], Mail.prototype, "content", void 0); +__decorate([ + AllowNull(false), + Column({ + type: DataTypes.BOOLEAN, + defaultValue: false + }) +], Mail.prototype, "seen", void 0); __decorate([ ForeignKey(() => User) ], Mail.prototype, "userUuid", void 0); diff --git a/src/imap/IMAPServer.ts b/src/imap/IMAPServer.ts new file mode 100644 index 0000000..76d4c64 --- /dev/null +++ b/src/imap/IMAPServer.ts @@ -0,0 +1,106 @@ +import net from "net" +import { readdirSync, readFileSync } from "fs" +import { createHash } from "node:crypto" +import User from "../models/User.js" +import Logger from "../Logger.js" +import getConfig from "../config.js" + +const logger = new Logger("IMAP", "TEAL") + +export default class IMAPServer { + + server: net.Server + + constructor(port: number) { + this.server = net.createServer() + this.server.listen(port, () => { + logger.log("Server listening on port " + port) + }) + this.server.on("connection", this.connection) + } + + connection(sock: net.Socket) { + logger.log("Client connected") + sock.write("* OK IMAP4rev1 Service Ready\r\n") + let user: User; + sock.on("data", async (data: Buffer) => { + const msg = data.toString().trim() + logger.log("Received data: " + msg) + const tag = msg.split(" ")[0] + const rest = msg.split(" ").slice(1).join(" ") + const cmd = rest.split(" ")[0].toLowerCase() + const args = (rest.match(/(".*?"|[^"\s]+)+(?=\s*|\s*$)/g) || []).filter(Boolean).splice(1); + logger.debug("Tag: " + tag) + logger.debug("Command: " + cmd) + logger.debug("Arguments: " + args.join(" ")) + if(cmd === "login") { + let username = args[0].trim().toLowerCase() + if(username.includes("@")) { + if(!username.endsWith("@" + getConfig("host", "localhost"))) { + return void sock.write(tag + " NO 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("@")) { + return void sock.write(tag + " NO Invalid username or password\r\n") + } + let _user = await User.findOne({ where: { username: username } }) + if(!_user) return void sock.write(tag + " NO Invalid username or password\r\n") + user = _user + + let password = args[1] + const hash = createHash("sha256") + hash.update(password) + const hashedPassword = hash.digest("hex") + if(user.password != hashedPassword) { + return void sock.write(tag + " NO Invalid username or password\r\n") + } + sock.write(tag + " OK Logged in\r\n") + } else if(cmd === "select") { + const mailbox = args[0] + if(mailbox == "INBOX") { + sock.write("* 18 EXISTS\r\n"); + sock.write("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n"); + sock.write("* 2 RECENT\r\n"); + sock.write("* OK [UNSEEN 1] Message 1 is the first unseen message\r\n"); + sock.write("* OK [UIDVALIDITY 1] UIDs valid\r\n"); + sock.write(tag + " OK [READ-WRITE] SELECT completed\r\n"); + } else { + sock.write(tag + " NO [NONEXISTENT] SELECT failed\r\n") + } + } else if(cmd === "fetch") { + // fetch 1 body[...] + const id = args[0] // 1 + const body = args[1] // body[...] + const mails = await user.$get("mails") + const mail = mails[parseInt(id) - 1] + if(!mail) return void sock.write(tag + " NO [NONEXISTENT] FETCH failed\r\n") + if(body == "body[]") { + sock.write("* 1 FETCH (BODY[] {" + mail.content.length + "}\r\n") + sock.write(mail.content + ")\r\n") + sock.write(tag + " OK FETCH completed\r\n") + } + } else if(cmd === "logout") { + sock.write("* BYE IMAP4rev1 Server logging out\r\n") + sock.write(tag + " OK LOGOUT completed\r\n") + sock.end() + } else if(cmd === "capability") { + sock.write("* CAPABILITY IMAP4rev1\r\n") + sock.write(tag + " OK CAPABILITY completed\r\n") + } else if(cmd === "list") { + sock.write("* LIST (\\HasNoChildren) \"/\" \"INBOX\"\r\n") + sock.write(tag + " OK LIST completed\r\n") + } else if(cmd === "noop") { + sock.write(tag + " OK NOOP completed\r\n") + } else if(cmd === "uid") { + sock.write(tag + " OK UID completed\r\n") + } + }) + sock.addListener("close", () => { + logger.log("Client disconnected") + }) + } + +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 6e7b610..adf6787 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,7 @@ import Mail from "./models/Mail.js"; import getConfig from "./config.js"; import { existsSync } from "node:fs"; import { readFile } from "node:fs/promises"; +import IMAPServer from "./imap/IMAPServer.js"; export const sql = new Sequelize({ database: getConfig("db_database"), @@ -37,3 +38,4 @@ secure: if (getConfig("enable_pop3s", false) || getConfig("enable_smtps", false) if (getConfig("enable_smtp", true)) new SMTPServer(getConfig("smtp_port", 25)) // Port 25 for regular SMTP, 465 for SMTPS if (getConfig("enable_pop3", true)) new POP3Server(getConfig("pop3_port", 110), false) // Port 110 for regular POP3, 995 for POP3S +new IMAPServer(143); \ No newline at end of file diff --git a/src/models/Mail.ts b/src/models/Mail.ts index ddd9e86..b23f548 100644 --- a/src/models/Mail.ts +++ b/src/models/Mail.ts @@ -28,6 +28,13 @@ export default class Mail extends Model { @AllowNull(false) @Column(DataTypes.STRING) declare content: string + + @AllowNull(false) + @Column({ + type: DataTypes.BOOLEAN, + defaultValue: false + }) + declare seen: boolean @ForeignKey(() => User) declare userUuid: string;