From f1d4d5073ccd7ddceabfdbd268c5ff142480fb75 Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Tue, 24 Sep 2024 17:19:55 +0200 Subject: [PATCH] Feat: Added ability to overwrite sender mail and name for templates and campaigns --- package.json | 2 +- packages/api/src/controllers/Tasks.ts | 13 ++++- packages/api/src/controllers/v1/Campaigns.ts | 27 ++++++++- packages/api/src/controllers/v1/Templates.ts | 32 +++++++++- packages/api/src/services/ActionService.ts | 4 +- .../dashboard/src/pages/campaigns/[id].tsx | 55 +++++++++++++++--- .../dashboard/src/pages/campaigns/new.tsx | 58 ++++++++++++++++--- .../dashboard/src/pages/templates/[id].tsx | 39 +++++++++++++ .../dashboard/src/pages/templates/new.tsx | 41 ++++++++++++- packages/shared/src/index.ts | 8 +++ prisma/schema.prisma | 9 ++- 11 files changed, 259 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index d3bc611..65c31dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plunk", - "version": "1.0.5", + "version": "1.0.6", "private": true, "license": "agpl-3.0", "workspaces": { diff --git a/packages/api/src/controllers/Tasks.ts b/packages/api/src/controllers/Tasks.ts index 92532ce..4e00342 100644 --- a/packages/api/src/controllers/Tasks.ts +++ b/packages/api/src/controllers/Tasks.ts @@ -41,6 +41,9 @@ export class Tasks { let subject = ""; let body = ""; + let email = ""; + let name = ""; + if (action) { const { template, notevents } = action; @@ -52,6 +55,9 @@ export class Tasks { } } + email = project.verified && project.email ? template.email ?? project.email : "no-reply@useplunk.dev"; + name = template.from ?? project.from ?? project.name; + ({ subject, body } = EmailService.format({ subject: template.subject, body: template.body, @@ -62,6 +68,9 @@ export class Tasks { }, })); } else if (campaign) { + email = project.verified && project.email ? campaign.email ?? project.email : "no-reply@useplunk.dev"; + name = campaign.from ?? project.from ?? project.name; + ({ subject, body } = EmailService.format({ subject: campaign.subject, body: campaign.body, @@ -75,8 +84,8 @@ export class Tasks { const { messageId } = await EmailService.send({ from: { - name: project.from ?? project.name, - email: project.verified && project.email ? project.email : "no-reply@useplunk.dev", + name, + email, }, to: [contact.email], content: { diff --git a/packages/api/src/controllers/v1/Campaigns.ts b/packages/api/src/controllers/v1/Campaigns.ts index 9072f98..321129a 100644 --- a/packages/api/src/controllers/v1/Campaigns.ts +++ b/packages/api/src/controllers/v1/Campaigns.ts @@ -3,7 +3,7 @@ import { CampaignSchemas, UtilitySchemas } from "@plunk/shared"; import dayjs from "dayjs"; import type { Request, Response } from "express"; import { prisma } from "../../database/prisma"; -import { HttpException, NotFound } from "../../exceptions"; +import { HttpException, NotAllowed, NotFound } from "../../exceptions"; import { type IJwt, type ISecret, isAuthenticated, isValidSecretKey } from "../../middleware/auth"; import { CampaignService } from "../../services/CampaignService"; import { ContactService } from "../../services/ContactService"; @@ -160,6 +160,7 @@ export class Campaigns { subject: campaign.subject, body: campaign.body, style: campaign.style, + email: campaign.email, }, }); @@ -180,7 +181,15 @@ export class Campaigns { throw new NotFound("project"); } - let { subject, body, recipients, style } = CampaignSchemas.create.parse(req.body); + let { subject, body, recipients, style, email, from } = CampaignSchemas.create.parse(req.body); + + if (email && !project.verified) { + throw new NotAllowed("You need to attach a domain to your project to customize the sender address"); + } + + if (email && email.split("@")[1] !== project.email?.split("@")[1]) { + throw new NotAllowed("The sender address must be the same domain as the project's email address"); + } if (recipients.length === 1 && recipients[0] === "all") { const projectContacts = await prisma.contact.findMany({ @@ -197,6 +206,8 @@ export class Campaigns { subject, body, style, + from: from === "" ? null : from, + email: email === "" ? null : email, }, }); @@ -253,7 +264,15 @@ export class Campaigns { throw new NotFound("project"); } - let { id, subject, body, recipients, style } = CampaignSchemas.update.parse(req.body); + let { id, subject, body, recipients, style, email, from } = CampaignSchemas.update.parse(req.body); + + if (email && !project.verified) { + throw new NotAllowed("You need to attach a domain to your project to customize the sender address"); + } + + if (email && email.split("@")[1] !== project.email?.split("@")[1]) { + throw new NotAllowed("The sender address must be the same domain as the project's email address"); + } if (recipients.length === 1 && recipients[0] === "all") { const projectContacts = await prisma.contact.findMany({ @@ -276,6 +295,8 @@ export class Campaigns { subject, body, style, + from: from === "" ? null : from, + email: email === "" ? null : email, }, include: { recipients: { select: { id: true } }, diff --git a/packages/api/src/controllers/v1/Templates.ts b/packages/api/src/controllers/v1/Templates.ts index 2089c20..c2973b1 100644 --- a/packages/api/src/controllers/v1/Templates.ts +++ b/packages/api/src/controllers/v1/Templates.ts @@ -60,6 +60,7 @@ export class Templates { body: template.body, type: template.type, style: template.style, + email: template.email, }, }); @@ -101,7 +102,15 @@ export class Templates { throw new NotFound("project"); } - const { subject, body, type, style } = TemplateSchemas.create.parse(req.body); + const { subject, body, type, style, email, from } = TemplateSchemas.create.parse(req.body); + + if (email && !project.verified) { + throw new NotAllowed("You need to attach a domain to your project to customize the sender address"); + } + + if (email && email.split("@")[1] !== project.email?.split("@")[1]) { + throw new NotAllowed("The sender address must be the same domain as the project's email address"); + } const template = await prisma.template.create({ data: { @@ -110,6 +119,8 @@ export class Templates { body, type, style, + from: from === "" ? null : from, + email: email === "" ? null : email, }, }); @@ -151,7 +162,7 @@ export class Templates { throw new NotFound("project"); } - const { id, subject, body, type, style } = TemplateSchemas.update.parse(req.body); + const { id, subject, body, type, style, email, from } = TemplateSchemas.update.parse(req.body); let template = await TemplateService.id(id); @@ -159,9 +170,24 @@ export class Templates { throw new NotFound("template"); } + if (email && !project.verified) { + throw new NotAllowed("You need to attach a domain to your project to customize the sender address"); + } + + if (email && email.split("@")[1] !== project.email?.split("@")[1]) { + throw new NotAllowed("The sender address must be the same domain as the project's email address"); + } + template = await prisma.template.update({ where: { id }, - data: { subject, body, type, style }, + data: { + subject, + body, + type, + style, + from: from === "" ? null : from, + email: email === "" ? null : email, + }, include: { actions: true, }, diff --git a/packages/api/src/services/ActionService.ts b/packages/api/src/services/ActionService.ts index 26a88fa..58ccf61 100644 --- a/packages/api/src/services/ActionService.ts +++ b/packages/api/src/services/ActionService.ts @@ -120,8 +120,8 @@ export class ActionService { const { messageId } = await EmailService.send({ from: { - name: project.from ?? project.name, - email: project.verified && project.email ? project.email : "no-reply@useplunk.dev", + name: action.template.from ?? project.from ?? project.name, + email: project.verified && project.email ? action.template.email ?? project.email : "no-reply@useplunk.dev", }, to: [contact.email], content: { diff --git a/packages/dashboard/src/pages/campaigns/[id].tsx b/packages/dashboard/src/pages/campaigns/[id].tsx index 4e83bb3..cd1dfb0 100644 --- a/packages/dashboard/src/pages/campaigns/[id].tsx +++ b/packages/dashboard/src/pages/campaigns/[id].tsx @@ -32,6 +32,8 @@ import { network } from "../../lib/network"; interface CampaignValues { subject: string; body: string; + email?: string; + from?: string; recipients: string[]; style: "PLUNK" | "HTML"; } @@ -71,6 +73,8 @@ export default function Index() { watch, reset, setValue, + setError, + clearErrors, } = useForm({ resolver: zodResolver(CampaignSchemas.update), defaultValues: { recipients: [], body: undefined }, @@ -87,6 +91,21 @@ export default function Index() { }); }, [reset, campaign]); + useEffect(() => { + watch((value, { name, type }) => { + if (name === "email") { + if (value.email && project?.email && !value.email.endsWith(project.email.split("@")[1])) { + setError("email", { + type: "manual", + message: `The sender address must end with @${project.email?.split("@")[1]}`, + }); + } else { + clearErrors("email"); + } + } + }); + }, [watch, project, setError, clearErrors]); + if (!project || !campaign || !events || (watch("body") as string | undefined) === undefined) { return ; } @@ -404,13 +423,35 @@ export default function Index() { } >
- +
+ + + {project.verified && ( + + )} + + {project.verified && ( + + )} +
{contacts ? ( <> diff --git a/packages/dashboard/src/pages/campaigns/new.tsx b/packages/dashboard/src/pages/campaigns/new.tsx index bca49f3..41d4cab 100644 --- a/packages/dashboard/src/pages/campaigns/new.tsx +++ b/packages/dashboard/src/pages/campaigns/new.tsx @@ -6,7 +6,7 @@ import dayjs from "dayjs"; import { AnimatePresence, motion } from "framer-motion"; import { Search, Users2, XIcon } from "lucide-react"; import { useRouter } from "next/router"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { type FieldError, useForm } from "react-hook-form"; import { toast } from "sonner"; import { Alert, Card, Dropdown, Editor, FullscreenLoader, Input, MultiselectDropdown } from "../../components"; @@ -20,6 +20,8 @@ import { network } from "../../lib/network"; interface CampaignValues { subject: string; body: string; + email?: string; + from?: string; recipients: string[]; style: "PLUNK" | "HTML"; } @@ -58,6 +60,8 @@ export default function Index() { formState: { errors }, setValue, watch, + setError, + clearErrors, } = useForm({ resolver: zodResolver(CampaignSchemas.create), defaultValues: { @@ -67,6 +71,21 @@ export default function Index() { }, }); + useEffect(() => { + watch((value, { name, type }) => { + if (name === "email") { + if (value.email && project?.email && !value.email.endsWith(project.email.split("@")[1])) { + setError("email", { + type: "manual", + message: `The sender address must end with @${project.email?.split("@")[1]}`, + }); + } else { + clearErrors("email"); + } + } + }); + }, [watch, project, setError, clearErrors]); + if (!project || !events) { return ; } @@ -194,13 +213,36 @@ export default function Index() { - +
+ + + {project.verified && ( + + )} + + {project.verified && ( + + )} +
+ {contacts ? ( <>
diff --git a/packages/dashboard/src/pages/templates/[id].tsx b/packages/dashboard/src/pages/templates/[id].tsx index 14dadd1..0317c2b 100644 --- a/packages/dashboard/src/pages/templates/[id].tsx +++ b/packages/dashboard/src/pages/templates/[id].tsx @@ -16,6 +16,8 @@ import { network } from "../../lib/network"; interface TemplateValues { subject: string; body: string; + email?: string; + from?: string; type: "MARKETING" | "TRANSACTIONAL"; style: "PLUNK" | "HTML"; } @@ -41,6 +43,8 @@ export default function Index() { watch, setValue, reset, + setError, + clearErrors, } = useForm({ resolver: zodResolver(TemplateSchemas.update), defaultValues: { @@ -56,6 +60,21 @@ export default function Index() { reset(template); }, [reset, template]); + useEffect(() => { + watch((value, { name, type }) => { + if (name === "email") { + if (value.email && project?.email && !value.email.endsWith(project.email.split("@")[1])) { + setError("email", { + type: "manual", + message: `The sender address must end with @${project.email?.split("@")[1]}`, + }); + } else { + clearErrors("email"); + } + } + }); + }, [watch, project, setError, clearErrors]); + if (!project || !template || (watch("body") as string | undefined) === undefined) { return ; } @@ -248,6 +267,26 @@ export default function Index() {
+ {project.verified && ( + + )} + + {project.verified && ( + + )} +
({ resolver: zodResolver(TemplateSchemas.create), defaultValues: { @@ -50,6 +54,21 @@ export default function Index() { }, }); + useEffect(() => { + watch((value, { name, type }) => { + if (name === "email") { + if (value.email && project?.email && !value.email.endsWith(project.email.split("@")[1])) { + setError("email", { + type: "manual", + message: `The sender address must end with @${project.email?.split("@")[1]}`, + }); + } else { + clearErrors("email"); + } + } + }); + }, [watch, project, setError, clearErrors]); + if (!project) { return ; } @@ -141,6 +160,26 @@ export default function Index() {
+ {project.verified && ( + + )} + + {project.verified && ( + + )} +