initial version - hackathon
This commit is contained in:
commit
56bc5b62a6
11 changed files with 1928 additions and 0 deletions
524
imapServer.ts
Normal file
524
imapServer.ts
Normal file
|
@ -0,0 +1,524 @@
|
|||
import type { Buffer } from "node:buffer";
|
||||
import * as net from "node:net";
|
||||
import type { Event, Filter } from "@nostr/tools";
|
||||
import { getPublicKey, nip19 } from "@nostr/tools";
|
||||
import type { NPub, NSec } from "@nostr/tools/nip19";
|
||||
import { type ImapCommandParameters, parseImapCommand } from "./imapParsing.ts";
|
||||
import { handleUidFetch } from "./handleUidFetch.ts";
|
||||
import {
|
||||
findEventIdForUid,
|
||||
getFlags,
|
||||
logMessage,
|
||||
markMessageAsFlagged,
|
||||
markMessageAsRead,
|
||||
sendSocketMessage,
|
||||
} from "./utils.ts";
|
||||
import { countingRelay, mailingLists, queryingRelay } from "./main.ts";
|
||||
|
||||
export interface ImapSession {
|
||||
npub: NPub | null;
|
||||
selectedMailingList: string | null;
|
||||
}
|
||||
|
||||
function handleLogin(
|
||||
session: ImapSession,
|
||||
socket: net.Socket,
|
||||
tag: string,
|
||||
npubString: ImapCommandParameters,
|
||||
nsecString: ImapCommandParameters,
|
||||
) {
|
||||
if (typeof npubString !== "string" || typeof nsecString !== "string") {
|
||||
sendSocketMessage(socket, `${tag} NO LOGIN failed\r\n`);
|
||||
return;
|
||||
}
|
||||
if (!nsecString.startsWith("nsec1")) {
|
||||
sendSocketMessage(socket, `${tag} NO LOGIN failed\r\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
const sk = nip19.decode<"nsec">(nsecString as NSec);
|
||||
if (!sk?.data) {
|
||||
sendSocketMessage(socket, `${tag} NO LOGIN failed\r\n`);
|
||||
return;
|
||||
}
|
||||
const pkFromSk = getPublicKey(sk.data);
|
||||
const npub = nip19.npubEncode(pkFromSk);
|
||||
|
||||
if (npubString === npub) {
|
||||
session.npub = npub;
|
||||
sendSocketMessage(socket, `${tag} OK LOGIN completed\r\n`);
|
||||
} else {
|
||||
sendSocketMessage(socket, `${tag} NO LOGIN failed\r\n`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSelect(
|
||||
session: ImapSession,
|
||||
socket: net.Socket,
|
||||
tag: string,
|
||||
mailbox: ImapCommandParameters,
|
||||
) {
|
||||
if (!session.npub) {
|
||||
sendSocketMessage(socket, `${tag} NO Not authenticated\r\n`);
|
||||
return;
|
||||
}
|
||||
const mailboxName = Array.isArray(mailbox)
|
||||
? mailbox[mailbox.length - 1]
|
||||
: mailbox;
|
||||
if (!mailboxName.startsWith("npub")) {
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`${tag} NO [READ-WRITE] SELECT failed, invalid mailbox\r\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const mailboxNpub = nip19.decode<"npub">(mailboxName as NPub);
|
||||
if (!mailboxNpub) {
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`${tag} NO [READ-WRITE] SELECT failed, invalid mailbox\r\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
session.selectedMailingList = mailboxNpub.data;
|
||||
|
||||
const threadsInMailBox = await countingRelay.count(
|
||||
[
|
||||
{
|
||||
kinds: [11, 1111],
|
||||
"#P": [session.selectedMailingList],
|
||||
},
|
||||
],
|
||||
{},
|
||||
);
|
||||
const recentThreads = await countingRelay.count(
|
||||
[
|
||||
{
|
||||
kinds: [11, 1111],
|
||||
"#P": [session.selectedMailingList],
|
||||
since: Math.floor((Date.now() - 1000 * 60 * 60 * 24) / 1000),
|
||||
},
|
||||
],
|
||||
{},
|
||||
);
|
||||
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
"* FLAGS (\\Flagged \\Seen \\Answered)\r\n",
|
||||
);
|
||||
sendSocketMessage(socket, `* ${threadsInMailBox} EXISTS\r\n`);
|
||||
sendSocketMessage(socket, `* ${recentThreads} RECENT\r\n`);
|
||||
sendSocketMessage(socket, `${tag} OK [READ-WRITE] SELECT completed\r\n`);
|
||||
}
|
||||
|
||||
function handleCreate(
|
||||
session: ImapSession,
|
||||
socket: net.Socket,
|
||||
tag: string,
|
||||
mailbox: ImapCommandParameters,
|
||||
) {
|
||||
if (!session.npub) {
|
||||
sendSocketMessage(socket, `${tag} NO Not authenticated\r\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
const mailboxName = Array.isArray(mailbox)
|
||||
? mailbox[mailbox.length - 1]
|
||||
: mailbox;
|
||||
|
||||
if (!mailboxName.startsWith("npub")) {
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`${tag} NO [READ-WRITE] CREATE failed, invalid mailbox\r\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const mailboxNpub = nip19.decode<"npub">(mailboxName as NPub);
|
||||
if (!mailboxNpub) {
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`${tag} NO [READ-WRITE] CREATE failed, invalid mailbox\r\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const mailingList = mailingLists.find(
|
||||
(list: string) => list === mailboxNpub.data,
|
||||
);
|
||||
if (mailingList) {
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`${tag} NO [READ-WRITE] CREATE failed, mailbox already exists\r\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
mailingLists.push(mailboxName);
|
||||
Deno.writeFileSync(
|
||||
"maling-lists.json",
|
||||
new TextEncoder().encode(JSON.stringify(mailingLists)),
|
||||
);
|
||||
|
||||
sendSocketMessage(socket, `${tag} OK [READ-WRITE] CREATE completed\r\n`);
|
||||
}
|
||||
|
||||
function handleLogout(socket: net.Socket, tag: string) {
|
||||
sendSocketMessage(socket, "* BYE IMAP server logging out\r\n");
|
||||
sendSocketMessage(socket, `${tag} OK LOGOUT completed\r\n`);
|
||||
socket.end();
|
||||
}
|
||||
|
||||
function handleList(
|
||||
session: ImapSession,
|
||||
socket: net.Socket,
|
||||
tag: string,
|
||||
_: ImapCommandParameters,
|
||||
filter: ImapCommandParameters,
|
||||
) {
|
||||
if (!session.npub) {
|
||||
sendSocketMessage(socket, `${tag} NO Not authenticated\r\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
let filterMailingList = Array.isArray(filter)
|
||||
? filter[filter.length - 1]
|
||||
: filter;
|
||||
|
||||
if (filterMailingList.includes('"')) {
|
||||
filterMailingList = filterMailingList.split('"')[1];
|
||||
}
|
||||
|
||||
if (filterMailingList.startsWith("npub")) {
|
||||
const mailboxNpub = nip19.decode<"npub">(filterMailingList as NPub);
|
||||
if (!mailboxNpub) {
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`${tag} NO [READ-WRITE] LIST failed, invalid mailbox\r\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const mailingList = mailingLists.find(
|
||||
(list: string) => list === filterMailingList,
|
||||
);
|
||||
if (!mailingList) {
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`${tag} NO [READ-WRITE] LIST failed, mailbox does not exist\r\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`* LIST (\\HasNoChildren) "" ${filterMailingList}\r\n`,
|
||||
);
|
||||
sendSocketMessage(socket, `${tag} OK LIST completed\r\n`);
|
||||
} else if (filterMailingList === "*") {
|
||||
for (const mailingList of mailingLists) {
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`* LIST (\\HasNoChildren) "" ${mailingList}\r\n`,
|
||||
);
|
||||
}
|
||||
sendSocketMessage(socket, `${tag} OK LIST completed\r\n`);
|
||||
}
|
||||
}
|
||||
|
||||
function handleLsub(
|
||||
session: ImapSession,
|
||||
socket: net.Socket,
|
||||
tag: string,
|
||||
reference: ImapCommandParameters,
|
||||
mailbox: ImapCommandParameters,
|
||||
) {
|
||||
if (!session.npub) {
|
||||
sendSocketMessage(socket, `${tag} NO Not authenticated\r\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
const referenceName = Array.isArray(reference)
|
||||
? reference[reference.length - 1]
|
||||
: reference;
|
||||
const mailboxName = Array.isArray(mailbox)
|
||||
? mailbox[mailbox.length - 1]
|
||||
: mailbox;
|
||||
|
||||
if (referenceName !== "") {
|
||||
sendSocketMessage(socket, `${tag} NO LSUB failed, invalid reference\r\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mailboxName.endsWith("*")) {
|
||||
sendSocketMessage(socket, `${tag} NO LSUB failed, invalid mailbox\r\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const mailingList of mailingLists) {
|
||||
sendSocketMessage(socket, `* LSUB () "" ${mailingList}\r\n`);
|
||||
}
|
||||
sendSocketMessage(socket, `${tag} OK LSUB completed\r\n`);
|
||||
}
|
||||
|
||||
function handleSubscribe(
|
||||
session: ImapSession,
|
||||
socket: net.Socket,
|
||||
tag: string,
|
||||
mailbox: ImapCommandParameters,
|
||||
) {
|
||||
if (!session.npub) {
|
||||
sendSocketMessage(socket, `${tag} NO Not authenticated\r\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
const mailboxName = Array.isArray(mailbox)
|
||||
? mailbox[mailbox.length - 1]
|
||||
: mailbox;
|
||||
|
||||
if (!mailboxName.startsWith("npub")) {
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`${tag} NO SUBSCRIBE failed, invalid mailbox\r\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const mailingList = mailingLists.find(
|
||||
(list: string) => list === mailboxName,
|
||||
);
|
||||
if (!mailingList) {
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`${tag} NO SUBSCRIBE failed, mailbox does not exist\r\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
sendSocketMessage(socket, `${tag} OK SUBSCRIBE completed\r\n`);
|
||||
}
|
||||
|
||||
export function fetchAllEventsMatchingQuery(query: Filter[]): Promise<Event[]> {
|
||||
return new Promise((resolve) => {
|
||||
const allEvents: Event[] = [];
|
||||
const allThreadsSubscription = queryingRelay.subscribe(
|
||||
query,
|
||||
{
|
||||
onevent: (event) => {
|
||||
allEvents.push(event);
|
||||
},
|
||||
oneose: () => {
|
||||
allThreadsSubscription.close();
|
||||
resolve(allEvents);
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleUidStore(
|
||||
session: ImapSession,
|
||||
socket: net.Socket,
|
||||
tag: string,
|
||||
uid: ImapCommandParameters,
|
||||
storeItem: ImapCommandParameters,
|
||||
value: ImapCommandParameters,
|
||||
) {
|
||||
if (!session.npub) {
|
||||
sendSocketMessage(socket, `${tag} NO Not authenticated\r\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof uid !== "string") {
|
||||
sendSocketMessage(socket, `${tag} BAD UID STORE failed, invalid uid\r\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof storeItem !== "string" ||
|
||||
storeItem.toLowerCase() !== "+flags" && storeItem.toLowerCase() !== "-flags"
|
||||
) {
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`${tag} BAD UID STORE failed, invalid store item\r\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(value)) {
|
||||
sendSocketMessage(socket, `${tag} BAD UID STORE failed, invalid value\r\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
const eventId = findEventIdForUid(Number.parseInt(uid));
|
||||
|
||||
for (const flag of value) {
|
||||
if (flag.toLowerCase() === "\\seen") {
|
||||
await markMessageAsRead(
|
||||
session,
|
||||
eventId,
|
||||
storeItem.toLowerCase() === "+flags",
|
||||
);
|
||||
}
|
||||
if (flag.toLowerCase() === "\\flagged") {
|
||||
await markMessageAsFlagged(
|
||||
session,
|
||||
eventId,
|
||||
storeItem.toLowerCase() === "+flags",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const flags = await getFlags(session, eventId);
|
||||
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`* 1 FETCH (FLAGS (${flags.join(" ")}) UID ${uid})\r\n`,
|
||||
);
|
||||
|
||||
sendSocketMessage(socket, `${tag} OK UID STORE completed\r\n`);
|
||||
}
|
||||
|
||||
export default function startImapServer(): void {
|
||||
const server = net.createServer((socket: net.Socket) => {
|
||||
const session: ImapSession = {
|
||||
npub: null,
|
||||
selectedMailingList: null,
|
||||
};
|
||||
|
||||
try {
|
||||
sendSocketMessage(socket, "* OK Nostr over IMAP ready\r\n");
|
||||
} catch (e) {
|
||||
console.error("Error sending initial greeting:", e);
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.on("data", async (data: Buffer) => {
|
||||
if (socket.destroyed || socket.closed) {
|
||||
console.error("Socket is destroyed or closed");
|
||||
return;
|
||||
}
|
||||
logMessage("in", data.toString());
|
||||
|
||||
const command = parseImapCommand(data.toString().trim());
|
||||
|
||||
console.log(command.command);
|
||||
|
||||
if (!command.command) {
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`${command.tag} BAD Command not recognized\r\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command.command.toUpperCase()) {
|
||||
case "CAPABILITY":
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
"* CAPABILITY IMAP4rev1 THREAD=REFERENCES\r\n",
|
||||
);
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`${command.tag} OK CAPABILITY completed\r\n`,
|
||||
);
|
||||
break;
|
||||
|
||||
case "LOGIN":
|
||||
handleLogin(
|
||||
session,
|
||||
socket,
|
||||
command.tag,
|
||||
command.parameters[0],
|
||||
command.parameters[1],
|
||||
);
|
||||
break;
|
||||
|
||||
case "CREATE":
|
||||
handleCreate(session, socket, command.tag, command.parameters[0]);
|
||||
break;
|
||||
|
||||
case "LIST":
|
||||
handleList(
|
||||
session,
|
||||
socket,
|
||||
command.tag,
|
||||
command.parameters[0],
|
||||
command.parameters[1],
|
||||
);
|
||||
break;
|
||||
|
||||
case "LSUB":
|
||||
handleLsub(
|
||||
session,
|
||||
socket,
|
||||
command.tag,
|
||||
command.parameters[0],
|
||||
command.parameters[1],
|
||||
);
|
||||
break;
|
||||
|
||||
case "SUBSCRIBE":
|
||||
handleSubscribe(session, socket, command.tag, command.parameters[0]);
|
||||
break;
|
||||
|
||||
case "SELECT":
|
||||
await handleSelect(
|
||||
session,
|
||||
socket,
|
||||
command.tag,
|
||||
command.parameters[0],
|
||||
);
|
||||
break;
|
||||
|
||||
case "LOGOUT":
|
||||
handleLogout(socket, command.tag);
|
||||
break;
|
||||
|
||||
case "UID FETCH":
|
||||
await handleUidFetch(
|
||||
session,
|
||||
socket,
|
||||
command.tag,
|
||||
command.parameters[0],
|
||||
command.parameters[1],
|
||||
);
|
||||
break;
|
||||
|
||||
case "UID STORE":
|
||||
await handleUidStore(
|
||||
session,
|
||||
socket,
|
||||
command.tag,
|
||||
command.parameters[0],
|
||||
command.parameters[1],
|
||||
command.parameters[2],
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log("Invalid command", command.command);
|
||||
sendSocketMessage(
|
||||
socket,
|
||||
`${command.tag} BAD Command not recognized\r\n`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("error", (err: Error) => {
|
||||
console.error("Socket error:", err);
|
||||
if (socket.destroyed || socket.closed) {
|
||||
return;
|
||||
}
|
||||
socket.end();
|
||||
});
|
||||
});
|
||||
|
||||
const PORT = 1143;
|
||||
server.listen(PORT, () => {
|
||||
console.log(`IMAP server listening on port ${PORT}`);
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue