import { iterateReader } from "https://deno.land/std@0.133.0/streams/conversion.ts"; async function* newlineIterator(iterator: AsyncIterableIterator) { const decoder = new TextDecoder(); let buffer = ""; for await (const chunk of iterator) { let chunkString = decoder.decode(chunk); const newlinePos = chunkString.indexOf("\r\n"); if (newlinePos >= 0) { const out = buffer + chunkString.substring(0, newlinePos); buffer = chunkString.substring(newlinePos + 2); yield out; } else { buffer += chunkString; } let pos: number; while ((pos = buffer.indexOf("\r\n")) >= 0) { const out = buffer.substring(0, pos); buffer = buffer.substring(pos + 2); yield out; } } } enum State { WaitingHello, WaitingMail, WaitingRecipient, WaitingData, ReceivingData, } export type OnReceive = ( recipient: string, sender: string, data: string, ) => void; export class Session { private state: State; private encoder: TextEncoder; private data: string; private recipient: string; private sender: string; constructor(readonly conn: Deno.Conn, readonly onReceive: OnReceive) { this.state = State.WaitingHello; this.encoder = new TextEncoder(); this.data = ""; this.recipient = ""; this.sender = ""; } async start() { this.output("220 smtp2rss"); for await (const chunk of newlineIterator(iterateReader(this.conn))) { this.input(chunk); } } input(data: string) { switch (this.state) { case State.WaitingHello: return this.waitingHello(data); case State.WaitingMail: return this.waitingMail(data); case State.WaitingRecipient: return this.waitingRecipient(data); case State.WaitingData: return this.waitingData(data); case State.ReceivingData: return this.receivingData(data); } } waitingHello(data: string) { const command = data.substring(0, 4); if (command === "HELO" || command === "EHLO") { this.state = State.WaitingMail; this.output("250 OK"); } else if (command === "QUIT") { this.output("221 smtp2rss closing"); this.conn.close(); } else { this.output("500 Invalid state"); } } waitingMail(data: string) { const command = data.substring(0, 4); if (command === "MAIL") { const match = data.match(/^MAIL FROM:<([^>]*)>.*$/); if (match !== null) { this.sender = match[1]; this.state = State.WaitingRecipient; this.output("250 OK"); } else { this.output("500 Invalid sender"); } } else { this.output("500 Invalid state"); } } waitingRecipient(data: string) { const command = data.substring(0, 4); if (command === "RCPT") { const match = data.match(/^RCPT TO:<([^>]*)>.*$/); if (match !== null) { this.recipient = match[1]; this.state = State.WaitingData; this.output("250 OK"); } else { this.output("500 Invalid sender"); } } else { this.output("500 Invalid state"); } } waitingData(data: string) { const command = data.substring(0, 4); if (command === "DATA") { this.state = State.ReceivingData; this.output("354 Waiting for data"); } else { this.output("500 Invalid state"); } } receivingData(data: string) { if (data === ".") { this.state = State.WaitingMail; this.output("250 OK"); this.onReceive(this.recipient, this.sender, this.data); this.recipient = ""; this.sender = ""; this.data = ""; } else { this.data += data; } } output(response: string) { this.conn.write(this.encoder.encode(`${response}\r\n`)); } } export async function handleSmtp(conn: Deno.Conn, onReceive: OnReceive) { const session = new Session(conn, onReceive); await session.start(); }