Files
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

225 lines
6.2 KiB
TypeScript

import { AppEvents, Apps } from '@rocket.chat/apps';
import type { ISetting } from '@rocket.chat/core-typings';
import { Settings } from '@rocket.chat/models';
import { escapeHTML, validateEmail } from '@rocket.chat/tools';
import juice from 'juice';
import { Email } from 'meteor/email';
import { Meteor } from 'meteor/meteor';
import { stripHtml } from 'string-strip-html';
import _ from 'underscore';
import { replaceVariables } from './replaceVariables';
import { strLeft, strRightBack } from '../../../../lib/utils/stringUtils';
import { settings } from '../../../settings';
import { i18n } from '../../i18n';
import { notifyOnSettingChanged } from '../../notifyListener';
let contentHeader: string | undefined;
let contentFooter: string | undefined;
let body: string | undefined;
// define server language for email translations
// @TODO: change TAPi18n.__ function to use the server language by default
let lng = 'en';
settings.watch<string>('Language', (value) => {
lng = value || 'en';
});
export const replacekey = (str: string, key: string, value = ''): string =>
str.replace(new RegExp(`(\\[${key}\\]|__${key}__)`, 'igm'), value);
export const translate = (str: string): string => replaceVariables(str, (_match, key) => i18n.t(key, { lng }));
export const replace = (str: string, data: { [key: string]: unknown } = {}): string => {
if (!str) {
return '';
}
const options = {
Site_Name: settings.get<string>('Site_Name'),
Site_URL: settings.get<string>('Site_Url'),
Site_URL_Slash: settings.get<string>('Site_Url')?.replace(/\/?$/, '/'),
...(data.name
? {
fname: strLeft(String(data.name), ' '),
lname: strRightBack(String(data.name), ' '),
}
: {}),
...data,
};
return Object.entries(options).reduce((ret, [key, value]) => replacekey(ret, key, value), translate(str));
};
const nonEscapeKeys = ['room_path'];
export const replaceEscaped = (str: string, data: { [key: string]: unknown } = {}): string => {
const siteName = settings.get<string>('Site_Name');
const siteUrl = settings.get<string>('Site_Url');
return replace(str, {
Site_Name: siteName ? escapeHTML(siteName) : undefined,
Site_Url: siteUrl ? escapeHTML(siteUrl) : undefined,
...Object.entries(data).reduce<{ [key: string]: string }>((ret, [key, value]) => {
if (value !== undefined && value !== null) {
ret[key] = nonEscapeKeys.includes(key) ? String(value) : escapeHTML(String(value));
}
return ret;
}, {}),
});
};
export const wrap = (html: string, data: { [key: string]: unknown } = {}): string => {
if (settings.get('email_plain_text_only')) {
return replace(html, data);
}
if (!body) {
throw new Error('error-email-body-not-initialized');
}
return replaceEscaped(body.replace('{{body}}', html), data);
};
export const inlinecss = (html: string): string => {
const css = settings.get<string>('email_style');
return css ? juice.inlineContent(html, css) : html;
};
export const getTemplate = (template: ISetting['_id'], fn: (html: string) => void, escape = true): void => {
let html = '';
settings.watch<string>(template, (value) => {
html = value || '';
fn(escape ? inlinecss(html) : html);
});
settings.watch<string>('email_style', () => {
fn(escape ? inlinecss(html) : html);
});
};
export const getTemplateWrapped = (template: ISetting['_id'], fn: (html: string) => void): void => {
let html = '';
const wrapInlineCSS = _.debounce(() => fn(wrap(inlinecss(html))), 100);
settings.watch<string>('Email_Header', () => html && wrapInlineCSS());
settings.watch<string>('Email_Footer', () => html && wrapInlineCSS());
settings.watch<string>('email_style', () => html && wrapInlineCSS());
settings.watch<string>(template, (value) => {
html = value || '';
return html && wrapInlineCSS();
});
};
settings.watchMultiple(['Email_Header', 'Email_Footer'], () => {
getTemplate(
'Email_Header',
(value) => {
contentHeader = replace(value || '');
body = inlinecss(`${contentHeader} {{body}} ${contentFooter}`);
},
false,
);
getTemplate(
'Email_Footer',
(value) => {
contentFooter = replace(value || '');
body = inlinecss(`${contentHeader} {{body}} ${contentFooter}`);
},
false,
);
body = inlinecss(`${contentHeader} {{body}} ${contentFooter}`);
});
export const checkAddressFormat = (adresses: string | string[]): boolean =>
([] as string[]).concat(adresses).every((address) => validateEmail(address));
export const sendNoWrap = async ({
to,
from,
replyTo,
subject,
html,
text,
headers,
}: {
to: string | string[];
from: string;
replyTo?: string;
subject: string;
html?: string;
text?: string;
headers?: string;
}) => {
if (!checkAddressFormat(to)) {
throw new Meteor.Error('invalid email');
}
if (!text) {
text = html ? stripHtml(html).result : undefined;
}
if (settings.get('email_plain_text_only')) {
html = undefined;
}
const value = await Settings.incrementValueById('Triggered_Emails_Count', 1, { returnDocument: 'after' });
if (value) {
void notifyOnSettingChanged(value);
}
const email = { to, from, replyTo, subject, html, text, headers };
const eventResult = await Apps.self?.triggerEvent(AppEvents.IPreEmailSent, { email });
setImmediate(() => Email.sendAsync(eventResult || email).catch((e) => console.error(e)));
};
export const send = async ({
to,
from,
replyTo,
subject,
html,
text,
data,
headers,
}: {
to: string | string[];
from: string;
replyTo?: string;
subject: string;
html?: string;
text?: string;
headers?: string;
data?: { [key: string]: unknown };
}): Promise<void> =>
sendNoWrap({
to,
from,
replyTo,
subject: replace(subject, data),
text: (text && replace(text, data)) || (html && stripHtml(replace(html, data)).result) || undefined,
html: html ? wrap(html, data) : undefined,
headers,
});
// Needed because of https://github.com/microsoft/TypeScript/issues/36931
type Assert = (input: string, func: string) => asserts input;
export const checkAddressFormatAndThrow: Assert = (from: string, func: string): asserts from => {
if (checkAddressFormat(from)) {
return;
}
throw new Meteor.Error('error-invalid-from-address', 'Invalid from address', {
function: func,
});
};
export const getHeader = (): string | undefined => contentHeader;
export const getFooter = (): string | undefined => contentFooter;