auto unsubscribe on bounce and compaints (#117)

* auto unsubscribe on bounce and compaints

* console
This commit is contained in:
KM Koushik
2025-03-18 00:25:11 +11:00
committed by GitHub
parent 0a1d93ac60
commit 0fc27d8d7e
11 changed files with 231 additions and 143 deletions

View File

@@ -141,6 +141,7 @@ export const contactsRouter = createTRPCRouter({
subscribed: true,
createdAt: true,
contactBookId: true,
unsubscribeReason: true,
},
orderBy: {
createdAt: "desc",

View File

@@ -2,7 +2,12 @@ import { EmailRenderer } from "@unsend/email-editor/src/renderer";
import { db } from "../db";
import { createHash } from "crypto";
import { env } from "~/env";
import { Campaign, Contact, EmailStatus } from "@prisma/client";
import {
Campaign,
Contact,
EmailStatus,
UnsubscribeReason,
} from "@prisma/client";
import { validateDomainFromEmail } from "./domain-service";
import { EmailQueueService } from "./email-queue-service";
import { Queue, Worker } from "bullmq";
@@ -91,7 +96,7 @@ export function createUnsubUrl(contactId: string, campaignId: string) {
return `${env.NEXTAUTH_URL}/unsubscribe?id=${unsubId}&hash=${unsubHash}`;
}
export async function unsubscribeContact(id: string, hash: string) {
export async function unsubscribeContactFromLink(id: string, hash: string) {
const [contactId, campaignId] = id.split("-");
if (!contactId || !campaignId) {
@@ -107,6 +112,18 @@ export async function unsubscribeContact(id: string, hash: string) {
throw new Error("Invalid unsubscribe link");
}
return await unsubscribeContact(
contactId,
campaignId,
UnsubscribeReason.UNSUBSCRIBED
);
}
export async function unsubscribeContact(
contactId: string,
campaignId: string,
reason: UnsubscribeReason
) {
// Update the contact's subscription status
try {
const contact = await db.contact.findUnique({
@@ -120,7 +137,7 @@ export async function unsubscribeContact(id: string, hash: string) {
if (contact.subscribed) {
await db.contact.update({
where: { id: contactId },
data: { subscribed: false },
data: { subscribed: false, unsubscribeReason: reason },
});
await db.campaign.update({

View File

@@ -50,6 +50,8 @@ export async function createDomain(
) {
const domainStr = tldts.getDomain(name);
console.log("Creating domain", { domainStr, name, region });
if (!domainStr) {
throw new Error("Invalid domain");
}

View File

@@ -1,7 +1,10 @@
import { EmailStatus, Prisma } from "@prisma/client";
import { EmailStatus, Prisma, UnsubscribeReason } from "@prisma/client";
import { SesClick, SesEvent, SesEventDataKey } from "~/types/aws-types";
import { db } from "../db";
import { updateCampaignAnalytics } from "./campaign-service";
import {
unsubscribeContact,
updateCampaignAnalytics,
} from "./campaign-service";
import { env } from "~/env";
import { getRedis } from "../redis";
import { Queue, Worker } from "bullmq";
@@ -100,6 +103,8 @@ export async function parseSesHook(data: SesEvent) {
mailStatus !== "CLICKED" ||
!(mailData as SesClick).link.startsWith(`${env.NEXTAUTH_URL}/unsubscribe`)
) {
await checkUnsubscribe(email.campaignId, email.contactId!, mailStatus);
const mailEvent = await db.emailEvent.findFirst({
where: {
emailId: email.id,
@@ -124,6 +129,22 @@ export async function parseSesHook(data: SesEvent) {
return true;
}
function checkUnsubscribe(
campaignId: string,
contactId: string,
event: EmailStatus
) {
if (event === EmailStatus.BOUNCED || event === EmailStatus.COMPLAINED) {
return unsubscribeContact(
contactId,
campaignId,
event === EmailStatus.BOUNCED
? UnsubscribeReason.BOUNCED
: UnsubscribeReason.COMPLAINED
);
}
}
function getEmailStatus(data: SesEvent) {
const { eventType } = data;