feat(core): Switch to MJML for email templates (#10518)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-08-28 17:15:18 +02:00 committed by GitHub
parent 9e1dac0465
commit dbc10fe9f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 754 additions and 86 deletions

View file

@ -5,6 +5,7 @@
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"mjmlio.vscode-mjml",
"Vue.volar" "Vue.volar"
] ]
} }

View file

@ -49,19 +49,19 @@ class SmtpConfig {
export class TemplateConfig { export class TemplateConfig {
/** Overrides default HTML template for inviting new people (use full path) */ /** Overrides default HTML template for inviting new people (use full path) */
@Env('N8N_UM_EMAIL_TEMPLATES_INVITE') @Env('N8N_UM_EMAIL_TEMPLATES_INVITE')
invite: string = ''; 'user-invited': string = '';
/** Overrides default HTML template for resetting password (use full path) */ /** Overrides default HTML template for resetting password (use full path) */
@Env('N8N_UM_EMAIL_TEMPLATES_PWRESET') @Env('N8N_UM_EMAIL_TEMPLATES_PWRESET')
passwordReset: string = ''; 'password-reset-requested': string = '';
/** Overrides default HTML template for notifying that a workflow was shared (use full path) */ /** Overrides default HTML template for notifying that a workflow was shared (use full path) */
@Env('N8N_UM_EMAIL_TEMPLATES_WORKFLOW_SHARED') @Env('N8N_UM_EMAIL_TEMPLATES_WORKFLOW_SHARED')
workflowShared: string = ''; 'workflow-shared': string = '';
/** Overrides default HTML template for notifying that credentials were shared (use full path) */ /** Overrides default HTML template for notifying that credentials were shared (use full path) */
@Env('N8N_UM_EMAIL_TEMPLATES_CREDENTIALS_SHARED') @Env('N8N_UM_EMAIL_TEMPLATES_CREDENTIALS_SHARED')
credentialsShared: string = ''; 'credentials-shared': string = '';
} }
@Config @Config

View file

@ -89,10 +89,10 @@ describe('GlobalConfig', () => {
}, },
}, },
template: { template: {
credentialsShared: '', 'credentials-shared': '',
invite: '', 'user-invited': '',
passwordReset: '', 'password-reset-requested': '',
workflowShared: '', 'workflow-shared': '',
}, },
}, },
}, },

View file

@ -78,6 +78,7 @@
"chokidar": "^3.5.2", "chokidar": "^3.5.2",
"concurrently": "^8.2.0", "concurrently": "^8.2.0",
"ioredis-mock": "^8.8.1", "ioredis-mock": "^8.8.1",
"mjml": "^4.15.3",
"ts-essentials": "^7.0.3" "ts-essentials": "^7.0.3"
}, },
"dependencies": { "dependencies": {

View file

@ -3,6 +3,7 @@ import { writeFileSync } from 'fs';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import shell from 'shelljs'; import shell from 'shelljs';
import { rawTimeZones } from '@vvo/tzdb'; import { rawTimeZones } from '@vvo/tzdb';
import glob from 'fast-glob';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@ -13,7 +14,7 @@ const SPEC_THEME_FILENAME = 'swagger-theme.css';
const publicApiEnabled = process.env.N8N_PUBLIC_API_DISABLED !== 'true'; const publicApiEnabled = process.env.N8N_PUBLIC_API_DISABLED !== 'true';
copyUserManagementEmailTemplates(); generateUserManagementEmailTemplates();
generateTimezoneData(); generateTimezoneData();
if (publicApiEnabled) { if (publicApiEnabled) {
@ -21,13 +22,22 @@ if (publicApiEnabled) {
bundleOpenApiSpecs(); bundleOpenApiSpecs();
} }
function copyUserManagementEmailTemplates() { function generateUserManagementEmailTemplates() {
const templates = { const sourceDir = path.resolve(ROOT_DIR, 'src', 'user-management', 'email', 'templates');
source: path.resolve(ROOT_DIR, 'src', 'user-management', 'email', 'templates'), const destinationDir = path.resolve(ROOT_DIR, 'dist', 'user-management', 'email', 'templates');
destination: path.resolve(ROOT_DIR, 'dist', 'user-management', 'email'),
};
shell.cp('-r', templates.source, templates.destination); shell.mkdir('-p', destinationDir);
const templates = glob.sync('*.mjml', { cwd: sourceDir });
templates.forEach((template) => {
if (template.startsWith('_')) return;
const source = path.resolve(sourceDir, template);
const destination = path.resolve(destinationDir, template.replace(/\.mjml$/, '.handlebars'));
const command = `pnpm mjml --output ${destination} ${source}`;
shell.exec(command, { silent: false });
});
shell.cp(path.resolve(sourceDir, 'n8n-logo.png'), destinationDir);
} }
function copySwaggerTheme() { function copySwaggerTheme() {

View file

@ -13,7 +13,6 @@ import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { MfaService } from '@/mfa/mfa.service'; import { MfaService } from '@/mfa/mfa.service';
import { Logger } from '@/logger'; import { Logger } from '@/logger';
import { ExternalHooks } from '@/external-hooks'; import { ExternalHooks } from '@/external-hooks';
import { UrlService } from '@/services/url.service';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
@ -31,7 +30,6 @@ export class PasswordResetController {
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly userService: UserService, private readonly userService: UserService,
private readonly mfaService: MfaService, private readonly mfaService: MfaService,
private readonly urlService: UrlService,
private readonly license: License, private readonly license: License,
private readonly passwordUtility: PasswordUtility, private readonly passwordUtility: PasswordUtility,
private readonly userRepository: UserRepository, private readonly userRepository: UserRepository,
@ -108,14 +106,12 @@ export class PasswordResetController {
const url = this.authService.generatePasswordResetUrl(user); const url = this.authService.generatePasswordResetUrl(user);
const { id, firstName, lastName } = user; const { id, firstName } = user;
try { try {
await this.mailer.passwordReset({ await this.mailer.passwordReset({
email, email,
firstName, firstName,
lastName,
passwordResetUrl: url, passwordResetUrl: url,
domain: this.urlService.getInstanceBaseUrl(),
}); });
} catch (error) { } catch (error) {
this.eventService.emit('email-failed', { this.eventService.emit('email-failed', {

View file

@ -138,7 +138,6 @@ export class UserService {
const result = await this.mailer.invite({ const result = await this.mailer.invite({
email, email,
inviteAcceptUrl, inviteAcceptUrl,
domain,
}); });
if (result.emailSent) { if (result.emailSent) {
invitedUser.user.emailSent = true; invitedUser.user.emailSent = true;
@ -168,7 +167,6 @@ export class UserService {
this.logger.error('Failed to send email', { this.logger.error('Failed to send email', {
userId: owner.id, userId: owner.id,
inviteAcceptUrl, inviteAcceptUrl,
domain,
email, email,
}); });
invitedUser.error = e.message; invitedUser.error = e.message;

View file

@ -1,17 +1,12 @@
export type InviteEmailData = { export type InviteEmailData = {
email: string; email: string;
firstName?: string;
lastName?: string;
inviteAcceptUrl: string; inviteAcceptUrl: string;
domain: string;
}; };
export type PasswordResetData = { export type PasswordResetData = {
email: string; email: string;
firstName?: string; firstName: string;
lastName?: string;
passwordResetUrl: string; passwordResetUrl: string;
domain: string;
}; };
export type SendEmailResult = { export type SendEmailResult = {

View file

@ -1,6 +1,7 @@
import type { GlobalConfig } from '@n8n/config'; import type { GlobalConfig } from '@n8n/config';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import type { UrlService } from '@/services/url.service';
import type { InviteEmailData, PasswordResetData } from '@/user-management/email/Interfaces'; import type { InviteEmailData, PasswordResetData } from '@/user-management/email/Interfaces';
import { NodeMailer } from '@/user-management/email/node-mailer'; import { NodeMailer } from '@/user-management/email/node-mailer';
import { UserManagementMailer } from '@/user-management/email/user-management-mailer'; import { UserManagementMailer } from '@/user-management/email/user-management-mailer';
@ -31,7 +32,7 @@ describe('UserManagementMailer', () => {
}, },
}, },
}); });
const userManagementMailer = new UserManagementMailer(config, mock(), mock(), mock()); const userManagementMailer = new UserManagementMailer(config, mock(), mock(), mock(), mock());
it('should not setup email transport', async () => { it('should not setup email transport', async () => {
expect(userManagementMailer.isEmailSetUp).toBe(false); expect(userManagementMailer.isEmailSetUp).toBe(false);
@ -56,7 +57,18 @@ describe('UserManagementMailer', () => {
}, },
}, },
}); });
const userManagementMailer = new UserManagementMailer(config, mock(), mock(), mock()); const urlService = mock<UrlService>();
const userManagementMailer = new UserManagementMailer(
config,
mock(),
mock(),
urlService,
mock(),
);
beforeEach(() => {
urlService.getInstanceBaseUrl.mockReturnValue('https://n8n.url');
});
it('should setup email transport', async () => { it('should setup email transport', async () => {
expect(userManagementMailer.isEmailSetUp).toBe(true); expect(userManagementMailer.isEmailSetUp).toBe(true);
@ -67,9 +79,7 @@ describe('UserManagementMailer', () => {
const result = await userManagementMailer.invite(inviteEmailData); const result = await userManagementMailer.invite(inviteEmailData);
expect(result.emailSent).toBe(true); expect(result.emailSent).toBe(true);
expect(nodeMailer.sendMail).toHaveBeenCalledWith({ expect(nodeMailer.sendMail).toHaveBeenCalledWith({
body: expect.stringContaining( body: expect.stringContaining(`href="${inviteEmailData.inviteAcceptUrl}"`),
`<a href="${inviteEmailData.inviteAcceptUrl}" target="_blank">`,
),
emailRecipients: email, emailRecipients: email,
subject: 'You have been invited to n8n', subject: 'You have been invited to n8n',
}); });
@ -79,7 +89,7 @@ describe('UserManagementMailer', () => {
const result = await userManagementMailer.passwordReset(passwordResetData); const result = await userManagementMailer.passwordReset(passwordResetData);
expect(result.emailSent).toBe(true); expect(result.emailSent).toBe(true);
expect(nodeMailer.sendMail).toHaveBeenCalledWith({ expect(nodeMailer.sendMail).toHaveBeenCalledWith({
body: expect.stringContaining(`<a href="${passwordResetData.passwordResetUrl}">`), body: expect.stringContaining(`href="${passwordResetData.passwordResetUrl}"`),
emailRecipients: email, emailRecipients: email,
subject: 'n8n password reset', subject: 'n8n password reset',
}); });

View file

@ -1,4 +1,5 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import path from 'node:path';
import { pick } from 'lodash'; import { pick } from 'lodash';
import type { Transporter } from 'nodemailer'; import type { Transporter } from 'nodemailer';
import { createTransport } from 'nodemailer'; import { createTransport } from 'nodemailer';
@ -45,12 +46,20 @@ export class NodeMailer {
async sendMail(mailData: MailData): Promise<SendEmailResult> { async sendMail(mailData: MailData): Promise<SendEmailResult> {
try { try {
await this.transport?.sendMail({ await this.transport.sendMail({
from: this.sender, from: this.sender,
to: mailData.emailRecipients, to: mailData.emailRecipients,
subject: mailData.subject, subject: mailData.subject,
text: mailData.textOnly, text: mailData.textOnly,
html: mailData.body, html: mailData.body,
attachments: [
{
cid: 'n8n-logo',
filename: 'n8n-logo.png',
path: path.resolve(__dirname, 'templates/n8n-logo.png'),
contentDisposition: 'inline',
},
],
}); });
this.logger.debug( this.logger.debug(
`Email sent successfully to the following recipients: ${mailData.emailRecipients.toString()}`, `Email sent successfully to the following recipients: ${mailData.emailRecipients.toString()}`,

View file

@ -0,0 +1,25 @@
<mj-head>
<mj-attributes>
<mj-all font-family="Open Sans, sans-serif"></mj-all>
<mj-body background-color="#fbfcfe"></mj-body>
<mj-text
font-weight="400"
font-size="16px"
color="#444444"
line-height="24px"
padding="10px 0 0 0"
align="center"
></mj-text>
<mj-button
background-color="#ff6f5c"
color="#ffffff"
font-size="18px"
font-weight="600"
align="center"
padding-top="20px"
line-height="24px"
border-radius="4px"
></mj-button>
<mj-section padding="20px 0px"></mj-section>
</mj-attributes>
</mj-head>

View file

@ -0,0 +1,5 @@
<mj-section>
<mj-column>
<mj-image src="cid:n8n-logo" height="40px" width="70px" />
</mj-column>
</mj-section>

View file

@ -0,0 +1,18 @@
<mjml>
<mj-include path="./_common.mjml" />
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="24px" color="#ff6f5c">A credential has been shared with you</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF" border="1px solid #ddd">
<mj-column>
<mj-text><b>"{{ credentialsName }}"</b> credential has been shared with you.</mj-text>
<mj-text>To access it, please click the button below.</mj-text>
<mj-button href="{{credentialsListUrl}}">Open credential</mj-button>
</mj-column>
</mj-section>
<mj-include path="./_logo.mjml" />
</mj-body>
</mjml>

View file

@ -1,4 +0,0 @@
<p>Hi there,</p>
<p><b>"{{ credentialsName }}" credential</b> has been shared with you.</p>
<p>To view all the credentials you have access to within n8n, click the following link:</p>
<p><a href="{{ credentialsListUrl }}" target="_blank">{{ credentialsListUrl }}</a></p>

View file

@ -1,5 +0,0 @@
<h1>Hi there!</h1>
<p>Welcome to n8n, {{firstName}} {{lastName}}</p>
<p>Your instance is set up!</p>
<p>Use your email to login: {{email}} and the chosen password.</p>
<p>Have fun automating!</p>

View file

@ -1,4 +0,0 @@
<p>Hi there,</p>
<p>You have been invited to join n8n ({{ domain }}).</p>
<p>To accept, click the following link:</p>
<p><a href="{{ inviteAcceptUrl }}" target="_blank">{{ inviteAcceptUrl }}</a></p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,31 @@
<mjml>
<mj-include path="./_common.mjml" />
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="24px" color="#ff6f5c">Reset your n8n password</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF" border="1px solid #ddd">
<mj-column>
<mj-text font-size="20px">Hi {{firstName}},</mj-text>
<mj-text>Somebody asked to reset your password on n8n at <b>{{domain}}</b> .</mj-text>
<mj-text> Click the following link to choose a new password. </mj-text>
<mj-button href="{{passwordResetUrl}}">Set a new password</mj-button>
<mj-text font-size="14px">
The link is only valid for 20 minutes since this email was sent.
</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column>
<mj-text font-size="12px" color="#777">
If you did not request this email, you can safely ignore this. <br />
Your password will not be changed.
</mj-text>
</mj-column>
</mj-section>
<mj-include path="./_logo.mjml" />
</mj-body>
</mjml>

View file

@ -1,5 +0,0 @@
<p>Hi {{firstName}},</p>
<p>Somebody asked to reset your password on n8n ({{ domain }}).</p>
<br />
<p>Click the following link to choose a new password. The link is valid for 20 minutes.</p>
<a href="{{ passwordResetUrl }}">{{ passwordResetUrl }}</a>

View file

@ -0,0 +1,18 @@
<mjml>
<mj-include path="./_common.mjml" />
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="24px" color="#ff6f5c">Welcome to n8n! 🎉</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF" border="1px solid #ddd">
<mj-column>
<mj-text>You have been invited to join n8n at <b>{{domain}}</b> .</mj-text>
<mj-text>To accept, please click the button below.</mj-text>
<mj-button href="{{inviteAcceptUrl}}">Set up your n8n account</mj-button>
</mj-column>
</mj-section>
<mj-include path="./_logo.mjml" />
</mj-body>
</mjml>

View file

@ -0,0 +1,18 @@
<mjml>
<mj-include path="./_common.mjml" />
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="24px" color="#ff6f5c">A workflow has been shared with you</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF" border="1px solid #ddd">
<mj-column>
<mj-text><b>"{{ workflowName }}"</b> workflow has been shared with you.</mj-text>
<mj-text>To access it, please click the button below.</mj-text>
<mj-button href="{{workflowUrl}}">Open Workflow</mj-button>
</mj-column>
</mj-section>
<mj-include path="./_logo.mjml" />
</mj-body>
</mjml>

View file

@ -1,4 +0,0 @@
<p>Hi there,</p>
<p><b>"{{ workflowName }}" workflow</b> has been shared with you.</p>
<p>To access the workflow, click the following link:</p>
<p><a href="{{ workflowUrl }}" target="_blank">{{ workflowUrl }}</a></p>

View file

@ -8,6 +8,7 @@ import { GlobalConfig } from '@n8n/config';
import type { User } from '@/databases/entities/User'; import type { User } from '@/databases/entities/User';
import type { WorkflowEntity } from '@/databases/entities/workflow-entity'; import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { EventService } from '@/events/event.service';
import { Logger } from '@/logger'; import { Logger } from '@/logger';
import { UrlService } from '@/services/url.service'; import { UrlService } from '@/services/url.service';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';
@ -15,10 +16,14 @@ import { toError } from '@/utils';
import type { InviteEmailData, PasswordResetData, SendEmailResult } from './Interfaces'; import type { InviteEmailData, PasswordResetData, SendEmailResult } from './Interfaces';
import { NodeMailer } from './node-mailer'; import { NodeMailer } from './node-mailer';
import { EventService } from '@/events/event.service'; import { inTest } from '@/constants';
type Template = HandlebarsTemplateDelegate<unknown>; type Template = HandlebarsTemplateDelegate<unknown>;
type TemplateName = 'invite' | 'passwordReset' | 'workflowShared' | 'credentialsShared'; type TemplateName =
| 'user-invited'
| 'password-reset-requested'
| 'workflow-shared'
| 'credentials-shared';
@Service() @Service()
export class UserManagementMailer { export class UserManagementMailer {
@ -35,6 +40,7 @@ export class UserManagementMailer {
private readonly logger: Logger, private readonly logger: Logger,
private readonly userRepository: UserRepository, private readonly userRepository: UserRepository,
private readonly urlService: UrlService, private readonly urlService: UrlService,
private readonly eventService: EventService,
) { ) {
const emailsConfig = globalConfig.userManagement.emails; const emailsConfig = globalConfig.userManagement.emails;
this.isEmailSetUp = emailsConfig.mode === 'smtp' && emailsConfig.smtp.host !== ''; this.isEmailSetUp = emailsConfig.mode === 'smtp' && emailsConfig.smtp.host !== '';
@ -49,31 +55,23 @@ export class UserManagementMailer {
async invite(inviteEmailData: InviteEmailData): Promise<SendEmailResult> { async invite(inviteEmailData: InviteEmailData): Promise<SendEmailResult> {
if (!this.mailer) return { emailSent: false }; if (!this.mailer) return { emailSent: false };
const template = await this.getTemplate('invite'); const template = await this.getTemplate('user-invited');
const result = await this.mailer.sendMail({ return await this.mailer.sendMail({
emailRecipients: inviteEmailData.email, emailRecipients: inviteEmailData.email,
subject: 'You have been invited to n8n', subject: 'You have been invited to n8n',
body: template(inviteEmailData), body: template({ ...this.basePayload, ...inviteEmailData }),
}); });
// If mailer does not exist it means mail has been disabled.
// No error, just say no email was sent.
return result ?? { emailSent: false };
} }
async passwordReset(passwordResetData: PasswordResetData): Promise<SendEmailResult> { async passwordReset(passwordResetData: PasswordResetData): Promise<SendEmailResult> {
if (!this.mailer) return { emailSent: false }; if (!this.mailer) return { emailSent: false };
const template = await this.getTemplate('passwordReset', 'passwordReset.html'); const template = await this.getTemplate('password-reset-requested');
const result = await this.mailer.sendMail({ return await this.mailer.sendMail({
emailRecipients: passwordResetData.email, emailRecipients: passwordResetData.email,
subject: 'n8n password reset', subject: 'n8n password reset',
body: template(passwordResetData), body: template({ ...this.basePayload, ...passwordResetData }),
}); });
// If mailer does not exist it means mail has been disabled.
// No error, just say no email was sent.
return result ?? { emailSent: false };
} }
async notifyWorkflowShared({ async notifyWorkflowShared({
@ -93,7 +91,7 @@ export class UserManagementMailer {
const emailRecipients = recipients.map(({ email }) => email); const emailRecipients = recipients.map(({ email }) => email);
const populateTemplate = await this.getTemplate('workflowShared', 'workflowShared.html'); const populateTemplate = await this.getTemplate('workflow-shared');
const baseUrl = this.urlService.getInstanceBaseUrl(); const baseUrl = this.urlService.getInstanceBaseUrl();
@ -111,7 +109,7 @@ export class UserManagementMailer {
this.logger.info('Sent workflow shared email successfully', { sharerId: sharer.id }); this.logger.info('Sent workflow shared email successfully', { sharerId: sharer.id });
Container.get(EventService).emit('user-transactional-email-sent', { this.eventService.emit('user-transactional-email-sent', {
userId: sharer.id, userId: sharer.id,
messageType: 'Workflow shared', messageType: 'Workflow shared',
publicApi: false, publicApi: false,
@ -119,7 +117,7 @@ export class UserManagementMailer {
return result; return result;
} catch (e) { } catch (e) {
Container.get(EventService).emit('email-failed', { this.eventService.emit('email-failed', {
user: sharer, user: sharer,
messageType: 'Workflow shared', messageType: 'Workflow shared',
publicApi: false, publicApi: false,
@ -148,7 +146,7 @@ export class UserManagementMailer {
const emailRecipients = recipients.map(({ email }) => email); const emailRecipients = recipients.map(({ email }) => email);
const populateTemplate = await this.getTemplate('credentialsShared', 'credentialsShared.html'); const populateTemplate = await this.getTemplate('credentials-shared');
const baseUrl = this.urlService.getInstanceBaseUrl(); const baseUrl = this.urlService.getInstanceBaseUrl();
@ -166,7 +164,7 @@ export class UserManagementMailer {
this.logger.info('Sent credentials shared email successfully', { sharerId: sharer.id }); this.logger.info('Sent credentials shared email successfully', { sharerId: sharer.id });
Container.get(EventService).emit('user-transactional-email-sent', { this.eventService.emit('user-transactional-email-sent', {
userId: sharer.id, userId: sharer.id,
messageType: 'Credentials shared', messageType: 'Credentials shared',
publicApi: false, publicApi: false,
@ -174,7 +172,7 @@ export class UserManagementMailer {
return result; return result;
} catch (e) { } catch (e) {
Container.get(EventService).emit('email-failed', { this.eventService.emit('email-failed', {
user: sharer, user: sharer,
messageType: 'Credentials shared', messageType: 'Credentials shared',
publicApi: false, publicApi: false,
@ -186,21 +184,25 @@ export class UserManagementMailer {
} }
} }
async getTemplate( async getTemplate(templateName: TemplateName): Promise<Template> {
templateName: TemplateName,
defaultFilename = `${templateName}.html`,
): Promise<Template> {
let template = this.templatesCache[templateName]; let template = this.templatesCache[templateName];
if (!template) { if (!template) {
const fileExtension = inTest ? 'mjml' : 'handlebars';
const templateOverride = this.templateOverrides[templateName]; const templateOverride = this.templateOverrides[templateName];
const templatePath = const templatePath =
templateOverride && existsSync(templateOverride) templateOverride && existsSync(templateOverride)
? templateOverride ? templateOverride
: pathJoin(__dirname, `templates/${defaultFilename}`); : pathJoin(__dirname, `templates/${templateName}.${fileExtension}`);
const markup = await readFile(templatePath, 'utf-8'); const markup = await readFile(templatePath, 'utf-8');
template = Handlebars.compile(markup); template = Handlebars.compile(markup);
this.templatesCache[templateName] = template; this.templatesCache[templateName] = template;
} }
return template; return template;
} }
private get basePayload() {
const baseUrl = this.urlService.getInstanceBaseUrl();
const domain = new URL(baseUrl).hostname;
return { baseUrl, domain };
}
} }

View file

@ -1005,6 +1005,9 @@ importers:
ioredis-mock: ioredis-mock:
specifier: ^8.8.1 specifier: ^8.8.1
version: 8.8.1(@types/ioredis-mock@8.2.2)(ioredis@5.3.2) version: 8.8.1(@types/ioredis-mock@8.2.2)(ioredis@5.3.2)
mjml:
specifier: ^4.15.3
version: 4.15.3(encoding@0.1.13)
ts-essentials: ts-essentials:
specifier: ^7.0.3 specifier: ^7.0.3
version: 7.0.3(typescript@5.5.2) version: 7.0.3(typescript@5.5.2)
@ -6641,6 +6644,9 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
camel-case@3.0.0:
resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==}
camel-case@4.1.2: camel-case@4.1.2:
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
@ -6771,6 +6777,10 @@ packages:
classnames@2.5.1: classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
clean-css@4.2.4:
resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==}
engines: {node: '>= 4.0'}
clean-regexp@1.0.0: clean-regexp@1.0.0:
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -7380,6 +7390,9 @@ packages:
detect-node-es@1.1.0: detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
detect-node@2.1.0:
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
detect-package-manager@2.0.1: detect-package-manager@2.0.1:
resolution: {integrity: sha512-j/lJHyoLlWi6G1LDdLgvUtz60Zo5GEj+sVYtTVXnYLDPuzgC3llMxonXym9zIwhhUII8vjdw0LXxavpLqTbl1A==} resolution: {integrity: sha512-j/lJHyoLlWi6G1LDdLgvUtz60Zo5GEj+sVYtTVXnYLDPuzgC3llMxonXym9zIwhhUII8vjdw0LXxavpLqTbl1A==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -7450,6 +7463,10 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
deprecated: Use your platform's native DOMException instead deprecated: Use your platform's native DOMException instead
domhandler@3.3.0:
resolution: {integrity: sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==}
engines: {node: '>= 4'}
domhandler@4.3.1: domhandler@4.3.1:
resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
@ -7471,6 +7488,9 @@ packages:
domutils@3.0.1: domutils@3.0.1:
resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==} resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==}
domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
dot-case@3.0.4: dot-case@3.0.4:
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
@ -7668,6 +7688,10 @@ packages:
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
engines: {node: '>=6'} engines: {node: '>=6'}
escape-goat@3.0.0:
resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==}
engines: {node: '>=10'}
escape-html@1.0.3: escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
@ -8590,16 +8614,27 @@ packages:
html-escaper@2.0.2: html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
html-minifier@4.0.0:
resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==}
engines: {node: '>=6'}
hasBin: true
html-to-text@9.0.5: html-to-text@9.0.5:
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
engines: {node: '>=14'} engines: {node: '>=14'}
htmlparser2@5.0.1:
resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==}
htmlparser2@6.1.0: htmlparser2@6.1.0:
resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==}
htmlparser2@8.0.2: htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
htmlparser2@9.1.0:
resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==}
http-cache-semantics@4.1.1: http-cache-semantics@4.1.1:
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
@ -9390,6 +9425,11 @@ packages:
jszip@3.10.1: jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
juice@10.0.1:
resolution: {integrity: sha512-ZhJT1soxJCkOiO55/mz8yeBKTAJhRzX9WBO+16ZTqNTONnnVlUPyVBIzQ7lDRjaBdTbid+bAnyIon/GM3yp4cA==}
engines: {node: '>=10.0.0'}
hasBin: true
jwa@1.4.1: jwa@1.4.1:
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
@ -9830,6 +9870,9 @@ packages:
loupe@2.3.7: loupe@2.3.7:
resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
lower-case@1.1.4:
resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==}
lower-case@2.0.2: lower-case@2.0.2:
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
@ -9976,6 +10019,9 @@ packages:
memory-pager@1.5.0: memory-pager@1.5.0:
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
mensch@0.3.4:
resolution: {integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==}
merge-descriptors@1.0.1: merge-descriptors@1.0.1:
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
@ -10112,6 +10158,105 @@ packages:
resolution: {integrity: sha512-MGZAq0Q3OuRYgZKvlB69z4gLN4G3PvgC4A2zhkCXCXrLD5wm2cCnwNB59xOBVA+srZ0zEes6u+VylcPIkB4SqA==} resolution: {integrity: sha512-MGZAq0Q3OuRYgZKvlB69z4gLN4G3PvgC4A2zhkCXCXrLD5wm2cCnwNB59xOBVA+srZ0zEes6u+VylcPIkB4SqA==}
engines: {node: 6.* || 8.* || >= 10.*} engines: {node: 6.* || 8.* || >= 10.*}
mjml-accordion@4.15.3:
resolution: {integrity: sha512-LPNVSj1LyUVYT9G1gWwSw3GSuDzDsQCu0tPB2uDsq4VesYNnU6v3iLCQidMiR6azmIt13OEozG700ygAUuA6Ng==}
mjml-body@4.15.3:
resolution: {integrity: sha512-7pfUOVPtmb0wC+oUOn4xBsAw4eT5DyD6xqaxj/kssu6RrFXOXgJaVnDPAI9AzIvXJ/5as9QrqRGYAddehwWpHQ==}
mjml-button@4.15.3:
resolution: {integrity: sha512-79qwn9AgdGjJR1vLnrcm2rq2AsAZkKC5JPwffTMG+Nja6zGYpTDZFZ56ekHWr/r1b5WxkukcPj2PdevUug8c+Q==}
mjml-carousel@4.15.3:
resolution: {integrity: sha512-3ju6I4l7uUhPRrJfN3yK9AMsfHvrYbRkcJ1GRphFHzUj37B2J6qJOQUpzA547Y4aeh69TSb7HFVf1t12ejQxVw==}
mjml-cli@4.15.3:
resolution: {integrity: sha512-+V2TDw3tXUVEptFvLSerz125C2ogYl8klIBRY1m5BHd4JvGVf3yhx8N3PngByCzA6PGcv/eydGQN+wy34SHf0Q==}
hasBin: true
mjml-column@4.15.3:
resolution: {integrity: sha512-hYdEFdJGHPbZJSEysykrevEbB07yhJGSwfDZEYDSbhQQFjV2tXrEgYcFD5EneMaowjb55e3divSJxU4c5q4Qgw==}
mjml-core@4.15.3:
resolution: {integrity: sha512-Dmwk+2cgSD9L9GmTbEUNd8QxkTZtW9P7FN/ROZW/fGZD6Hq6/4TB0zEspg2Ow9eYjZXO2ofOJ3PaQEEShKV0kQ==}
mjml-divider@4.15.3:
resolution: {integrity: sha512-vh27LQ9FG/01y0b9ntfqm+GT5AjJnDSDY9hilss2ixIUh0FemvfGRfsGVeV5UBVPBKK7Ffhvfqc7Rciob9Spzw==}
mjml-group@4.15.3:
resolution: {integrity: sha512-HSu/rKnGZVKFq3ciT46vi1EOy+9mkB0HewO4+P6dP/Y0UerWkN6S3UK11Cxsj0cAp0vFwkPDCdOeEzRdpFEkzA==}
mjml-head-attributes@4.15.3:
resolution: {integrity: sha512-2ISo0r5ZKwkrvJgDou9xVPxxtXMaETe2AsAA02L89LnbB2KC0N5myNsHV0sEysTw9+CfCmgjAb0GAI5QGpxKkQ==}
mjml-head-breakpoint@4.15.3:
resolution: {integrity: sha512-Eo56FA5C2v6ucmWQL/JBJ2z641pLOom4k0wP6CMZI2utfyiJ+e2Uuinj1KTrgDcEvW4EtU9HrfAqLK9UosLZlg==}
mjml-head-font@4.15.3:
resolution: {integrity: sha512-CzV2aDPpiNIIgGPHNcBhgyedKY4SX3BJoTwOobSwZVIlEA6TAWB4Z9WwFUmQqZOgo1AkkiTHPZQvGcEhFFXH6g==}
mjml-head-html-attributes@4.15.3:
resolution: {integrity: sha512-MDNDPMBOgXUZYdxhosyrA2kudiGO8aogT0/cODyi2Ed9o/1S7W+je11JUYskQbncqhWKGxNyaP4VWa+6+vUC/g==}
mjml-head-preview@4.15.3:
resolution: {integrity: sha512-J2PxCefUVeFwsAExhrKo4lwxDevc5aKj888HBl/wN4EuWOoOg06iOGCxz4Omd8dqyFsrqvbBuPqRzQ+VycGmaA==}
mjml-head-style@4.15.3:
resolution: {integrity: sha512-9J+JuH+mKrQU65CaJ4KZegACUgNIlYmWQYx3VOBR/tyz+8kDYX7xBhKJCjQ1I4wj2Tvga3bykd89Oc2kFZ5WOw==}
mjml-head-title@4.15.3:
resolution: {integrity: sha512-IM59xRtsxID4DubQ0iLmoCGXguEe+9BFG4z6y2xQDrscIa4QY3KlfqgKGT69ojW+AVbXXJPEVqrAi4/eCsLItQ==}
mjml-head@4.15.3:
resolution: {integrity: sha512-o3mRuuP/MB5fZycjD3KH/uXsnaPl7Oo8GtdbJTKtH1+O/3pz8GzGMkscTKa97l03DAG2EhGrzzLcU2A6eshwFw==}
mjml-hero@4.15.3:
resolution: {integrity: sha512-9cLAPuc69yiuzNrMZIN58j+HMK1UWPaq2i3/Fg2ZpimfcGFKRcPGCbEVh0v+Pb6/J0+kf8yIO0leH20opu3AyQ==}
mjml-image@4.15.3:
resolution: {integrity: sha512-g1OhSdofIytE9qaOGdTPmRIp7JsCtgO0zbsn1Fk6wQh2gEL55Z40j/VoghslWAWTgT2OHFdBKnMvWtN6U5+d2Q==}
mjml-migrate@4.15.3:
resolution: {integrity: sha512-sr/+35RdxZroNQVegjpfRHJ5hda9XCgaS4mK2FGO+Mb1IUevKfeEPII3F/cHDpNwFeYH3kAgyqQ22ClhGLWNBA==}
hasBin: true
mjml-navbar@4.15.3:
resolution: {integrity: sha512-VsKH/Jdlf8Yu3y7GpzQV5n7JMdpqvZvTSpF6UQXL0PWOm7k6+LX+sCZimOfpHJ+wCaaybpxokjWZ71mxOoCWoA==}
mjml-parser-xml@4.15.3:
resolution: {integrity: sha512-Tz0UX8/JVYICLjT+U8J1f/TFxIYVYjzZHeh4/Oyta0pLpRLeZlxEd71f3u3kdnulCKMP4i37pFRDmyLXAlEuLw==}
mjml-preset-core@4.15.3:
resolution: {integrity: sha512-1zZS8P4O0KweWUqNS655+oNnVMPQ1Rq1GaZq5S9JfwT1Vh/m516lSmiTW9oko6gGHytt5s6Yj6oOeu5Zm8FoLw==}
mjml-raw@4.15.3:
resolution: {integrity: sha512-IGyHheOYyRchBLiAEgw3UM11kFNmBSMupu2BDdejC6ZiDhEAdG+tyERlsCwDPYtXanvFpGWULIu3XlsUPc+RZw==}
mjml-section@4.15.3:
resolution: {integrity: sha512-JfVPRXH++Hd933gmQfG8JXXCBCR6fIzC3DwiYycvanL/aW1cEQ2EnebUfQkt5QzlYjOkJEH+JpccAsq3ln6FZQ==}
mjml-social@4.15.3:
resolution: {integrity: sha512-7sD5FXrESOxpT9Z4Oh36bS6u/geuUrMP1aCg2sjyAwbPcF1aWa2k9OcatQfpRf6pJEhUZ18y6/WBBXmMVmSzXg==}
mjml-spacer@4.15.3:
resolution: {integrity: sha512-3B7Qj+17EgDdAtZ3NAdMyOwLTX1jfmJuY7gjyhS2HtcZAmppW+cxqHUBwCKfvSRgTQiccmEvtNxaQK+tfyrZqA==}
mjml-table@4.15.3:
resolution: {integrity: sha512-FLx7DcRKTdKdcOCbMyBaeudeHaHpwPveRrBm6WyQe3LXx6FfdmOh59i71/16LFQMgBOD3N4/UJkzxLzlTJzMqQ==}
mjml-text@4.15.3:
resolution: {integrity: sha512-+C0hxCmw9kg0XzT6vhE5mFkK6y225nC8UEQcN94K0fBCjPKkM+HqZMwGX205fzdGRi+Bxa55b/VhrIVwdv+8vw==}
mjml-validator@4.15.3:
resolution: {integrity: sha512-Xb72KdqRwjv/qM2rJpV22syyP2N3cRQ9VVDrN6u2FSzLq02buFNxmSPJ7CKhat3PrUNdVHU75KZwOf/tz4UEhA==}
mjml-wrapper@4.15.3:
resolution: {integrity: sha512-ditsCijeHJrmBmObtJmQ18ddLxv5oPyMTdPU8Di8APOnD2zPk7Z4UAuJSl7HXB45oFiivr3MJf4koFzMUSZ6Gg==}
mjml@4.15.3:
resolution: {integrity: sha512-bW2WpJxm6HS+S3Yu6tq1DUPFoTxU9sPviUSmnL7Ua+oVO3WA5ILFWqvujUlz+oeuM+HCwEyMiP5xvKNPENVjYA==}
hasBin: true
mkdirp-classic@0.5.3: mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
@ -10312,6 +10457,9 @@ packages:
neo-async@2.6.2: neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
no-case@2.3.2:
resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==}
no-case@3.0.4: no-case@3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
@ -10690,6 +10838,9 @@ packages:
pako@1.0.11: pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
param-case@2.1.1:
resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==}
param-case@3.0.4: param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
@ -11483,6 +11634,10 @@ packages:
reinterval@1.1.0: reinterval@1.1.0:
resolution: {integrity: sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==} resolution: {integrity: sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==}
relateurl@0.2.7:
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
engines: {node: '>= 0.10'}
remove-trailing-slash@0.1.1: remove-trailing-slash@0.1.1:
resolution: {integrity: sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA==} resolution: {integrity: sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA==}
@ -11831,6 +11986,9 @@ packages:
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
slick@1.12.2:
resolution: {integrity: sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==}
slugify@1.4.7: slugify@1.4.7:
resolution: {integrity: sha512-tf+h5W1IrjNm/9rKKj0JU2MDMruiopx0jjVA5zCdBtcGjfp0+c5rHw/zADLC3IeKlGHtVbHtpfzvYA0OYT+HKg==} resolution: {integrity: sha512-tf+h5W1IrjNm/9rKKj0JU2MDMruiopx0jjVA5zCdBtcGjfp0+c5rHw/zADLC3IeKlGHtVbHtpfzvYA0OYT+HKg==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
@ -12761,6 +12919,9 @@ packages:
upper-case-first@2.0.2: upper-case-first@2.0.2:
resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==}
upper-case@1.1.3:
resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==}
upper-case@2.0.2: upper-case@2.0.2:
resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==}
@ -12844,6 +13005,10 @@ packages:
resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==}
engines: {node: '>=10.12.0'} engines: {node: '>=10.12.0'}
valid-data-url@3.0.1:
resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==}
engines: {node: '>=10'}
validate-npm-package-license@3.0.4: validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
@ -13080,6 +13245,10 @@ packages:
wcwidth@1.0.1: wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
web-resource-inliner@6.0.1:
resolution: {integrity: sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==}
engines: {node: '>=10.0.0'}
web-streams-polyfill@3.2.1: web-streams-polyfill@3.2.1:
resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -20131,6 +20300,11 @@ snapshots:
callsites@3.1.0: {} callsites@3.1.0: {}
camel-case@3.0.0:
dependencies:
no-case: 2.3.2
upper-case: 1.1.3
camel-case@4.1.2: camel-case@4.1.2:
dependencies: dependencies:
pascal-case: 3.1.2 pascal-case: 3.1.2
@ -20287,6 +20461,10 @@ snapshots:
classnames@2.5.1: {} classnames@2.5.1: {}
clean-css@4.2.4:
dependencies:
source-map: 0.6.1
clean-regexp@1.0.0: clean-regexp@1.0.0:
dependencies: dependencies:
escape-string-regexp: 1.0.5 escape-string-regexp: 1.0.5
@ -20947,6 +21125,8 @@ snapshots:
detect-node-es@1.1.0: {} detect-node-es@1.1.0: {}
detect-node@2.1.0: {}
detect-package-manager@2.0.1: detect-package-manager@2.0.1:
dependencies: dependencies:
execa: 5.1.1 execa: 5.1.1
@ -21017,6 +21197,10 @@ snapshots:
dependencies: dependencies:
webidl-conversions: 7.0.0 webidl-conversions: 7.0.0
domhandler@3.3.0:
dependencies:
domelementtype: 2.3.0
domhandler@4.3.1: domhandler@4.3.1:
dependencies: dependencies:
domelementtype: 2.3.0 domelementtype: 2.3.0
@ -21041,6 +21225,12 @@ snapshots:
domelementtype: 2.3.0 domelementtype: 2.3.0
domhandler: 5.0.3 domhandler: 5.0.3
domutils@3.1.0:
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dot-case@3.0.4: dot-case@3.0.4:
dependencies: dependencies:
no-case: 3.0.4 no-case: 3.0.4
@ -21369,6 +21559,8 @@ snapshots:
escalade@3.1.1: {} escalade@3.1.1: {}
escape-goat@3.0.0: {}
escape-html@1.0.3: {} escape-html@1.0.3: {}
escape-string-regexp@1.0.5: {} escape-string-regexp@1.0.5: {}
@ -22537,6 +22729,16 @@ snapshots:
html-escaper@2.0.2: {} html-escaper@2.0.2: {}
html-minifier@4.0.0:
dependencies:
camel-case: 3.0.0
clean-css: 4.2.4
commander: 2.20.3
he: 1.2.0
param-case: 2.1.1
relateurl: 0.2.7
uglify-js: 3.17.4
html-to-text@9.0.5: html-to-text@9.0.5:
dependencies: dependencies:
'@selderee/plugin-htmlparser2': 0.11.0 '@selderee/plugin-htmlparser2': 0.11.0
@ -22545,6 +22747,13 @@ snapshots:
htmlparser2: 8.0.2 htmlparser2: 8.0.2
selderee: 0.11.0 selderee: 0.11.0
htmlparser2@5.0.1:
dependencies:
domelementtype: 2.3.0
domhandler: 3.3.0
domutils: 2.8.0
entities: 2.2.0
htmlparser2@6.1.0: htmlparser2@6.1.0:
dependencies: dependencies:
domelementtype: 2.3.0 domelementtype: 2.3.0
@ -22559,6 +22768,13 @@ snapshots:
domutils: 3.0.1 domutils: 3.0.1
entities: 4.5.0 entities: 4.5.0
htmlparser2@9.1.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.1.0
entities: 4.5.0
http-cache-semantics@4.1.1: http-cache-semantics@4.1.1:
optional: true optional: true
@ -23621,6 +23837,16 @@ snapshots:
readable-stream: 2.3.7 readable-stream: 2.3.7
setimmediate: 1.0.5 setimmediate: 1.0.5
juice@10.0.1(encoding@0.1.13):
dependencies:
cheerio: 1.0.0-rc.12
commander: 6.2.1
mensch: 0.3.4
slick: 1.12.2
web-resource-inliner: 6.0.1(encoding@0.1.13)
transitivePeerDependencies:
- encoding
jwa@1.4.1: jwa@1.4.1:
dependencies: dependencies:
buffer-equal-constant-time: 1.0.1 buffer-equal-constant-time: 1.0.1
@ -23983,6 +24209,8 @@ snapshots:
dependencies: dependencies:
get-func-name: 2.0.2 get-func-name: 2.0.2
lower-case@1.1.4: {}
lower-case@2.0.2: lower-case@2.0.2:
dependencies: dependencies:
tslib: 2.6.2 tslib: 2.6.2
@ -24153,6 +24381,8 @@ snapshots:
memory-pager@1.5.0: {} memory-pager@1.5.0: {}
mensch@0.3.4: {}
merge-descriptors@1.0.1: {} merge-descriptors@1.0.1: {}
merge-stream@2.0.0: {} merge-stream@2.0.0: {}
@ -24275,6 +24505,303 @@ snapshots:
lodash: 4.17.21 lodash: 4.17.21
pretender: 3.4.7 pretender: 3.4.7
mjml-accordion@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-body@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-button@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-carousel@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-cli@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
chokidar: 3.5.2
glob: 10.3.10
html-minifier: 4.0.0
js-beautify: 1.14.9
lodash: 4.17.21
minimatch: 9.0.5
mjml-core: 4.15.3(encoding@0.1.13)
mjml-migrate: 4.15.3(encoding@0.1.13)
mjml-parser-xml: 4.15.3
mjml-validator: 4.15.3
yargs: 17.7.2
transitivePeerDependencies:
- encoding
mjml-column@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-core@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
cheerio: 1.0.0-rc.12
detect-node: 2.1.0
html-minifier: 4.0.0
js-beautify: 1.14.9
juice: 10.0.1(encoding@0.1.13)
lodash: 4.17.21
mjml-migrate: 4.15.3(encoding@0.1.13)
mjml-parser-xml: 4.15.3
mjml-validator: 4.15.3
transitivePeerDependencies:
- encoding
mjml-divider@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-group@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-head-attributes@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-head-breakpoint@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-head-font@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-head-html-attributes@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-head-preview@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-head-style@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-head-title@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-head@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-hero@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-image@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-migrate@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
js-beautify: 1.14.9
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
mjml-parser-xml: 4.15.3
yargs: 17.7.2
transitivePeerDependencies:
- encoding
mjml-navbar@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-parser-xml@4.15.3:
dependencies:
'@babel/runtime': 7.24.7
detect-node: 2.1.0
htmlparser2: 9.1.0
lodash: 4.17.21
mjml-preset-core@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
mjml-accordion: 4.15.3(encoding@0.1.13)
mjml-body: 4.15.3(encoding@0.1.13)
mjml-button: 4.15.3(encoding@0.1.13)
mjml-carousel: 4.15.3(encoding@0.1.13)
mjml-column: 4.15.3(encoding@0.1.13)
mjml-divider: 4.15.3(encoding@0.1.13)
mjml-group: 4.15.3(encoding@0.1.13)
mjml-head: 4.15.3(encoding@0.1.13)
mjml-head-attributes: 4.15.3(encoding@0.1.13)
mjml-head-breakpoint: 4.15.3(encoding@0.1.13)
mjml-head-font: 4.15.3(encoding@0.1.13)
mjml-head-html-attributes: 4.15.3(encoding@0.1.13)
mjml-head-preview: 4.15.3(encoding@0.1.13)
mjml-head-style: 4.15.3(encoding@0.1.13)
mjml-head-title: 4.15.3(encoding@0.1.13)
mjml-hero: 4.15.3(encoding@0.1.13)
mjml-image: 4.15.3(encoding@0.1.13)
mjml-navbar: 4.15.3(encoding@0.1.13)
mjml-raw: 4.15.3(encoding@0.1.13)
mjml-section: 4.15.3(encoding@0.1.13)
mjml-social: 4.15.3(encoding@0.1.13)
mjml-spacer: 4.15.3(encoding@0.1.13)
mjml-table: 4.15.3(encoding@0.1.13)
mjml-text: 4.15.3(encoding@0.1.13)
mjml-wrapper: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-raw@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-section@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-social@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-spacer@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-table@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-text@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml-validator@4.15.3:
dependencies:
'@babel/runtime': 7.24.7
mjml-wrapper@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
lodash: 4.17.21
mjml-core: 4.15.3(encoding@0.1.13)
mjml-section: 4.15.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
mjml@4.15.3(encoding@0.1.13):
dependencies:
'@babel/runtime': 7.24.7
mjml-cli: 4.15.3(encoding@0.1.13)
mjml-core: 4.15.3(encoding@0.1.13)
mjml-migrate: 4.15.3(encoding@0.1.13)
mjml-preset-core: 4.15.3(encoding@0.1.13)
mjml-validator: 4.15.3
transitivePeerDependencies:
- encoding
mkdirp-classic@0.5.3: {} mkdirp-classic@0.5.3: {}
mkdirp@0.5.6: mkdirp@0.5.6:
@ -24484,6 +25011,10 @@ snapshots:
neo-async@2.6.2: {} neo-async@2.6.2: {}
no-case@2.3.2:
dependencies:
lower-case: 1.1.4
no-case@3.0.4: no-case@3.0.4:
dependencies: dependencies:
lower-case: 2.0.2 lower-case: 2.0.2
@ -24923,6 +25454,10 @@ snapshots:
pako@1.0.11: {} pako@1.0.11: {}
param-case@2.1.1:
dependencies:
no-case: 2.3.2
param-case@3.0.4: param-case@3.0.4:
dependencies: dependencies:
dot-case: 3.0.4 dot-case: 3.0.4
@ -25819,6 +26354,8 @@ snapshots:
reinterval@1.1.0: {} reinterval@1.1.0: {}
relateurl@0.2.7: {}
remove-trailing-slash@0.1.1: {} remove-trailing-slash@0.1.1: {}
replace-in-file@6.3.5: replace-in-file@6.3.5:
@ -26246,6 +26783,8 @@ snapshots:
astral-regex: 2.0.0 astral-regex: 2.0.0
is-fullwidth-code-point: 3.0.0 is-fullwidth-code-point: 3.0.0
slick@1.12.2: {}
slugify@1.4.7: {} slugify@1.4.7: {}
smart-buffer@4.2.0: smart-buffer@4.2.0:
@ -27159,8 +27698,7 @@ snapshots:
ufo@1.5.4: {} ufo@1.5.4: {}
uglify-js@3.17.4: uglify-js@3.17.4: {}
optional: true
uid-safe@2.1.5: uid-safe@2.1.5:
dependencies: dependencies:
@ -27302,6 +27840,8 @@ snapshots:
dependencies: dependencies:
tslib: 2.6.2 tslib: 2.6.2
upper-case@1.1.3: {}
upper-case@2.0.2: upper-case@2.0.2:
dependencies: dependencies:
tslib: 2.6.2 tslib: 2.6.2
@ -27375,6 +27915,8 @@ snapshots:
'@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-lib-coverage': 2.0.4
convert-source-map: 1.9.0 convert-source-map: 1.9.0
valid-data-url@3.0.1: {}
validate-npm-package-license@3.0.4: validate-npm-package-license@3.0.4:
dependencies: dependencies:
spdx-correct: 3.1.1 spdx-correct: 3.1.1
@ -27633,6 +28175,17 @@ snapshots:
dependencies: dependencies:
defaults: 1.0.4 defaults: 1.0.4
web-resource-inliner@6.0.1(encoding@0.1.13):
dependencies:
ansi-colors: 4.1.3
escape-goat: 3.0.0
htmlparser2: 5.0.1
mime: 2.6.0
node-fetch: 2.7.0(encoding@0.1.13)
valid-data-url: 3.0.1
transitivePeerDependencies:
- encoding
web-streams-polyfill@3.2.1: {} web-streams-polyfill@3.2.1: {}
web-streams-polyfill@4.0.0-beta.3: {} web-streams-polyfill@4.0.0-beta.3: {}