TS207 linhas10.936 bytes
apps/worker/src/services/premiacao-email.ts
SHA-256
6306c2e701e05e2187b57d8744bf2e8fc89cd6e35ce6dd78df7642c9ce47b326
Somente leiturafonte-0531042d9f3e
import type { Env } from '../types'
import type { Certificado, DadosCertificado } from './premiacao-edicoes'
import { listarCertificados, registrarEnvio } from './premiacao-edicoes'
import { URL_VERIFICACAO, justificativa, nomeArquivo, pdfDoCertificado } from './certificado'
/**
* E-mail de entrega dos certificados ao gabinete.
*
* Um e-mail por senador, com todos os certificados dele na edição anexados.
* Remetente e assinatura são só institucionais; o texto evita concordância de
* gênero fora da saudação, e a saudação usa a forma oficial de tratamento.
*/
const FROM_EMAIL = 'Observatório do Senado <[email protected]>'
const REPLY_TO = '[email protected]'
const SITE = 'https://observasenado.org'
function esc(s: string): string {
return s.replace(/[&<>"']/g, (c) =>
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]!,
)
}
/** "Excelentíssima Senhora Senadora Fulana" / "Excelentíssimo Senhor Senador Fulano". */
function saudacao(d: DadosCertificado): string {
const t = d.senador.tratamento
if (t === 'Senadora') return `Excelentíssima Senhora Senadora ${d.senador.nome}`
if (t === 'Senador') return `Excelentíssimo Senhor Senador ${d.senador.nome}`
return `Ao Gabinete de ${t} ${d.senador.nome}`
}
export interface OpcoesEmail {
/** Parágrafo opcional escrito pelo admin, ex.: data da divulgação pública. */
mensagem?: string
/** Marca o e-mail como reenvio. */
reenvio?: boolean
/** Envio de teste para o admin: assunto com [TESTE] e aviso no topo. */
teste?: boolean
}
export function montarEmail(
certs: Certificado[],
opts: OpcoesEmail = {},
): { assunto: string; html: string; texto: string } {
const d = certs[0].dados
const varios = certs.length > 1
const edicao = `Prêmio Observatório do Senado — ${d.edicao.titulo}`
const assunto =
(opts.teste ? '[TESTE] ' : '') +
(opts.reenvio ? 'Reenvio: ' : '') +
`${edicao}: ${varios ? 'certificados' : 'certificado'} de reconhecimento`
const categoriasHtml = certs
.map((c) => `
<tr><td style="padding:14px 0;border-top:1px solid #e2dfd8">
<div style="font-family:Georgia,serif;font-size:17px;color:#8a6a22;font-weight:bold;letter-spacing:.04em">${esc(c.dados.categoria.titulo)}</div>
<div style="font-size:13px;color:#3d4247;line-height:1.6;margin-top:4px">${esc(justificativa(c.dados))}</div>
<div style="font-size:12px;color:#5b6166;margin-top:6px">Código de verificação:
<a href="${URL_VERIFICACAO}?codigo=${encodeURIComponent(c.codigo)}" style="color:#0F3D5C;font-weight:bold;letter-spacing:.06em">${esc(c.codigo)}</a></div>
</td></tr>`)
.join('')
const periodoTxt = d.edicao.tipo === 'legislatura'
? 'ao longo da 57ª Legislatura'
: `no ano legislativo de ${d.edicao.titulo.replace(/\D/g, '')}`
const mensagem = opts.mensagem?.trim()
const html = `<!doctype html>
<html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(assunto)}</title></head>
<body style="margin:0;padding:24px 12px;background:#f7f6f3;font-family:system-ui,-apple-system,'Segoe UI',Arial,sans-serif;color:#1a1a1a">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" align="center" width="620" style="max-width:620px;width:100%;background:#ffffff;border:1px solid #e2dfd8;border-radius:4px">
<tr><td style="padding:22px 32px;background:#0F3D5C;border-bottom:3px solid #C9A24B">
<div style="font-family:Georgia,serif;font-size:18px;color:#ffffff;letter-spacing:.03em">Observatório do Senado</div>
<div style="font-size:11px;color:#C9A24B;text-transform:uppercase;letter-spacing:.14em;margin-top:4px">${esc(edicao.replace('Prêmio Observatório do Senado — ', 'Prêmio · '))}</div>
</td></tr>
${opts.teste ? `<tr><td style="padding:10px 32px;background:#fff6d8;font-size:12px;color:#7a5b00">Envio de teste. Os anexos são os certificados reais desta edição.</td></tr>` : ''}
<tr><td style="padding:30px 32px 8px;font-size:15px;line-height:1.7">
<p style="margin:0 0 18px">${esc(saudacao(d))},</p>
<p style="margin:0 0 14px">O Observatório do Senado tem a satisfação de comunicar que Vossa Excelência obteve o primeiro lugar ${varios ? `em ${certs.length} categorias` : 'na categoria abaixo'} do <strong>${esc(edicao)}</strong>, que reconhece o desempenho parlamentar ${periodoTxt}.</p>
${opts.reenvio ? `<p style="margin:0 0 14px">Reenviamos, a pedido ou por precaução, ${varios ? 'os certificados já emitidos' : 'o certificado já emitido'}. ${varios ? 'Os códigos de verificação continuam os mesmos.' : 'O código de verificação continua o mesmo.'}</p>` : ''}
</td></tr>
<tr><td style="padding:0 32px">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">${categoriasHtml}</table>
</td></tr>
<tr><td style="padding:18px 32px 8px;font-size:14px;line-height:1.7;color:#1a1a1a">
<p style="margin:0 0 14px">O prêmio não tem inscrição, indicação nem votação. O resultado decorre exclusivamente dos dados abertos do Senado Federal, aplicados ao Índice de Desempenho Senatorial (IDS), cuja <a href="${SITE}/metodologia" style="color:#0F3D5C">metodologia</a> e cujo <a href="${SITE}/codigo" style="color:#0F3D5C">código-fonte</a> são públicos e podem ser conferidos por qualquer pessoa.</p>
<p style="margin:0 0 14px">${varios ? 'Seguem anexos os certificados' : 'Segue anexo o certificado'} em PDF. ${varios ? 'Cada um traz' : 'Ele traz'} um código e um QR code que permitem conferir a autenticidade em <a href="${URL_VERIFICACAO}" style="color:#0F3D5C">observasenado.org/premiacao/verificar</a>. O gabinete pode divulgar ${varios ? 'os certificados' : 'o certificado'} livremente.</p>
${mensagem ? `<p style="margin:0 0 14px">${esc(mensagem).replace(/\n/g, '<br>')}</p>` : ''}
<p style="margin:22px 0 0">Respeitosamente,</p>
<p style="margin:6px 0 0;font-family:Georgia,serif;font-size:16px;color:#0F3D5C"><strong>Observatório do Senado</strong></p>
<p style="margin:2px 0 0;font-size:12px;color:#5b6166">Iniciativa cidadã independente, sem vínculo partidário</p>
</td></tr>
<tr><td style="padding:22px 32px 26px">
<hr style="border:none;border-top:1px solid #e2dfd8;margin:0 0 12px">
<p style="margin:0;font-size:11px;color:#8a8e93;line-height:1.6">Dúvidas ou correções: responda a este e-mail ou escreva para <a href="mailto:${REPLY_TO}" style="color:#5b6166">${REPLY_TO}</a>. <a href="${SITE}" style="color:#5b6166">observasenado.org</a></p>
</td></tr>
</table>
</body></html>`
const texto = [
`${saudacao(d)},`,
'',
`O Observatório do Senado tem a satisfação de comunicar que Vossa Excelência obteve o primeiro lugar ${varios ? `em ${certs.length} categorias` : 'na categoria abaixo'} do ${edicao}, que reconhece o desempenho parlamentar ${periodoTxt}.`,
...(opts.reenvio ? ['', `Reenviamos ${varios ? 'os certificados já emitidos; os códigos' : 'o certificado já emitido; o código'} de verificação ${varios ? 'continuam os mesmos' : 'continua o mesmo'}.`] : []),
'',
...certs.flatMap((c) => [
`• ${c.dados.categoria.titulo}`,
` ${justificativa(c.dados)}`,
` Código de verificação: ${c.codigo} (${URL_VERIFICACAO}?codigo=${c.codigo})`,
'',
]),
'O prêmio não tem inscrição, indicação nem votação. O resultado decorre exclusivamente dos dados abertos do Senado Federal, aplicados ao Índice de Desempenho Senatorial (IDS), com metodologia e código-fonte públicos em observasenado.org.',
'',
`${varios ? 'Seguem anexos os certificados' : 'Segue anexo o certificado'} em PDF, com código e QR code para conferência da autenticidade em observasenado.org/premiacao/verificar. O gabinete pode divulgá-${varios ? 'los' : 'lo'} livremente.`,
...(mensagem ? ['', mensagem] : []),
'',
'Respeitosamente,',
'Observatório do Senado',
'Iniciativa cidadã independente, sem vínculo partidário',
REPLY_TO,
].join('\n')
return { assunto, html, texto }
}
/** Certificados de um senador na edição, sem os revogados. */
async function certificadosDoSenador(
env: Env,
edicaoId: string,
senadorCod: string,
): Promise<Certificado[]> {
return (await listarCertificados(env, edicaoId)).filter(
(c) => c.senadorCod === senadorCod && !c.revogadoEm,
)
}
export async function previaEmail(
env: Env,
edicaoId: string,
senadorCod: string,
opts: OpcoesEmail = {},
) {
const certs = await certificadosDoSenador(env, edicaoId, senadorCod)
if (certs.length === 0) return null
return { ...montarEmail(certs, opts), para: certs[0].email, anexos: certs.map((c) => nomeArquivo(c.dados)) }
}
export interface ResultadoEnvio {
senadorCod: string
para: string
ok: boolean
certificados: string[]
erro?: string
}
/**
* Envia os certificados do senador. Com `paraTeste`, manda ao endereço do
* admin e não marca nada como enviado.
*/
export async function enviarCertificados(
env: Env,
edicaoId: string,
senadorCod: string,
opts: OpcoesEmail & { paraTeste?: string } = {},
): Promise<ResultadoEnvio> {
if (!env.RESEND_API_KEY) throw new Error('RESEND_API_KEY não configurada')
const certs = await certificadosDoSenador(env, edicaoId, senadorCod)
if (certs.length === 0) throw new Error('nenhum certificado válido para este senador')
const para = opts.paraTeste ?? certs[0].email
if (!para) throw new Error('defina o e-mail de envio antes')
const anexos = []
for (const c of certs) {
const pdf = await pdfDoCertificado(env, c.codigo)
if (!pdf) throw new Error(`PDF do certificado ${c.codigo} indisponível`)
anexos.push({ filename: nomeArquivo(c.dados), content: Buffer.from(pdf).toString('base64') })
}
const teste = Boolean(opts.paraTeste)
const reenvio = !teste && (opts.reenvio ?? certs.some((c) => c.enviadoEm))
const { assunto, html, texto } = montarEmail(certs, { ...opts, teste, reenvio })
const codigos = certs.map((c) => c.codigo)
let resultado: { ok: boolean; erro?: string }
try {
const r = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: { Authorization: `Bearer ${env.RESEND_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
from: FROM_EMAIL,
to: [para],
reply_to: REPLY_TO,
subject: assunto,
html,
text: texto,
attachments: anexos,
}),
})
resultado = r.ok ? { ok: true } : { ok: false, erro: `Resend ${r.status}: ${(await r.text()).slice(0, 200)}` }
} catch (err) {
resultado = { ok: false, erro: String(err) }
}
if (!teste) await registrarEnvio(env, codigos, resultado)
return { senadorCod, para, ok: resultado.ok, certificados: codigos, erro: resultado.erro }
}