reports(SLA): aplica filtro de período (7d/30d/90d) no Convex e inclui período no filename do CSV; admin(alerts): filtros no servidor; alerts: batch de últimos alertas por slugs; filtros persistentes de empresa (localStorage) em relatórios; prisma: Company.contractedHoursPerMonth; smtp: suporte a múltiplos destinatários e timeout opcional
This commit is contained in:
parent
a23b429e4d
commit
384d4411b6
13 changed files with 133 additions and 38 deletions
|
|
@ -7,6 +7,8 @@ type SmtpConfig = {
|
|||
password: string
|
||||
from: string
|
||||
tls?: boolean
|
||||
rejectUnauthorized?: boolean
|
||||
timeoutMs?: number
|
||||
}
|
||||
|
||||
function b64(input: string) {
|
||||
|
|
@ -27,24 +29,32 @@ function extractEnvelopeAddress(from: string): string {
|
|||
return from
|
||||
}
|
||||
|
||||
export async function sendSmtpMail(cfg: SmtpConfig, to: string, subject: string, html: string) {
|
||||
export async function sendSmtpMail(cfg: SmtpConfig, to: string | string[], subject: string, html: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const socket = tls.connect(cfg.port, cfg.host, { rejectUnauthorized: false }, () => {
|
||||
const socket = tls.connect(cfg.port, cfg.host, { rejectUnauthorized: cfg.rejectUnauthorized ?? false }, () => {
|
||||
let buffer = ""
|
||||
const send = (line: string) => socket.write(line + "\r\n")
|
||||
const wait = (expected: string | RegExp) =>
|
||||
new Promise<void>((res, rej) => {
|
||||
const timeout = setTimeout(() => {
|
||||
socket.removeListener("data", onData)
|
||||
rej(new Error("smtp_timeout"))
|
||||
}, Math.max(1000, cfg.timeoutMs ?? 10000))
|
||||
const onData = (data: Buffer) => {
|
||||
buffer += data.toString()
|
||||
const lines = buffer.split(/\r?\n/)
|
||||
const last = lines.filter(Boolean).slice(-1)[0] ?? ""
|
||||
if (typeof expected === "string" ? last.startsWith(expected) : expected.test(last)) {
|
||||
socket.removeListener("data", onData)
|
||||
clearTimeout(timeout)
|
||||
res()
|
||||
}
|
||||
}
|
||||
socket.on("data", onData)
|
||||
socket.on("error", rej)
|
||||
socket.on("error", (e) => {
|
||||
clearTimeout(timeout)
|
||||
rej(e)
|
||||
})
|
||||
})
|
||||
|
||||
;(async () => {
|
||||
|
|
@ -61,13 +71,21 @@ export async function sendSmtpMail(cfg: SmtpConfig, to: string, subject: string,
|
|||
const envelopeFrom = extractEnvelopeAddress(cfg.from)
|
||||
send(`MAIL FROM:<${envelopeFrom}>`)
|
||||
await wait(/^250 /)
|
||||
send(`RCPT TO:<${to}>`)
|
||||
await wait(/^250 /)
|
||||
const rcpts: string[] = Array.isArray(to)
|
||||
? to
|
||||
: String(to)
|
||||
.split(/[;,]/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
for (const rcpt of rcpts) {
|
||||
send(`RCPT TO:<${rcpt}>`)
|
||||
await wait(/^250 /)
|
||||
}
|
||||
send("DATA")
|
||||
await wait(/^354 /)
|
||||
const headers = [
|
||||
`From: ${cfg.from}`,
|
||||
`To: ${to}`,
|
||||
`To: ${Array.isArray(to) ? to.join(", ") : to}`,
|
||||
`Subject: ${subject}`,
|
||||
"MIME-Version: 1.0",
|
||||
"Content-Type: text/html; charset=UTF-8",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue