Incrusta el logo del reformista en los emails (CID) + tema de marca
- prepararLogo: el logo (data URI base64, URL http o ruta /public con APP_URL) se adjunta como imagen inline con Content-ID y se referencia con cid:, que es la forma fiable de mostrarlo en Gmail/Apple Mail/Outlook (el data URI directo lo bloquea Gmail). Fallback al nombre de la empresa si no hay logo. - El acento (barra, botón) ya usa el color de marca del tenant (resolveTheme). Probado: envío real con el logo de Reformas Ejemplo embebido y tema pizarra. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -43,18 +43,42 @@ type EmailParts = {
|
||||
parrafosHtml: string[]; // HTML seguro: las variables ya vienen escapadas
|
||||
cta?: { url: string; label: string } | null;
|
||||
footer: string;
|
||||
logoCid?: string | null; // si hay logo, se incrusta como adjunto inline (cid:)
|
||||
};
|
||||
|
||||
type LogoAdjunto = { filename: string; content?: Buffer; path?: string; cid: string; contentType?: string };
|
||||
|
||||
const LOGO_CID = 'logoempresa';
|
||||
|
||||
// Prepara el logo como adjunto inline (CID) para que se vea en todos los clientes (Gmail bloquea
|
||||
// las imágenes en data URI directas). Acepta data URI (base64), URL http(s), o ruta /public
|
||||
// (se vuelve absoluta con APP_URL). Devuelve null si no hay logo usable.
|
||||
function prepararLogo(logoUrl: string | null | undefined): LogoAdjunto | null {
|
||||
if (!logoUrl) return null;
|
||||
if (logoUrl.startsWith('data:')) {
|
||||
const coma = logoUrl.indexOf(',');
|
||||
if (coma === -1) return null;
|
||||
const mime = logoUrl.slice(5, coma).split(';')[0] || 'image/png';
|
||||
const ext = mime.split('/')[1]?.replace('+xml', '') || 'png';
|
||||
const content = Buffer.from(logoUrl.slice(coma + 1), 'base64');
|
||||
return { filename: `logo.${ext}`, content, cid: LOGO_CID, contentType: mime };
|
||||
}
|
||||
if (/^https?:\/\//.test(logoUrl)) return { filename: 'logo', path: logoUrl, cid: LOGO_CID };
|
||||
if (logoUrl.startsWith('/') && env.APP_URL) {
|
||||
return { filename: 'logo', path: `${env.APP_URL}${logoUrl}`, cid: LOGO_CID };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const FONT = "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif";
|
||||
|
||||
// Email transaccional: una columna (máx 600px), mobile-first, dark mode, botón bulletproof (tabla),
|
||||
// estilos inline como base + <style> para dark/responsive. Compatible con Gmail/Apple Mail/Outlook.
|
||||
function construirEmailHtml(p: EmailParts): string {
|
||||
const empresa = escapeHtml(p.empresa);
|
||||
const logo =
|
||||
p.brand.logoUrl && /^https?:\/\//.test(p.brand.logoUrl)
|
||||
? `<img src="${escapeHtml(p.brand.logoUrl)}" alt="${empresa}" height="34" style="max-height:34px;max-width:180px;display:block;border:0;" />`
|
||||
: `<span style="font-size:18px;font-weight:700;color:#1a1a1a;letter-spacing:-0.2px;">${empresa}</span>`;
|
||||
const logo = p.logoCid
|
||||
? `<img src="cid:${p.logoCid}" alt="${empresa}" height="40" style="max-height:40px;max-width:200px;display:block;border:0;" />`
|
||||
: `<span class="email-logo-name" style="font-size:18px;font-weight:700;color:#1a1a1a;letter-spacing:-0.2px;">${empresa}</span>`;
|
||||
|
||||
const cta = p.cta
|
||||
? `<tr><td style="height:28px;line-height:28px;"> </td></tr>
|
||||
@@ -110,7 +134,7 @@ function construirEmailHtml(p: EmailParts): string {
|
||||
<table role="presentation" class="email-card" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;background:#ffffff;border-radius:14px;overflow:hidden;">
|
||||
<tr><td style="height:4px;line-height:4px;background:${p.brand.primary};font-size:0;"> </td></tr>
|
||||
<tr><td class="email-head email-pad" style="padding:24px 40px 8px 40px;">
|
||||
<span class="email-logo-name">${logo}</span>
|
||||
${logo}
|
||||
</td></tr>
|
||||
<tr><td class="email-pad" style="padding:8px 40px 0 40px;">
|
||||
<h1 class="email-title" style="margin:0 0 18px 0;font-family:${FONT};font-size:26px;line-height:1.25;font-weight:800;color:#18181b;letter-spacing:-0.4px;">${escapeHtml(p.headline)}</h1>
|
||||
@@ -162,6 +186,7 @@ export async function enviarPresupuestoEmail(opts: {
|
||||
if (!transport) return false;
|
||||
|
||||
const empresaB = `<strong>${escapeHtml(opts.empresa)}</strong>`;
|
||||
const logo = prepararLogo(opts.brand?.logoUrl);
|
||||
const parts: EmailParts = {
|
||||
brand: opts.brand ?? BRAND_DEFECTO,
|
||||
empresa: opts.empresa,
|
||||
@@ -174,8 +199,18 @@ export async function enviarPresupuestoEmail(opts: {
|
||||
],
|
||||
cta: opts.cta ?? null,
|
||||
footer: `Presupuesto orientativo. El precio final puede variar según la visita técnica. · ${opts.empresa}`,
|
||||
logoCid: logo ? logo.cid : null,
|
||||
};
|
||||
|
||||
const attachments: Array<{
|
||||
filename: string;
|
||||
content?: Buffer;
|
||||
path?: string;
|
||||
cid?: string;
|
||||
contentType?: string;
|
||||
}> = [{ filename: opts.filename, content: opts.pdf, contentType: 'application/pdf' }];
|
||||
if (logo) attachments.push(logo);
|
||||
|
||||
try {
|
||||
await transport.sendMail({
|
||||
from: env.EMAIL_FROM,
|
||||
@@ -183,7 +218,7 @@ export async function enviarPresupuestoEmail(opts: {
|
||||
subject: 'Aquí está tu presupuesto de reforma',
|
||||
html: construirEmailHtml(parts),
|
||||
text: construirEmailText(parts),
|
||||
attachments: [{ filename: opts.filename, content: opts.pdf, contentType: 'application/pdf' }],
|
||||
attachments,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
@@ -205,6 +240,7 @@ export async function enviarEnlaceFormulario(opts: {
|
||||
if (!transport) return false;
|
||||
|
||||
const empresaB = `<strong>${escapeHtml(opts.empresa)}</strong>`;
|
||||
const logo = prepararLogo(opts.brand?.logoUrl);
|
||||
const parts: EmailParts = {
|
||||
brand: opts.brand ?? BRAND_DEFECTO,
|
||||
empresa: opts.empresa,
|
||||
@@ -217,6 +253,7 @@ export async function enviarEnlaceFormulario(opts: {
|
||||
],
|
||||
cta: { url: encodeURI(opts.url), label: 'Subir mis fotos' },
|
||||
footer: opts.empresa,
|
||||
logoCid: logo ? logo.cid : null,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -226,6 +263,7 @@ export async function enviarEnlaceFormulario(opts: {
|
||||
subject: 'Sube las fotos de tu reforma',
|
||||
html: construirEmailHtml(parts),
|
||||
text: construirEmailText(parts),
|
||||
attachments: logo ? [logo] : undefined,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user