111 lines
3.8 KiB
TypeScript
111 lines
3.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest"
|
|
|
|
// Mock tls to simulate an SMTP server over implicit TLS
|
|
let lastWrites: string[] = []
|
|
vi.mock("tls", () => {
|
|
class MockSocket {
|
|
listeners: Record<string, Function[]> = {}
|
|
writes: string[] = []
|
|
// very small state machine of server responses
|
|
private step = 0
|
|
on(event: string, cb: Function) {
|
|
this.listeners[event] = this.listeners[event] || []
|
|
this.listeners[event].push(cb)
|
|
return this
|
|
}
|
|
removeListener(event: string, cb: Function) {
|
|
if (!this.listeners[event]) return this
|
|
this.listeners[event] = this.listeners[event].filter((f) => f !== cb)
|
|
return this
|
|
}
|
|
emit(event: string, data?: unknown) {
|
|
for (const cb of this.listeners[event] || []) cb(data)
|
|
}
|
|
write(chunk: string) {
|
|
this.writes.push(chunk)
|
|
const line = chunk.replace(/\r?\n/g, "")
|
|
// Respond depending on client command
|
|
if (this.step === 0 && line.startsWith("EHLO")) {
|
|
this.step = 1
|
|
this.emit("data", Buffer.from("250-local\r\n"))
|
|
this.emit("data", Buffer.from("250 OK\r\n"))
|
|
} else if (this.step === 1 && line === "AUTH LOGIN") {
|
|
this.step = 2
|
|
this.emit("data", Buffer.from("334 VXNlcm5hbWU6\r\n"))
|
|
} else if (this.step === 2) {
|
|
this.step = 3
|
|
this.emit("data", Buffer.from("334 UGFzc3dvcmQ6\r\n"))
|
|
} else if (this.step === 3) {
|
|
this.step = 4
|
|
this.emit("data", Buffer.from("235 Auth OK\r\n"))
|
|
} else if (this.step === 4 && line.startsWith("MAIL FROM:")) {
|
|
this.step = 5
|
|
this.emit("data", Buffer.from("250 FROM OK\r\n"))
|
|
} else if (this.step === 5 && line.startsWith("RCPT TO:")) {
|
|
this.step = 6
|
|
this.emit("data", Buffer.from("250 RCPT OK\r\n"))
|
|
} else if (this.step === 6 && line === "DATA") {
|
|
this.step = 7
|
|
this.emit("data", Buffer.from("354 End data with <CR><LF>.<CR><LF>\r\n"))
|
|
} else if (this.step === 7 && line.endsWith(".")) {
|
|
this.step = 8
|
|
this.emit("data", Buffer.from("250 Queued\r\n"))
|
|
} else if (this.step === 8 && line === "QUIT") {
|
|
this.emit("end")
|
|
}
|
|
}
|
|
end() {}
|
|
}
|
|
|
|
function connect(_port: number, _host: string, _opts: unknown, cb?: Function) {
|
|
const socket = new MockSocket()
|
|
lastWrites = socket.writes
|
|
// initial server greeting
|
|
setTimeout(() => {
|
|
cb?.()
|
|
socket.emit("data", Buffer.from("220 Mock SMTP Ready\r\n"))
|
|
}, 0)
|
|
return socket as unknown as NodeJS.WritableStream & { on: MockSocket["on"] }
|
|
}
|
|
|
|
return { default: { connect }, connect, __getLastWrites: () => lastWrites }
|
|
})
|
|
|
|
describe("sendSmtpMail", () => {
|
|
it("performs AUTH LOGIN and sends a message", async () => {
|
|
const { sendSmtpMail } = await import("@/server/email-smtp")
|
|
await expect(
|
|
sendSmtpMail(
|
|
{
|
|
host: "smtp.mock",
|
|
port: 465,
|
|
username: "user@example.com",
|
|
password: "secret",
|
|
from: "Sender <sender@example.com>",
|
|
},
|
|
"rcpt@example.com",
|
|
"Subject here",
|
|
"<p>Hello</p>"
|
|
)
|
|
).resolves.toBeUndefined()
|
|
})
|
|
|
|
it("extracts envelope address from parentheses or raw email", async () => {
|
|
const { sendSmtpMail } = await import("@/server/email-smtp")
|
|
const tlsMock = await import("tls" as any)
|
|
await sendSmtpMail(
|
|
{
|
|
host: "smtp.mock",
|
|
port: 465,
|
|
username: "user@example.com",
|
|
password: "secret",
|
|
from: "Chatwoot chat@esdrasrenan.com.br (chat@esdrasrenan.com.br)",
|
|
},
|
|
"rcpt@example.com",
|
|
"Subject",
|
|
"<p>Hi</p>"
|
|
)
|
|
const writes = (tlsMock as any).__getLastWrites() as string[]
|
|
expect(writes.some((w) => /MAIL FROM:<chat@esdrasrenan.com.br>\r\n/.test(w))).toBe(true)
|
|
})
|
|
})
|