strapi-plugin-email-designer-5
Version:
Strapi Email Designer (v5)
663 lines (662 loc) • 21.8 kB
JavaScript
;
const htmlToText = require("html-to-text");
const _ = require("lodash");
const yup = require("yup");
const decode = require("decode-html");
const Mustache = require("mustache");
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
function _interopNamespace(e) {
if (e && e.__esModule) return e;
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
if (e) {
for (const k in e) {
if (k !== "default") {
const d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: () => e[k]
});
}
}
}
n.default = e;
return Object.freeze(n);
}
const ___default = /* @__PURE__ */ _interopDefault(_);
const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
const decode__default = /* @__PURE__ */ _interopDefault(decode);
const Mustache__default = /* @__PURE__ */ _interopDefault(Mustache);
const config = {
default: () => ({
mergeTagsConfig: {
autocompleteTriggerChar: "@",
sort: false,
delimiter: ["{{", "}}"]
},
appearance: {
theme: "modern_light"
},
fonts: {
showDefaultFonts: false
},
tools: {
image: {
properties: {
src: {
value: {
url: "https://picsum.photos/600/350"
}
}
}
}
},
mergeTags: {
core: {
name: "Core",
mergeTags: {
// Values that can be used in the Reset Password context
resetPassword: {
name: "Reset Password",
mergeTags: {
// User in the Reset Password context
user: {
name: "USER",
mergeTags: {
username: {
name: "Username",
value: "{{= USER.username }}",
sample: "john_doe"
},
email: {
name: "Email",
value: "{{= USER.email }}",
sample: "johndoe@example.com"
}
}
},
token: {
name: "TOKEN",
value: "{{= TOKEN }}",
sample: "corresponds-to-the-token-generated-to-be-able-to-reset-the-password"
},
url: {
name: "URL",
value: "{{= URL }}",
sample: "is-the-link-where-the-user-will-be-redirected-after-clicking-on-it-in-the-email"
},
serverUrl: {
name: "SERVER_URL",
value: "{{= SERVER_URL }}",
sample: "is-the-absolute-server-url-(configured-in-server-configuration)"
}
}
},
// Values that can be used in the Email Addres Confirmation context
addressConfirmation: {
name: "Confirm Address",
mergeTags: {
// User in the Email Address Confirmation context
user: {
name: "USER",
mergeTags: {
username: {
name: "Username",
value: "{{= USER.username }}",
sample: "john_doe"
},
email: {
name: "Email",
value: "{{= USER.email }}",
sample: "johndoe@example.com"
}
}
},
code: {
name: "CODE",
value: "{{= CODE }}",
sample: "corresponds-to-the-CODE-generated-to-be-able-confirm-the-user-email"
},
url: {
name: "URL",
value: "{{= URL }}",
sample: "is-the-Strapi-backend-URL-that-confirms-the-code-(by-default-/auth/email-confirmation)"
},
serverUrl: {
name: "SERVER_URL",
value: "{{= SERVER_URL }}",
sample: "is-the-absolute-server-url-(configured-in-server-configuration)"
}
}
}
}
},
mustache: {
name: "Mustache",
mergeTags: {
basic: {
name: "Basic Output",
mergeTags: {
raw: {
name: "Display Raw Content",
value: "{{{REPLACE_ME}}}"
},
output: {
name: "Regular Output",
value: "{{REPLACE_ME}}"
},
dottedOutput: {
name: "Dot notation for Output",
value: "{{REPLACE_ME.NESTED_VALUE}}"
}
}
},
loops: {
name: "Loops",
mergeTags: {
raw: {
name: "Display Raw Content in Loop",
value: "{{#ARRAY_OR_OBJECT_TO_ITERATE}}\n{{{REPLACE_ME}}}\n{{/ARRAY_OR_OBJECT_TO_ITERATE}}"
},
output: {
name: "Regular Output in Loop",
value: "{{#ARRAY_OR_OBJECT_TO_ITERATE}}\n{{REPLACE_ME}}\n{{/ARRAY_OR_OBJECT_TO_ITERATE}}"
},
dottedOutput: {
name: "Dot notation for Output in Loop",
value: "{{#ARRAY_OR_OBJECT_TO_ITERATE}}\n{{REPLACE_ME.NESTED_VALUE}}\n{{/ARRAY_OR_OBJECT_TO_ITERATE}}"
}
}
}
}
}
}
}),
validator() {
},
/** The name of the strapi plugin
*
* @default "email-designer-5"
*/
pluginName: "email-designer-5"
};
const bootstrap = async ({ strapi }) => {
const actions = [
{
section: "plugins",
displayName: "Allow access to the Email Designer interface",
uid: "menu-link",
pluginName: config.pluginName
}
];
await strapi.admin.services.permission.actionProvider.registerMany(actions);
};
const contentTypes = {
"email-designer-template": {
schema: {
kind: "collectionType",
collectionName: "email-designer-templates",
info: {
singularName: "email-designer-template",
pluralName: "email-designer-templates",
displayName: "Email Designer Templates",
description: "This collection stores email templates created with the email designer."
},
pluginOptions: {
"content-manager": { visible: false },
"content-type-builder": { visible: false }
},
options: {
draftAndPublish: false
},
attributes: {
templateReferenceId: {
type: "integer",
required: false,
unique: true,
configurable: false
},
design: {
type: "json",
configurable: false
},
name: {
type: "string",
configurable: false
},
subject: {
type: "string",
configurable: false
},
bodyHtml: {
type: "text",
configurable: false
},
bodyText: {
type: "text",
configurable: false
},
tags: {
type: "json"
}
}
}
}
};
const controller$1 = ({ strapi }) => ({
getConfig: async (ctx) => {
const { configKey } = ctx.params;
const config$1 = await strapi.plugin(config.pluginName).service("config").getConfig(configKey);
ctx.send(config$1);
},
getFullConfig: async (ctx) => {
const config$1 = await strapi.config.get(`plugin::${config.pluginName}`);
ctx.send(config$1);
}
});
const isValidRefId = yup__namespace.number().required().label("Template reference ID").min(0);
const controller = ({ strapi }) => ({
/**
* Get template design action.
*
* @return {Object}
*/
getTemplates: async (ctx) => {
const templates = await strapi.plugin(config.pluginName).service("template").findMany();
ctx.send(templates);
},
/**
* Get template design action.
*
* @return {Object}
*/
getTemplate: async (ctx) => {
const template = await strapi.plugin(config.pluginName).service("template").findOne({ id: ctx.params.templateId });
ctx.send(template);
},
/**
* Delete template design action.
*
* @return {Object}
*/
deleteTemplate: async (ctx) => {
isValidRefId.validateSync(ctx.params.templateId);
await strapi.plugin(config.pluginName).service("template").delete({ id: ctx.params.templateId });
ctx.send({ removed: true });
},
/**
* Save template design action.
*
* @return {Object}
*/
saveTemplate: async (ctx) => {
let { templateId } = ctx.params;
const { templateReferenceId, import: importTemplate } = ctx.request.body;
try {
if (importTemplate === true) {
if (!_.isNil(templateReferenceId)) {
const foundTemplate = await strapi.plugin(config.pluginName).service("template").findOne({
templateReferenceId
});
if (!___default.default.isEmpty(foundTemplate)) {
if (templateId === "new") return ctx.badRequest("Template reference ID is already taken");
templateId = foundTemplate.id;
} else {
templateId = "new";
}
} else {
templateId = "new";
}
}
let template = {};
if (templateId === "new") {
const existingTemplate = await strapi.plugin(config.pluginName).service("template").findOne({
templateReferenceId
});
if (!___default.default.isEmpty(existingTemplate)) {
return ctx.badRequest("Template reference ID is already taken");
}
template = await strapi.plugin(config.pluginName).service("template").create(ctx.request.body);
} else {
const existingTemplate = await strapi.plugin(config.pluginName).service("template").findOne({
templateReferenceId,
id: { $ne: templateId }
});
if (!___default.default.isEmpty(existingTemplate)) {
return ctx.badRequest("Template reference ID is already taken");
}
template = await strapi.plugin(config.pluginName).service("template").update({ id: templateId }, ctx.request.body);
}
ctx.send(template || {});
} catch (error) {
console.log(error);
ctx.badRequest(null, error);
}
},
/**
* Duplicate a template.
*
* @return {Object}
*/
duplicateTemplate: async (ctx) => {
if (___default.default.isEmpty(ctx.params.sourceTemplateId)) {
return ctx.badRequest("No source template Id given");
}
const { __v, _id, id, updatedAt, createdAt, ...toClone } = await strapi.plugin(config.pluginName).service("template").findOne({ id: ctx.params.sourceTemplateId });
if (toClone) {
return strapi.plugin(config.pluginName).service("template").create({ ...toClone, name: `${toClone.name} copy`, templateReferenceId: null });
}
return null;
},
/**
* Downloads a template
*/
download: async (ctx) => {
try {
const { id } = ctx.params;
const { type = "json" } = ctx.query;
const template = await strapi.plugin(config.pluginName).service("template").findOne({ id });
if (!template) {
return ctx.notFound("Template not found");
}
let fileContent, fileName;
if (type === "json") {
fileContent = JSON.stringify(template.design, null, 2);
fileName = `template-${id}.json`;
ctx.set("Content-Type", "application/json");
} else if (type === "html") {
fileContent = template.bodyHtml;
fileName = `template-${id}.html`;
ctx.set("Content-Type", "text/html");
} else {
return ctx.badRequest('Invalid type, must be either "json" or "html".');
}
ctx.set("Content-Disposition", `attachment; filename="${fileName}"`);
ctx.send(fileContent);
} catch (err) {
strapi.log.error("Error downloading template:", err);
ctx.internalServerError("Failed to download the template");
}
},
/**
* Strapi's core templates
*/
/**
* Get strapi's core message template action.
*
* @return {Object}
*/
getCoreEmailType: async (ctx) => {
const { coreEmailType } = ctx.params;
if (!["user-address-confirmation", "reset-password"].includes(coreEmailType))
return ctx.badRequest("No valid core message key");
const pluginStoreEmailKey = coreEmailType === "user-address-confirmation" ? "email_confirmation" : "reset_password";
const pluginStore = await strapi.store({
environment: "",
type: "plugin",
name: "users-permissions"
});
let data = await pluginStore.get({ key: "email" }).then((storeEmail) => storeEmail[pluginStoreEmailKey]);
data = {
...data && data.options ? {
from: data.options.from,
message: data.options.message,
subject: data.options.object.replace(/<%|<%/g, "{{").replace(/%>|%>/g, "}}"),
bodyHtml: data.options.message.replace(/<%|<%/g, "{{").replace(/%>|%>/g, "}}"),
bodyText: htmlToText.htmlToText(
data.options.message.replace(/<%|<%/g, "{{").replace(/%>|%>/g, "}}"),
{
wordwrap: 130
}
)
} : {},
coreEmailType,
design: data.design
};
ctx.send(data);
},
/**
* Save strapi's core message template action.
*
* @return {Object}
*/
saveCoreEmailType: async (ctx) => {
const { coreEmailType } = ctx.params;
if (!["user-address-confirmation", "reset-password"].includes(coreEmailType))
return ctx.badRequest("No valid core message key");
const pluginStoreEmailKey = coreEmailType === "user-address-confirmation" ? "email_confirmation" : "reset_password";
const pluginStore = await strapi.store({
environment: "",
type: "plugin",
name: "users-permissions"
});
const emailsConfig = await pluginStore.get({ key: "email" });
strapi.plugin(config.pluginName).services.config.getConfig();
emailsConfig[pluginStoreEmailKey] = {
...emailsConfig[pluginStoreEmailKey],
options: {
...emailsConfig[pluginStoreEmailKey] ? emailsConfig[pluginStoreEmailKey].options : {},
message: ctx.request.body.message.replace(/{{/g, "<%").replace(/}}/g, "%>"),
object: ctx.request.body.subject.replace(/{{/g, "<%").replace(/}}/g, "%>")
// TODO: from: ctx.request.from,
// TODO: response_email: ctx.request.response_email,
},
design: ctx.request.body.design
};
await pluginStore.set({ key: "email", value: emailsConfig });
ctx.send({ message: "Saved" });
}
});
const controllers = {
config: controller$1,
designer: controller
};
const destroy = ({ strapi }) => {
};
const middlewares = {};
const policies = {};
const register = ({ strapi }) => {
};
const routes = [
{
method: "GET",
path: "/templates",
handler: "designer.getTemplates",
config: { policies: [], auth: false }
},
{
method: "GET",
path: "/templates/:templateId",
handler: "designer.getTemplate",
config: { policies: [], auth: false }
},
{
method: "POST",
path: "/templates/:templateId",
handler: "designer.saveTemplate",
config: { policies: [], auth: false }
},
{
method: "DELETE",
path: "/templates/:templateId",
handler: "designer.deleteTemplate",
config: { policies: [], auth: false }
},
{
method: "POST",
path: "/templates/duplicate/:sourceTemplateId",
handler: "designer.duplicateTemplate",
config: { policies: [], auth: false }
},
{
method: "GET",
path: "/config/:configKey",
handler: "config.getConfig",
config: { policies: [], auth: false }
},
{
method: "GET",
path: "/config",
handler: "config.getFullConfig",
config: { policies: [], auth: false }
},
{
method: "GET",
path: "/core/:coreEmailType",
handler: "designer.getCoreEmailType",
config: { policies: [], auth: false }
},
{
method: "POST",
path: "/core/:coreEmailType",
handler: "designer.saveCoreEmailType",
config: { policies: [], auth: false }
},
{
method: "GET",
path: "/download/:id",
handler: "designer.download",
config: { policies: [], auth: false }
}
];
const service$1 = ({ strapi }) => ({
getConfig(key = "editor") {
return strapi.plugin(config.pluginName).config(key) ?? {};
}
});
const isValidEmail = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;
const isValidEmailSchema = yup__namespace.string().test("is-valid-email", "Invalid email address", (value) => {
return isValidEmail.test(value);
});
const isTemplateReferenceIdSchema = yup__namespace.number().required().label("Template Reference Id").min(1);
const pluginUID = `plugin::${config.pluginName}.email-designer-template`;
const email = ({ strapi }) => {
const sendTemplatedEmail = async (emailOptions = {}, emailTemplate, data = {}) => {
const keysToIgnore = ["attachment", "attachments", "headers"];
Object.entries(emailOptions).forEach(([key, address]) => {
if (!keysToIgnore.includes(key)) {
if (Array.isArray(address)) {
address.forEach((email2) => {
isValidEmailSchema.validateSync(email2, { abortEarly: true });
});
} else {
isValidEmailSchema.validateSync(address, { abortEarly: true });
}
}
});
isTemplateReferenceIdSchema.validateSync(emailTemplate.templateReferenceId, { abortEarly: true });
const attributes = ["text", "html", "subject"];
const { templateReferenceId } = emailTemplate;
const response = await strapi.db.query(pluginUID).findOne({ where: { templateReferenceId } });
if (!response) {
strapi.log.error(`No email template found with referenceId "${templateReferenceId}"`);
return null;
}
let bodyHtml = "";
let bodyText = "";
let subject = "";
({ bodyHtml, bodyText, subject } = response);
bodyHtml = bodyHtml.replace(/<%/g, "{{").replace(/%>/g, "}}");
bodyText = bodyText.replace(/<%/g, "{{").replace(/%>/g, "}}");
subject = subject.replace(/<%/g, "{{").replace(/%>/g, "}}");
if ((!bodyText || !bodyText.length) && bodyHtml && bodyHtml.length) {
bodyText = htmlToText.htmlToText(bodyHtml, { wordwrap: 130 });
}
emailTemplate = {
...emailTemplate,
subject: !_.isEmpty(emailTemplate.subject) && emailTemplate.subject || !_.isEmpty(subject) && decode__default.default(subject) || "No Subject",
html: decode__default.default(bodyHtml),
text: decode__default.default(bodyText)
};
const templatedAttributes = attributes.reduce(
(compiled, attribute) => emailTemplate[attribute] ? Object.assign(compiled, { [attribute]: Mustache__default.default.render(emailTemplate[attribute], data) }) : compiled,
{}
);
return strapi.plugin("email").provider.send({ ...emailOptions, ...templatedAttributes });
};
const compose = async ({ templateReferenceId, data }) => {
isTemplateReferenceIdSchema.validateSync(templateReferenceId, { abortEarly: true });
let res = await strapi.db.query(pluginUID).findOne({ where: { templateReferenceId } });
if (!res) {
throw new Error(`No email template found with referenceId "${templateReferenceId}"`);
}
let { bodyHtml = "", bodyText = "", subject = "" } = res;
bodyHtml = bodyHtml.replace(/<%/g, "{{").replace(/%>/g, "}}");
bodyText = bodyText.replace(/<%/g, "{{").replace(/%>/g, "}}");
subject = subject.replace(/<%/g, "{{").replace(/%>/g, "}}");
if ((!bodyText || !bodyText.length) && bodyHtml && bodyHtml.length) {
bodyText = htmlToText.htmlToText(bodyHtml, { wordwrap: 130 });
}
const emailTemplate = {
html: decode__default.default(bodyHtml),
text: decode__default.default(bodyText)
};
const attributes = ["text", "html"];
const templatedAttributes = attributes.reduce(
(compiled, attribute) => emailTemplate[attribute] ? Object.assign(compiled, { [attribute]: Mustache__default.default.render(emailTemplate[attribute], data) }) : compiled,
{}
);
return {
composedHtml: templatedAttributes.html,
composedText: templatedAttributes.text
};
};
return {
sendTemplatedEmail,
compose
};
};
const service = ({ strapi }) => ({
/**
* Promise to fetch a template.
* @return {Promise}
*/
findOne(params) {
return strapi.db.query(pluginUID).findOne({ where: params });
},
/**
* Promise to fetch all templates.
* @return {Promise}
*/
findMany(params) {
return strapi.db.query(pluginUID).findMany({ where: params });
},
/**
* Promise to add a template.
* @return {Promise}
*/
async create(values) {
return strapi.db.query(pluginUID).create({ data: values });
},
/**
* Promise to edit a template.
* @return {Promise}
*/
async update(params, values) {
return strapi.db.query(pluginUID).update({ where: params, data: values });
},
/**
* Promise to remove a template.
* @return {Promise}
*/
async delete(params) {
return strapi.db.query(pluginUID).delete({ where: params });
}
});
const services = {
email,
config: service$1,
template: service
};
const index = {
register,
bootstrap,
destroy,
config,
controllers,
routes,
services,
contentTypes,
policies,
middlewares
};
module.exports = index;