class IMAPController { constructor (socket, tag) { this.socket = socket; this.tag = tag; } out (answer) { console.debug("IMAP response:", answer); this.socket.write(`${this.tag} ${answer}`); } untaggedOut (answer) { console.debug("IMAP response:", answer); this.socket.write(answer); } close () { this.socket.end(); } } const messages = [ { id: 1, uid: 101, flags: ["\\Seen"], raw: `From: Example \r\nTo: You \r\nSubject: Hello\r\n\r\nThis is a test email.` } ]; const parseUidRange = (input) => { if (input === '*') return messages.map(msg => msg.uid); if (input.includes(':')) { const [start, end] = input.split(':').map(Number); return messages .map(msg => msg.uid) .filter(uid => uid >= start && uid <= (isNaN(end) ? Infinity : end)); } return [Number(input)]; }; class Commands { constructor (client) { this.client = client; } // Methods // Required commands async capability (args, controller) { /*if (args.length > 0) return out();*/ controller.untaggedOut("* CAPABILITY IMAP4rev1 IDLE\r\n"); controller.out("OK CAPABILITY completed\r\n"); } async logout (args, controller) { controller.untaggedOut("* BYE Logging out\r\n"); controller.out("OK LOGOUT completed\r\n"); controller.close(); } async noop(args, controller) { controller.out("OK NOOP completed\r\n"); } async login(args, controller) { //this.client.state = "authenticated"; controller.out("OK LOGIN completed\r\n"); } // Mailposts async list(args, controller) { controller.untaggedOut(`* LIST (\\HasNoChildren) "." "INBOX"\r\n`); controller.out("OK LIST completed\r\n"); } async select(args, controller) { const messageCount = messages.length; controller.untaggedOut("* FLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft)\r\n"); controller.untaggedOut(`* ${messageCount} EXISTS\r\n`); controller.untaggedOut(`* ${messageCount} RECENT\r\n`); controller.untaggedOut("* OK [UNSEEN 1] First unseen\r\n"); controller.untaggedOut("* OK [UIDVALIDITY 1] UIDs valid\r\n"); controller.untaggedOut(`* OK [UIDNEXT ${messageCount + 1}] Predicted next UID\r\n`); controller.out("OK [READ-WRITE] SELECT completed\r\n"); //this.client.state = "selected"; } // Mailpost's messages async fetch(args, controller) { if (!args || args.length < 2) { return controller.out("BAD FETCH requires message sequence and data item\r\n"); } const seq = args[0]; const item = args.slice(1).join(" ").toUpperCase(); if (seq !== "1") { return controller.out("NO FETCH only supports message 1 for now\r\n"); } if (!item.includes("BODY[]")) { return controller.out("NO FETCH only supports BODY[] for now\r\n"); } const msg = messages.find(m => m.id === 1); if (!msg) { return controller.out("NO Message not found\r\n"); } const body = msg.raw; controller.untaggedOut(`* 1 FETCH (BODY[] {${body.length}}\r\n${body}\r\n)\r\n`); controller.out("OK FETCH completed\r\n"); } async uid(args, controller) { if (!args || args.length < 2) { return controller.out("BAD UID requires subcommand and arguments\r\n"); } const subCommand = args[0].toUpperCase(); if (subCommand === "FETCH") { const uidArg = args[1]; const fetchFields = args.slice(2).join(" ").toUpperCase(); const msg = messages.find(m => m.uid === Number(uidArg)); if (!msg) { return controller.out("NO No such message\r\n"); } const headersOnly = msg.raw.split("\r\n\r\n")[0] + "\r\n"; const headerLength = headersOnly.length; const bodySize = msg.raw.length; const flags = msg.flags.join(" "); controller.untaggedOut( `* ${msg.id} FETCH (` + `UID ${msg.uid} ` + `RFC822.SIZE ${bodySize} ` + `FLAGS (${flags}) ` + `BODY[HEADER.FIELDS (From To Subject Date Message-ID)] {${headerLength}}\r\n` + headersOnly + ")\r\n" ); return controller.out("OK UID FETCH completed\r\n"); } controller.out("BAD UID subcommand not supported\r\n"); } async store(args, controller) { controller.out("OK STORE not yet implemented\r\n"); } async expunge(args, controller) { controller.out("OK EXPUNGE not yet implemented\r\n"); } async copy(args, controller) { controller.out("OK COPY not yet implemented\r\n"); } async close(args, controller) { controller.out("OK CLOSE not yet implemented\r\n"); } async search(args, controller) { controller.out("* SEARCH\r\n"); controller.out("OK SEARCH completed\r\n"); } async examine(args, controller) { controller.out("OK EXAMINE not yet implemented\r\n"); } async append(args, controller) { controller.out("OK APPEND not yet implemented\r\n"); } // Push-notifications (IDLE) async idle(args, controller) { controller.untaggedOut("+ idling\r\n"); controller.out("OK IDLE not yet implemented\r\n"); } } class Client { constructor (socket) { this.socket = socket; this.commands = new Commands(this); } command (tag, command, args) { //console.debug("this >>", this, this.socket, this.commands); const cmd = this.commands[command.toLowerCase()]; if (cmd === undefined) { return this.socket.write(`${tag} BAD unknown command\r\n`); } cmd.call(this.commands, args, new IMAPController(this.socket, tag)); } } module.exports = { Client };