Skip to content

Commit 6dc6b4d

Browse files
authored
only unsub contacts on permanent bounces (#156)
* only unsub on permanent counces * add hard bounce to email usage * add hard bounce for campaign * fix
1 parent 759e438 commit 6dc6b4d

File tree

5 files changed

+57
-16
lines changed

5 files changed

+57
-16
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- AlterTable
2+
ALTER TABLE "DailyEmailUsage" ADD COLUMN "hardBounced" INTEGER NOT NULL DEFAULT 0;
3+
4+
-- AlterTable
5+
ALTER TABLE "EmailEvent" ALTER COLUMN "status" DROP DEFAULT;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "Campaign" ADD COLUMN "hardBounced" INTEGER NOT NULL DEFAULT 0;

apps/web/prisma/schema.prisma

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ model Campaign {
325325
clicked Int @default(0)
326326
unsubscribed Int @default(0)
327327
bounced Int @default(0)
328+
hardBounced Int @default(0)
328329
complained Int @default(0)
329330
status CampaignStatus @default(DRAFT)
330331
createdAt DateTime @default(now())
@@ -356,18 +357,19 @@ enum EmailUsageType {
356357
}
357358

358359
model DailyEmailUsage {
359-
teamId Int
360-
date String
361-
type EmailUsageType
362-
domainId Int
363-
sent Int @default(0)
364-
delivered Int @default(0)
365-
opened Int @default(0)
366-
clicked Int @default(0)
367-
bounced Int @default(0)
368-
complained Int @default(0)
369-
createdAt DateTime @default(now())
370-
updatedAt DateTime @updatedAt
360+
teamId Int
361+
date String
362+
type EmailUsageType
363+
domainId Int
364+
sent Int @default(0)
365+
delivered Int @default(0)
366+
opened Int @default(0)
367+
clicked Int @default(0)
368+
bounced Int @default(0)
369+
complained Int @default(0)
370+
hardBounced Int @default(0)
371+
createdAt DateTime @default(now())
372+
updatedAt DateTime @updatedAt
371373
372374
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
373375

apps/web/src/server/service/campaign-service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,8 @@ export async function sendCampaignEmail(
334334

335335
export async function updateCampaignAnalytics(
336336
campaignId: string,
337-
emailStatus: EmailStatus
337+
emailStatus: EmailStatus,
338+
hardBounce: boolean = false
338339
) {
339340
const campaign = await db.campaign.findUnique({
340341
where: { id: campaignId },
@@ -361,6 +362,9 @@ export async function updateCampaignAnalytics(
361362
break;
362363
case EmailStatus.BOUNCED:
363364
updateData.bounced = { increment: 1 };
365+
if (hardBounce) {
366+
updateData.hardBounced = { increment: 1 };
367+
}
364368
break;
365369
case EmailStatus.COMPLAINED:
366370
updateData.complained = { increment: 1 };

apps/web/src/server/service/ses-hook-parser.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { EmailStatus, Prisma, UnsubscribeReason } from "@prisma/client";
2-
import { SesClick, SesEvent, SesEventDataKey } from "~/types/aws-types";
2+
import {
3+
SesBounce,
4+
SesClick,
5+
SesEvent,
6+
SesEventDataKey,
7+
} from "~/types/aws-types";
38
import { db } from "../db";
49
import {
510
unsubscribeContact,
@@ -57,6 +62,12 @@ export async function parseSesHook(data: SesEvent) {
5762
// Update daily email usage statistics
5863
const today = new Date().toISOString().split("T")[0] as string; // Format: YYYY-MM-DD
5964

65+
const isHardBounced =
66+
mailStatus === EmailStatus.BOUNCED &&
67+
(mailData as SesBounce).bounceType === "Permanent";
68+
69+
console.log("mailStatus", mailStatus, "isHardBounced", isHardBounced);
70+
6071
if (
6172
[
6273
"DELIVERED",
@@ -89,11 +100,13 @@ export async function parseSesHook(data: SesEvent) {
89100
bounced: updateField === "bounced" ? 1 : 0,
90101
complained: updateField === "complained" ? 1 : 0,
91102
sent: updateField === "sent" ? 1 : 0,
103+
hardBounced: isHardBounced ? 1 : 0,
92104
},
93105
update: {
94106
[updateField]: {
95107
increment: 1,
96108
},
109+
...(isHardBounced ? { hardBounced: { increment: 1 } } : {}),
97110
},
98111
});
99112
}
@@ -108,6 +121,7 @@ export async function parseSesHook(data: SesEvent) {
108121
campaignId: email.campaignId,
109122
teamId: email.teamId,
110123
event: mailStatus,
124+
mailData: data,
111125
});
112126

113127
const mailEvent = await db.emailEvent.findFirst({
@@ -118,7 +132,11 @@ export async function parseSesHook(data: SesEvent) {
118132
});
119133

120134
if (!mailEvent) {
121-
await updateCampaignAnalytics(email.campaignId, mailStatus);
135+
await updateCampaignAnalytics(
136+
email.campaignId,
137+
mailStatus,
138+
isHardBounced
139+
);
122140
}
123141
}
124142
}
@@ -139,13 +157,23 @@ async function checkUnsubscribe({
139157
campaignId,
140158
teamId,
141159
event,
160+
mailData,
142161
}: {
143162
contactId: string;
144163
campaignId: string;
145164
teamId: number;
146165
event: EmailStatus;
166+
mailData: SesEvent;
147167
}) {
148-
if (event === EmailStatus.BOUNCED || event === EmailStatus.COMPLAINED) {
168+
/**
169+
* If the email is bounced and the bounce type is permanent, we need to unsubscribe the contact
170+
* If the email is complained, we need to unsubscribe the contact
171+
*/
172+
if (
173+
(event === EmailStatus.BOUNCED &&
174+
mailData.bounce?.bounceType === "Permanent") ||
175+
event === EmailStatus.COMPLAINED
176+
) {
149177
const contact = await db.contact.findUnique({
150178
where: {
151179
id: contactId,

0 commit comments

Comments
 (0)