SMTP: extend env parsing (domain/auth/starttls); add unit test with mocked TLS for sendSmtpMail; extend SmtpConfig; docs to set .env locally

This commit is contained in:
Esdras Renan 2025-10-07 16:01:56 -03:00
parent 53c76a0289
commit 81fd572e48
3 changed files with 98 additions and 1 deletions

91
tests/email-smtp.test.ts Normal file
View file

@ -0,0 +1,91 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
// Mock tls to simulate an SMTP server over implicit TLS
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()
// 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 }
})
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()
})
})