mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 02:52:33 -05:00
Notification e-mail now defaults to the clearly arranged layout of #5171 - board first, board, actor and card in bold and linked, with a plain-text alternative. One sentence (classic) is still one choice away at the Admin Panel, board and member levels, and the tray keeps the classic line. Account e-mails - reset password, verify e-mail, enroll account, and the board and registration invitations - get an HTML part in the same style, built from the same translated text, which is still sent as the text part: no translation changes. Every piece of text is escaped first, the link paragraph becomes a button, and only an http(s) address is linked. A caller's own HTML and texts that are not WeKan's e-mail strings are left as they were, and a failure to build the HTML leaves the text alone. models/lib/accountEmailHtml.js renders it; tests/accountEmailHtml.test.cjs lays out the real English texts and checks that markup in a name, a board title or the site name stays text and only http(s) is linked. notificationDelivery's default assertions now expect clear for e-mail, and emailLocalizationPreparation's harness allows the new module, checks the HTML part, and checks the absent-HTML case with literal text. Thanks to Meeques and xet7 ! Fixes #4434.
91 lines
3.9 KiB
JavaScript
91 lines
3.9 KiB
JavaScript
// emailLocalization.js
|
|
// Utility functions to handle email localization in Wekan
|
|
|
|
import { TAPi18n } from '/imports/i18n';
|
|
import { ReactiveCache } from '/imports/reactiveCache';
|
|
|
|
// Main object for email localization utilities
|
|
const EmailLocalization = {
|
|
/**
|
|
* Prepare transport content using the recipient's preferred language.
|
|
* Does not send; callers may persist the returned message before dispatch.
|
|
* @param {Object} options - Standard email sending options plus language options
|
|
* @param {String} options.to - Recipient email address
|
|
* @param {String} options.from - Sender email address
|
|
* @param {String} options.subject - Email subject i18n key
|
|
* @param {String} options.text - Email text i18n key
|
|
* @param {Object} options.params - Parameters for i18n translation
|
|
* @param {String} options.language - Language code to use (if not provided, will try to detect)
|
|
* @param {String} options.userId - User ID to determine language (if not provided with language)
|
|
*/
|
|
async prepareEmail(options) {
|
|
// Determine the language to use
|
|
let lang = options.language;
|
|
|
|
// If no language is specified but we have a userId, try to get the user's language
|
|
if (!lang && options.userId) {
|
|
const user = await ReactiveCache.getUser(options.userId);
|
|
if (user) {
|
|
lang = user.getLanguage();
|
|
}
|
|
}
|
|
|
|
// If no language could be determined, use the site default
|
|
if (!lang) {
|
|
lang = TAPi18n.getLanguage() || 'en';
|
|
}
|
|
|
|
// #5875: ensure the chosen language's bundle is loaded on the server before
|
|
// translating, otherwise __() falls back to English.
|
|
await TAPi18n.ensureLanguageLoaded(lang);
|
|
|
|
// Translate subject and text using the determined language
|
|
const subject = TAPi18n.__(options.subject, options.params || {}, lang);
|
|
let text = options.text;
|
|
|
|
// If text is an i18n key, translate it
|
|
if (typeof text === 'string' && text.startsWith('email-')) {
|
|
text = TAPi18n.__(text, options.params || {}, lang);
|
|
}
|
|
|
|
// Build final transport content without sending it. Durable callers can
|
|
// persist this snapshot instead of re-localizing during a later retry.
|
|
const mail = {
|
|
to: options.to,
|
|
from: options.from || Accounts.emailTemplates.from,
|
|
subject: subject,
|
|
text: text
|
|
};
|
|
if (options.attachments !== undefined) {
|
|
const { validateRuleEmailAttachments } = require('./ruleEmailAttachments');
|
|
// Only byte snapshots may cross this boundary, never paths or URLs.
|
|
mail.attachments = structuredClone(validateRuleEmailAttachments(options.attachments));
|
|
}
|
|
if (options.html !== undefined) mail.html = options.html;
|
|
// #4434: a WeKan account e-mail (an `email-...` i18n text such as a board
|
|
// invitation) also goes out in the clear HTML layout, built from the same
|
|
// translated text. A caller's own HTML, or text that is not one of
|
|
// WeKan's e-mail strings, is left exactly as it was.
|
|
else if (typeof options.text === 'string' && options.text.startsWith('email-') && typeof text === 'string') {
|
|
const { renderAccountEmailHtml } = require('/models/lib/accountEmailHtml');
|
|
mail.html = renderAccountEmailHtml({ text, siteName: Accounts.emailTemplates.siteName });
|
|
}
|
|
// #2414: reply-by-email - a Reply-To carrying an HMAC-signed card token,
|
|
// set by server/notifications/emailQueue.js, lets a reply sent through the
|
|
// inbound-email webhook (server/routes/inboundEmail.js) be matched back to
|
|
// the card it was about. Omitted entirely when the caller has none to give
|
|
// (e.g. WEKAN_INBOUND_EMAIL_DOMAIN is not configured).
|
|
if (options.replyTo) {
|
|
mail.replyTo = options.replyTo;
|
|
}
|
|
return mail;
|
|
},
|
|
|
|
async sendEmail(options) {
|
|
return await Email.sendAsync(await EmailLocalization.prepareEmail(options));
|
|
}
|
|
};
|
|
|
|
// Add module.exports to make it accessible from other files
|
|
export default EmailLocalization;
|