UNPKG

strapi-plugin-email-designer-5

Version:
663 lines (662 loc) 21.8 kB
"use strict"; 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(/<%|&#x3C;%/g, "{{").replace(/%>|%&#x3E;/g, "}}"), bodyHtml: data.options.message.replace(/<%|&#x3C;%/g, "{{").replace(/%>|%&#x3E;/g, "}}"), bodyText: htmlToText.htmlToText( data.options.message.replace(/<%|&#x3C;%/g, "{{").replace(/%>|%&#x3E;/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;