import fs from 'fs'; import type { Readable } from 'stream'; import * as Sentry from '@sentry/node'; import nodemailer from 'nodemailer'; import type StreamTransport from 'nodemailer/lib/stream-transport/index.d.ts'; import type { Translations } from '../locale/translations.ts'; import { loadSuml, loadSumlFromBase } from './loader.ts'; import { rootDir } from './paths.ts'; const color = '#C71585'; const logo = fs.readFileSync(`${rootDir}/public/logo/logo-primary.svg`).toString('utf-8'); const logoEncoded = `data:image/svg+xml,${encodeURIComponent(logo)}`; const translations = loadSuml('translations') as Translations; const fallbackTranslations = loadSumlFromBase('locale/_base/translations') as Translations; let transport: string | StreamTransport.Options = process.env.MAILER_TRANSPORT!; if (!transport && process.env.NODE_ENV === 'development') { transport = { streamTransport: true }; } const transporter = nodemailer.createTransport(transport); const sendEmail = ( to: string, subject: string, text: string | undefined = undefined, html: string | undefined = undefined, from: string | undefined = undefined, ): void => { transporter.sendMail({ to, subject, text, html, from: from || process.env.MAILER_FROM, }, function (err, info) { if (process.env.NODE_ENV === 'development') { const streamInfo = info as StreamTransport.SentMessageInfo; if (streamInfo.message) { (streamInfo.message as Readable).pipe(process.stdout); } } if (err) { Sentry.captureException(err); } }); }; const findTranslation = (key: string) => { return _findTranslation(translations, key) || _findTranslation(fallbackTranslations, key); }; const _findTranslation = (translations: Translations, key: string) => { let x = translations; for (const part of key.split('.')) { x = x[part]; if (x === undefined) { return undefined; } } return x; }; const violations = findTranslation('terms.content.content.violationsExamples'); delete violations.miscellaneous; const terms = `${findTranslation('terms.content.content.violations')} ${Object.values(violations).join(', ')}`; interface Template { subject: string; text: string; html: string; from?: string; } const templates: Record = { base: { subject: '[[title]] » {{content}}', text: '', html: `
Logo [[title]]
{{content}}
`, }, notify: { subject: 'There are entries awaiting moderation', text: 'Entries awaiting moderation:\n\n{{list:stats}}', html: `

Entries awaiting moderation

`, }, confirmCode: { subject: '[[user.login.email.subject]]', text: '[[user.login.email.instruction]]\n\n{{code}}\n\n[[user.login.email.extra]]', html: `

[[user.login.email.instruction]]

{{code}}

[[user.login.email.extra]]

`, }, ban: { subject: '[[ban.header]]', text: `[[ban.header]]\n\n[[ban.reason]][[quotation.colon]] %reason%\n\n[[quotation.start]]${terms}[[quotation.end]]`, html: `

[[ban.header]]

[[ban.reason]][[quotation.colon]] %reason%

[[quotation.start]]${terms}[[quotation.end]]

[[ban.appealEmail]]

@{{username}}

`, from: process.env.MAILER_FROM_MODERATION, }, inactivityWarning: { subject: '[[user.removeInactive.email.subject]]', text: '[[user.removeInactive.email.content]]', html: `

[[user.removeInactive.email.content]]

[[user.removeInactive.email.clarification]] @{{username}}

[[user.removeInactive.email.cta]]

@{{username}}

`, }, cardsWarning: { subject: 'Cards queue is getting long', text: 'There\'s {{count}} cards in the queue!', html: '

There\'s {{count}} cards in the queue!

', from: process.env.MAILER_FROM_TECHNICAL, }, linksWarning: { subject: 'Links queue is getting long', text: 'There\'s {{count}} links in the queue!', html: '

There\'s {{count}} links in the queue!

', from: process.env.MAILER_FROM_TECHNICAL, }, translationProposed: { subject: '[{{locale}}] New translations proposed', text: 'Check them out here: https://[[domain]]/admin', html: '

Check them out here: [[domain]]/admin/translations/awaiting

', from: process.env.MAILER_FROM_LOCALISATION, }, translationToMerge: { subject: '[{{locale}}] New translations ready to be merged', text: 'Check them out here: https://[[domain]]/admin', html: '

Check them out here: [[domain]]/admin/translations/awaiting

', from: process.env.MAILER_FROM_LOCALISATION, }, modMessage: { subject: '[[user.modMessage.subject]]', text: '[[user.modMessage.intro]]\n\n{{nl2br:message}}\n\n[[user.modMessage.respond]]', html: `

[[user.modMessage.intro]]

{{nl2br:message}}

[[user.modMessage.respond]]

@{{modUsername}} → @{{username}}

`, from: process.env.MAILER_FROM_MODERATION, }, sensitiveApplied: { subject: '[[profile.sensitive.email.subject]]', text: '[[profile.sensitive.email.content]]\n\n{{warnings}}', html: `

[[profile.sensitive.email.content]] {{warnings}}

@{{modUsername}} → @{{username}}

`, from: process.env.MAILER_FROM_MODERATION, }, miastamaszerujace: { subject: 'miastamaszerujace.pl – zmiana treści', text: '', html: `

przed:

{{before}}

po:

{{after}}
`, }, }; const applyTemplate = ( template: Template, context: 'subject' | 'text' | 'html', params: Record>, ): string => { if (!params.maxwidth) { params.maxwidth = '480px'; } let templateText = template[context]; if (templates.base[context] !== undefined) { templateText = templates.base[context].replace('{{content}}', templateText); } templateText = templateText.replace(/%reason%/g, '{{reason}}'); // TODO templateText = templateText.replace(/\[\[([^\]]+)]]/g, (m) => { return findTranslation(m.substring(2, m.length - 2)); }); templateText = templateText.replace(/{{([^}]+)}}/g, (m) => { const key = m.substring(2, m.length - 2); if (key.startsWith('list:')) { const value = params[key.substring(5)] as string[] | Record; if (Array.isArray(value)) { return context === 'html' ? value.map((s) => `
  • ${s}
  • `).join('') : value.map((s) => ` - ${s}`).join('\n'); } else { return context === 'html' ? Object.keys(value).map((s) => `
  • ${s}: ${value[s]}
  • `) .join('') : Object.keys(value).map((s) => ` - ${s}: ${value[s]}`) .join('\n'); } } if (key.startsWith('nl2br:')) { const value = params[key.substring(6)] as string; return context === 'html' ? value.replace(new RegExp('\\n', 'g'), '
    ') : value; } return params[key] as string; }); return templateText; }; export default (to: string, template: string | Template, params = {}): void => { if (typeof template === 'string') { template = templates[template]; } sendEmail( process.env.MAILER_OVERWRITE || to, applyTemplate(template, 'subject', params), applyTemplate(template, 'text', params), applyTemplate(template, 'html', params), template.from, ); };