/** * Correos con el diseño del tema — portado de `home/templates/emails/`. * * De las 7 plantillas de Django solo `security_token` y `activation` traían el * diseño; las otras 5 eran HTML soso, de ahí que aquí TODAS usen la misma * maqueta: la de `security_token`. * * La plantilla de Django era un volcado del DOM de Gmail. Al portarla se quitó * lo que no era diseño: los enlaces iban envueltos en el rastreador de Mailjet * (`079xk.mjt.lu`), que además redirigía a **ultimowow.com** en vez de a * nosotros; había un píxel de apertura, un "Click on me" oculto y atributos que * mete Gmail al mostrar el correo. Los 9 enlaces reales se reapuntan aquí. * * Ninguna URL va a fuego: todas cuelgan de SITE_URL (la variable que ya usa el * resto del proyecto), así que cambiar de dominio es tocar el .env. Ojo: en un * correo los enlaces han de ser ABSOLUTOS — se abre desde Gmail, no desde * nuestro dominio, así que "/ruta" no valdría. * * El contenido es el nuestro, no el de Django: el `activation` de allí mandaba * la contraseña en texto plano y aquí no se manda (además, con Battle.net se * entra con el correo, no con un usuario). */ import { MAX_GAME_ACCOUNTS, gameAccountDisplayName } from './bnet' const SERVER_NAME = 'NightSpire' /** Raíz del sitio, sin barra final. De aquí salen todos los enlaces e imágenes. */ function siteUrl(): string { return (process.env.SITE_URL || 'https://nightspire.gg').replace(/\/+$/, '') } /** Los correos son HTML crudo: hay que escapar todo lo que venga de fuera. */ export function esc(s: string): string { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') } // Estilos calcados del diseño original. const P_MAIN = "Margin:0;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';line-height:21px;color:#ebdec2" // Igual que el principal pero en el tono apagado: es lo que hace el diseño. const P_DATA = P_MAIN.replace('color:#ebdec2', 'color:#b1997f') const BTN = "text-decoration:none;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-size:16px;color:#ebdec2;border-style:solid;border-color:#211917;border-width:10px 20px 10px 20px;display:inline-block;background:#211917;border-radius:0px;font-weight:normal;font-style:normal;line-height:19px;width:auto;text-align:center" const H1 = "Margin:0;line-height:45px;font-family:georgia,times,'times new roman',serif;font-size:30px;font-style:normal;font-weight:normal;color:#b17c02" const TD = 'padding:0;Margin:0' const TD_LAST = 'padding:0;Margin:0;padding-top:5px;padding-bottom:20px' /** Párrafo normal (crema). El HTML que se le pase ya debe ir escapado. */ function p(html: string, td: string = TD): string { return `

${html}

` } /** Párrafo de datos (cuenta, token…), en el tono apagado del diseño. */ function pData(html: string): string { return `

${html}

` } /** La línea separadora. */ function divider(): string { return `
` } /** El botón del diseño (el mismo que traía `activation`). */ function button(href: string, text: string): string { return `${text}` } /** * Enlace de reserva: si el botón no tira, el usuario copia y pega. Lo hacía el * `activation` original y lo hacen ya nuestros correos. */ function fallbackLink(link: string): string { return p( `Si el botón no funciona, copia y pega este enlace en tu navegador:
${esc(link)}`, ) } /** Cierre común a todas las plantillas. */ function signature(): string { return p(`

Atentamente, el Equipo de ${SERVER_NAME}.`, TD_LAST) } /** * La maqueta: cabecera +

+ filas de contenido + pie. * @param label rótulo superior en mayúsculas (p.ej. TOKEN DE SEGURIDAD) * @param title el

grande * @param rows filas hechas con p()/pData()/divider()/button() */ function layout(label: string, title: string, rows: string[], locale = 'es'): string { const site = siteUrl() const year = new Date().getFullYear() const head = `

${label}

` const foot = `

SÍGUENOS EN

Fb Ig Tw Yt

Términos y Condiciones | Política de Reembolso | Política de Privacidad
${SERVER_NAME} © ${year}


` return head + `

` + esc(title) + '

' + rows.join('') + foot } // ─── Las 7 plantillas ──────────────────────────────────────────────────────── /** `activation.html` — alta de cuenta. */ export function activationEmailHtml(email: string, link: string, locale = 'es'): string { // El

lo escapa `layout`, así que el email va en crudo. Es el mismo valor que // en «Cuenta:»: el correo tal cual se registró (con bnet, la cuenta ES el correo). return layout('ACTIVACIÓN DE CUENTA', `Bienvenido ${email}`, [ p( `Gracias por crear una cuenta en ${SERVER_NAME}.
Para poder empezar a usar tu cuenta, deberás activarla haciendo clic en el botón ACTIVAR LA CUENTA.

Tus datos de cuenta son:`, ), pData(`Cuenta: ${esc(email)}

`), p( 'Recuerda que se entra tanto a la web como al servidor con tu correo y tu contraseña.
' + `Recuerda que puedes crear hasta ${MAX_GAME_ACCOUNTS} cuentas que quieras desde panel de usuario asociadas a este correo.`, ), button(link, 'ACTIVAR LA CUENTA'), fallbackLink(link), divider(), signature(), ], locale) } /** `security_token.html` — el diseño de referencia. */ export function securityTokenEmailHtml(username: string, token: string, ip: string, locale = 'es'): string { return layout('TOKEN DE SEGURIDAD', 'Token de Seguridad', [ p( `Alguien desde la IP ${esc(ip)} ha solicitado el Token de seguridad para la cuenta ${esc(username)}.
Si no has sido tú, te recomendamos cambiar la contraseña de tu cuenta.

Tus datos de cuenta son:`, ), pData(`Cuenta: ${esc(username)}
Token de Seguridad: ${esc(token)}

`), p( 'Recuerda que este Token es sensible a mayúsculas y minúsculas.
Al usarlo, escríbelo exactamente de la misma forma que aparece aquí.', ), divider(), p(`¡No compartas el Token de Seguridad con nadie!


Atentamente, el Equipo de ${SERVER_NAME}.`, TD_LAST), ], locale) } /** `password_reset.html` — restablecer contraseña. */ export function passwordResetEmailHtml(email: string, link: string, locale = 'es'): string { return layout('RESTABLECER CONTRASEÑA', 'Restablecer Contraseña', [ p( `Hemos recibido una solicitud para restablecer la contraseña de la cuenta asociada a ${esc(email)}.
Pulsa el botón para elegir una nueva. El enlace caduca en 1 hora.`, ), button(link, 'RESTABLECER CONTRASEÑA'), fallbackLink(link), divider(), p('Si no has solicitado este cambio, ignora este correo: tu contraseña no se modificará.', TD_LAST), ], locale) } /** `account_names.html` — recordatorio de cuentas de juego. */ /** `accounts` son los usernames internos («17#1»); aquí se muestran como «WOW1». */ export function accountNamesEmailHtml(email: string, accounts: string[], locale = 'es'): string { const list = accounts.length ? pData(accounts.map((a) => `Cuenta: ${esc(gameAccountDisplayName(a))}`).join('
') + '

') : pData('No hay cuentas de juego asociadas todavía.

') return layout('CUENTAS DE JUEGO', 'Tus Cuentas de Juego', [ p( `Estas son las cuentas de juego asociadas a tu cuenta Battle.net (${esc(email)}).
Recuerda que para entrar a la web usas tu correo electrónico.

Tus datos de cuenta son:`, ), list, divider(), p('Si no has solicitado esta información, ignora este correo.', TD_LAST), ], locale) } /** `confirm_old_email.html` — se confirma desde el correo ACTUAL. */ /** * Cómo se saluda a alguien: «INNA@INNA.CL (WOW1)». Antes se saludaba con el * username interno («Hola 15#1»), que no le dice nada a nadie: la cuenta es el * correo, y el «15#1» solo tiene sentido como WOW1. Si no se sabe la cuenta de * juego, va el correo a secas. */ function saludo(email: string, gameAccount?: string | null): string { const wow = gameAccount ? gameAccountDisplayName(gameAccount) : '' return wow ? `${esc(email)} (${esc(wow)})` : esc(email) } export function confirmOldEmailHtml( email: string, gameAccount: string | null, newEmail: string, link: string, locale = 'es', ): string { return layout('CAMBIO DE CORREO', 'Confirmación de Cambio de Correo', [ p( `Hola ${saludo(email, gameAccount)}, has solicitado cambiar tu correo en ${SERVER_NAME} a ${esc(newEmail)}.
Para confirmar el cambio, pulsa el botón.`, ), button(link, 'CONFIRMAR CAMBIO'), fallbackLink(link), divider(), p('Si no has solicitado este cambio, ignora este correo.', TD_LAST), ], locale) } /** `confirm_new_email.html` — se confirma desde el correo NUEVO. */ export function confirmNewEmailHtml( email: string, gameAccount: string | null, link: string, locale = 'es', ): string { return layout('NUEVO CORREO', 'Activación de Nuevo Correo', [ p( `Hola ${saludo(email, gameAccount)}, tu solicitud para cambiar el correo en ${SERVER_NAME} ha sido recibida.
Pulsa el botón para confirmar este correo como el nuevo de tu cuenta.`, ), button(link, 'ACTIVAR NUEVO CORREO'), fallbackLink(link), divider(), p('Si no has realizado esta solicitud, contacta con soporte.', TD_LAST), ], locale) } /** `old_email_notification.html` — aviso al correo antiguo, ya sin enlaces. */ export function oldEmailNotificationHtml( email: string, gameAccount: string | null, newEmail: string, locale = 'es', ): string { return layout('CAMBIO DE CORREO', 'Cambio de Correo Realizado', [ p( `Hola ${saludo(email, gameAccount)}, te avisamos de que el correo de tu cuenta en ${SERVER_NAME} se ha cambiado correctamente al siguiente:`, ), pData(`Nuevo correo: ${esc(newEmail)}

`), divider(), p('Si no has realizado este cambio, contacta con soporte inmediatamente.', TD_LAST), ], locale) } /** * Variante de `security_token` para el recordatorio de `/recover`: allí se * mandan de golpe los tokens de todas las cuentas de juego del correo, cosa que * la plantilla de Django (de una sola cuenta) no contemplaba. */ export function securityTokensListEmailHtml( email: string, items: { username: string; token: string }[], locale = 'es', ): string { return layout( 'TOKEN DE SEGURIDAD', 'Token de Seguridad', [ p(`Estos son los Token de seguridad de las cuentas de juego asociadas a ${esc(email)}.

Tus datos de cuenta son:`), pData( items .map((i) => `Cuenta: ${esc(gameAccountDisplayName(i.username))}
Token de Seguridad: ${esc(i.token)}`) .join('

') + '

', ), p( 'Recuerda que este Token es sensible a mayúsculas y minúsculas.
Al usarlo, escríbelo exactamente de la misma forma que aparece aquí.', ), divider(), p(`¡No compartas el Token de Seguridad con nadie!


Atentamente, el Equipo de ${SERVER_NAME}.`, TD_LAST), ], locale, ) }