diff --git a/imap/index.js b/imap/index.js index e41fce5..7bdf106 100644 --- a/imap/index.js +++ b/imap/index.js @@ -12,7 +12,7 @@ const server = net.createServer((socket) => { socket.on("data", (data) => { const line = data.toString().trim(); - console.debug("IMAP data:", line); + console.debug("IMAP request:", line); const splitted = line.split(/\s{1,}/g); if (splitted.length < 2) return socket.write(". BAD unknown command\r\n"); const [tag, command, ...args] = splitted; diff --git a/imap/logic.js b/imap/logic.js index 19bc80f..ce9e514 100644 --- a/imap/logic.js +++ b/imap/logic.js @@ -5,10 +5,12 @@ class IMAPController { } out (answer) { + console.debug("IMAP response:", answer); this.socket.write(`${this.tag} ${answer}`); } untaggedOut (answer) { + console.debug("IMAP response:", answer); this.socket.write(answer); } @@ -17,24 +19,177 @@ class IMAPController { } } +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 STARTTLS LOGINDISABLED\r\n"); + controller.untaggedOut("* CAPABILITY IMAP4rev1 IDLE\r\n"); controller.out("OK CAPABILITY completed\r\n"); } - async logout(args, controller) { + 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 { diff --git a/server.js b/server.js index 889debe..b28f787 100644 --- a/server.js +++ b/server.js @@ -51,6 +51,7 @@ class POP3 extends EventEmitter { } } +/* const pop3 = new POP3(); pop3.init().then(() => { let interval = setTimeout(async () => { @@ -61,6 +62,7 @@ pop3.init().then(() => { //console.log(str); }, 1); }); +*/ imap.listen( global.config["imap-config"].port ?? 143,