rebrand to useSend (#210)

This commit is contained in:
KM Koushik
2025-09-03 08:21:55 +10:00
committed by GitHub
parent b1a59d2705
commit 07c53d3f58
219 changed files with 1349 additions and 2835 deletions

View File

@@ -14,7 +14,7 @@ export const adminRouter = createTRPCRouter({
.input(
z.object({
region: z.string(),
})
}),
)
.query(async ({ input }) => {
const acc = await getAccount(input.region);
@@ -25,15 +25,15 @@ export const adminRouter = createTRPCRouter({
.input(
z.object({
region: z.string(),
unsendUrl: z.string().url(),
usesendUrl: z.string().url(),
sendRate: z.number(),
transactionalQuota: z.number(),
})
}),
)
.mutation(async ({ input }) => {
return SesSettingsService.createSesSetting({
region: input.region,
unsendUrl: input.unsendUrl,
usesendUrl: input.usesendUrl,
sendingRateLimit: input.sendRate,
transactionalQuota: input.transactionalQuota,
});
@@ -45,7 +45,7 @@ export const adminRouter = createTRPCRouter({
settingsId: z.string(),
sendRate: z.number(),
transactionalQuota: z.number(),
})
}),
)
.mutation(async ({ input }) => {
return SesSettingsService.updateSesSetting({
@@ -59,11 +59,11 @@ export const adminRouter = createTRPCRouter({
.input(
z.object({
region: z.string().optional().nullable(),
})
}),
)
.query(async ({ input }) => {
return SesSettingsService.getSetting(
input.region ?? env.AWS_DEFAULT_REGION
input.region ?? env.AWS_DEFAULT_REGION,
);
}),
});

View File

@@ -1,6 +1,6 @@
import { CampaignStatus, Prisma } from "@prisma/client";
import { TRPCError } from "@trpc/server";
import { EmailRenderer } from "@unsend/email-editor/src/renderer";
import { EmailRenderer } from "@usesend/email-editor/src/renderer";
import { z } from "zod";
import { env } from "~/env";
import {
@@ -29,7 +29,7 @@ export const campaignRouter = createTRPCRouter({
z.object({
page: z.number().optional(),
status: z.enum(statuses).optional().nullable(),
})
}),
)
.query(async ({ ctx: { db, team }, input }) => {
let completeTime = performance.now();
@@ -68,14 +68,14 @@ export const campaignRouter = createTRPCRouter({
campaignsP.then((campaigns) => {
logger.info(
`Time taken to get campaigns: ${performance.now() - time} milliseconds`
`Time taken to get campaigns: ${performance.now() - time} milliseconds`,
);
});
const [campaigns, count] = await Promise.all([campaignsP, countP]);
logger.info(
{ duration: performance.now() - completeTime },
`Time taken to complete request`
`Time taken to complete request`,
);
return { campaigns, totalPage: Math.ceil(count / limit) };
@@ -87,7 +87,7 @@ export const campaignRouter = createTRPCRouter({
name: z.string(),
from: z.string(),
subject: z.string(),
})
}),
)
.mutation(async ({ ctx: { db, team }, input }) => {
const domain = await validateDomainFromEmail(input.from, team.id);
@@ -113,7 +113,7 @@ export const campaignRouter = createTRPCRouter({
content: z.string().optional(),
contactBookId: z.string().optional(),
replyTo: z.string().array().optional(),
})
}),
)
.mutation(async ({ ctx: { db, team, campaign: campaignOld }, input }) => {
const { campaignId, ...data } = input;
@@ -161,7 +161,7 @@ export const campaignRouter = createTRPCRouter({
where: { id: input.campaignId, teamId: team.id },
});
return campaign;
}
},
),
getCampaign: campaignProcedure.query(async ({ ctx: { db, team }, input }) => {
@@ -194,7 +194,7 @@ export const campaignRouter = createTRPCRouter({
sendCampaign: campaignProcedure.mutation(
async ({ ctx: { db, team }, input }) => {
await sendCampaign(input.campaignId);
}
},
),
reSubscribeContact: publicProcedure
@@ -202,7 +202,7 @@ export const campaignRouter = createTRPCRouter({
z.object({
id: z.string(),
hash: z.string(),
})
}),
)
.mutation(async ({ ctx: { db }, input }) => {
await subscribeContact(input.id, input.hash);
@@ -223,7 +223,7 @@ export const campaignRouter = createTRPCRouter({
});
return newCampaign;
}
},
),
generateImagePresignedUrl: campaignProcedure
@@ -231,7 +231,7 @@ export const campaignRouter = createTRPCRouter({
z.object({
name: z.string(),
type: z.string(),
})
}),
)
.mutation(async ({ ctx: { team }, input }) => {
const extension = input.name.split(".").pop();
@@ -239,7 +239,7 @@ export const campaignRouter = createTRPCRouter({
const url = await getDocumentUploadUrl(
`${team.id}/${randomName}`,
input.type
input.type,
);
const imageUrl = `${env.S3_COMPATIBLE_PUBLIC_URL}/${team.id}/${randomName}`;

View File

@@ -1,17 +1,17 @@
import { Prisma } from "@prisma/client";
import { TRPCError } from "@trpc/server";
import { EmailRenderer } from "@unsend/email-editor/src/renderer";
import { EmailRenderer } from "@usesend/email-editor/src/renderer";
import { z } from "zod";
import { env } from "~/env";
import {
teamProcedure,
createTRPCRouter,
templateProcedure
templateProcedure,
} from "~/server/api/trpc";
import { nanoid } from "~/server/nanoid";
import {
getDocumentUploadUrl,
isStorageConfigured
isStorageConfigured,
} from "~/server/service/storage-service";
export const templateRouter = createTRPCRouter({
@@ -19,19 +19,17 @@ export const templateRouter = createTRPCRouter({
.input(
z.object({
page: z.number().optional(),
})
}),
)
.query(async ({ ctx: { db, team }, input }) => {
const page = input.page || 1;
const limit = 30;
const offset = (page - 1) * limit;
const whereConditions: Prisma.TemplateFindManyArgs["where"] = {
teamId: team.id,
};
const countP = db.template.count({ where: whereConditions });
const templatesP = db.template.findMany({
@@ -61,7 +59,7 @@ export const templateRouter = createTRPCRouter({
z.object({
name: z.string(),
subject: z.string(),
})
}),
)
.mutation(async ({ ctx: { db, team }, input }) => {
const template = await db.template.create({
@@ -80,7 +78,7 @@ export const templateRouter = createTRPCRouter({
name: z.string().optional(),
subject: z.string().optional(),
content: z.string().optional(),
})
}),
)
.mutation(async ({ ctx: { db }, input }) => {
const { templateId, ...data } = input;
@@ -109,7 +107,7 @@ export const templateRouter = createTRPCRouter({
where: { id: input.templateId, teamId: team.id },
});
return template;
}
},
),
getTemplate: templateProcedure.query(async ({ ctx: { db, team }, input }) => {
@@ -139,12 +137,12 @@ export const templateRouter = createTRPCRouter({
name: `${template.name} (Copy)`,
subject: template.subject,
content: template.content,
teamId: team.id
teamId: team.id,
},
});
return newTemplate;
}
},
),
generateImagePresignedUrl: templateProcedure
@@ -152,7 +150,7 @@ export const templateRouter = createTRPCRouter({
z.object({
name: z.string(),
type: z.string(),
})
}),
)
.mutation(async ({ ctx: { team }, input }) => {
const extension = input.name.split(".").pop();
@@ -160,7 +158,7 @@ export const templateRouter = createTRPCRouter({
const url = await getDocumentUploadUrl(
`${team.id}/${randomName}`,
input.type
input.type,
);
const imageUrl = `${env.S3_COMPATIBLE_PUBLIC_URL}/${team.id}/${randomName}`;

View File

@@ -85,7 +85,8 @@ function generateKeyPair() {
export async function addDomain(
domain: string,
region: string,
sesTenantId?: string
sesTenantId?: string,
dkimSelector: string = "usesend",
) {
const sesClient = getSesClient(region);
@@ -93,7 +94,7 @@ export async function addDomain(
const command = new CreateEmailIdentityCommand({
EmailIdentity: domain,
DkimSigningAttributes: {
DomainSigningSelector: "unsend",
DomainSigningSelector: dkimSelector,
DomainSigningPrivateKey: privateKey,
},
});
@@ -114,13 +115,13 @@ export async function addDomain(
});
const tenantResourceAssociationResponse = await sesClient.send(
tenantResourceAssociationCommand
tenantResourceAssociationCommand,
);
if (tenantResourceAssociationResponse.$metadata.httpStatusCode !== 200) {
logger.error(
{ tenantResourceAssociationResponse },
"Failed to associate domain with tenant"
"Failed to associate domain with tenant",
);
throw new Error("Failed to associate domain with tenant");
}
@@ -132,7 +133,7 @@ export async function addDomain(
) {
logger.error(
{ response, emailIdentityResponse },
"Failed to create domain identity"
"Failed to create domain identity",
);
throw new Error("Failed to create domain identity");
}
@@ -143,7 +144,7 @@ export async function addDomain(
export async function deleteDomain(
domain: string,
region: string,
sesTenantId?: string
sesTenantId?: string,
) {
const sesClient = getSesClient(region);
@@ -155,13 +156,13 @@ export async function deleteDomain(
});
const tenantResourceAssociationResponse = await sesClient.send(
tenantResourceAssociationCommand
tenantResourceAssociationCommand,
);
if (tenantResourceAssociationResponse.$metadata.httpStatusCode !== 200) {
logger.error(
{ tenantResourceAssociationResponse },
"Failed to delete tenant resource association"
"Failed to delete tenant resource association",
);
throw new Error("Failed to delete tenant resource association");
}
@@ -233,7 +234,9 @@ export async function sendRawEmail({
bcc,
headers: {
"X-Entity-Ref-ID": nanoid(),
...(emailId ? { "X-Unsend-Email-ID": emailId } : {}),
...(emailId
? { "X-Usesend-Email-ID": emailId, "X-Unsend-Email-ID": emailId }
: {}),
...(unsubUrl
? {
"List-Unsubscribe": `<${unsubUrl}>`,
@@ -289,7 +292,7 @@ export async function addWebhookConfiguration(
configName: string,
topicArn: string,
eventTypes: EventType[],
region: string
region: string,
) {
const sesClient = getSesClient(region);
@@ -305,7 +308,7 @@ export async function addWebhookConfiguration(
const command = new CreateConfigurationSetEventDestinationCommand({
ConfigurationSetName: configName, // required
EventDestinationName: "unsend_destination", // required
EventDestinationName: "usesend_destination", // required
EventDestination: {
Enabled: true,
MatchingEventTypes: eventTypes,

View File

@@ -16,7 +16,7 @@ interface OtpEmailProps {
export function OtpEmail({
otpCode,
loginUrl,
hostName = "Unsend",
hostName = "useSend",
logoUrl,
}: OtpEmailProps) {
return (
@@ -45,7 +45,7 @@ export function OtpEmail({
textAlign: "left" as const,
}}
>
Use the verification code below to sign in to your Unsend account:
Use the verification code below to sign in to your useSend account:
</Text>
<Container

View File

@@ -22,7 +22,7 @@ export function TeamInviteEmail({
role = "member",
}: TeamInviteEmailProps) {
return (
<EmailLayout preview={`You've been invited to join ${teamName} on Unsend`}>
<EmailLayout preview={`You've been invited to join ${teamName} on useSend`}>
<EmailHeader logoUrl={logoUrl} title="You're invited!" />
<Container style={{ padding: "20px 0", textAlign: "left" as const }}>
@@ -50,7 +50,7 @@ export function TeamInviteEmail({
{inviterName
? `${inviterName} has invited you to join `
: "You have been invited to join "}
<strong style={{ color: "#000000" }}>{teamName}</strong> on Unsend
<strong style={{ color: "#000000" }}>{teamName}</strong> on useSend
{role && role !== "member" && (
<span>
{" "}

View File

@@ -7,8 +7,8 @@ interface EmailFooterProps {
}
export function EmailFooter({
companyName = "Unsend",
supportUrl = "https://unsend.dev"
companyName = "useSend",
supportUrl = "https://usesend.com"
}: EmailFooterProps) {
return (
<Container
@@ -40,4 +40,4 @@ export function EmailFooter({
</Text>
</Container>
);
}
}

View File

@@ -17,7 +17,7 @@ export function EmailHeader({ logoUrl, title }: EmailHeaderProps) {
{logoUrl && (
<Img
src={logoUrl}
alt="Unsend"
alt="useSend"
style={{
width: "48px",
height: "48px",
@@ -42,4 +42,4 @@ export function EmailHeader({ logoUrl, title }: EmailHeaderProps) {
)}
</Container>
);
}
}

View File

@@ -7,8 +7,8 @@ async function testEmailTemplates() {
// Test OTP email
const otpHtml = await renderOtpEmail({
otpCode: 'ABC123',
loginUrl: 'https://app.unsend.dev/login?token=abc123',
hostName: 'Unsend',
loginUrl: 'https://app.usesend.com/login?token=abc123',
hostName: 'useSend',
});
console.log('✅ OTP Email rendered successfully');
@@ -17,7 +17,7 @@ async function testEmailTemplates() {
// Test Team Invite email
const inviteHtml = await renderTeamInviteEmail({
teamName: 'My Awesome Team',
inviteUrl: 'https://app.unsend.dev/join-team?inviteId=123',
inviteUrl: 'https://app.usesend.com/join-team?inviteId=123',
});
console.log('✅ Team Invite Email rendered successfully');
@@ -33,4 +33,4 @@ async function testEmailTemplates() {
if (require.main === module) {
testEmailTemplates();
}
}

View File

@@ -1,5 +1,5 @@
import { env } from "~/env";
import { Unsend } from "unsend";
import { UseSend } from "usesend";
import { isSelfHosted } from "~/utils/common";
import { db } from "./db";
import { getDomains } from "./service/domain-service";
@@ -7,13 +7,13 @@ import { sendEmail } from "./service/email-service";
import { logger } from "./logger/log";
import { renderOtpEmail, renderTeamInviteEmail } from "./email-templates";
let unsend: Unsend | undefined;
let usesend: UseSend | undefined;
const getClient = () => {
if (!unsend) {
unsend = new Unsend(env.UNSEND_API_KEY);
if (!usesend) {
usesend = new UseSend(env.USESEND_API_KEY ?? env.UNSEND_API_KEY);
}
return unsend;
return usesend;
};
export async function sendSignUpEmail(
@@ -28,7 +28,7 @@ export async function sendSignUpEmail(
return;
}
const subject = "Sign in to Unsend";
const subject = "Sign in to useSend";
// Use jsx-email template for beautiful HTML
const html = await renderOtpEmail({
@@ -38,7 +38,7 @@ export async function sendSignUpEmail(
});
// Fallback text version
const text = `Hey,\n\nYou can sign in to Unsend by clicking the below URL:\n${url}\n\nYou can also use this OTP: ${token}\n\nThanks,\nUnsend Team`;
const text = `Hey,\n\nYou can sign in to useSend by clicking the below URL:\n${url}\n\nYou can also use this OTP: ${token}\n\nThanks,\nuseSend Team`;
await sendMail(email, subject, text, html);
}
@@ -55,7 +55,7 @@ export async function sendTeamInviteEmail(
return;
}
const subject = "You have been invited to join Unsend";
const subject = "You have been invited to join useSend";
// Use jsx-email template for beautiful HTML
const html = await renderTeamInviteEmail({
@@ -64,7 +64,7 @@ export async function sendTeamInviteEmail(
});
// Fallback text version
const text = `Hey,\n\nYou have been invited to join the team ${teamName} on Unsend.\n\nYou can accept the invitation by clicking the below URL:\n${url}\n\nThanks,\nUnsend Team`;
const text = `Hey,\n\nYou have been invited to join the team ${teamName} on useSend.\n\nYou can accept the invitation by clicking the below URL:\n${url}\n\nThanks,\nuseSend Team`;
await sendMail(email, subject, text, html);
}
@@ -118,15 +118,15 @@ async function sendMail(
});
if (resp.data) {
logger.info("Email sent using unsend");
logger.info("Email sent using usesend");
return;
} else {
logger.error(
{ code: resp.error?.code, message: resp.error?.message },
"Error sending email using unsend, so fallback to resend"
"Error sending email using usesend, so fallback to resend"
);
}
} else {
throw new Error("UNSEND_API_KEY not found");
throw new Error("USESEND_API_KEY/UNSEND_API_KEY not found");
}
}

View File

@@ -118,7 +118,7 @@ export function getApp() {
openapi: "3.0.0",
info: {
version: "1.0.0",
title: "Unsend API",
title: "useSend API",
},
servers: [{ url: `${env.NEXTAUTH_URL}/api` }],
}));

View File

@@ -1,4 +1,4 @@
import { EmailRenderer } from "@unsend/email-editor/src/renderer";
import { EmailRenderer } from "@usesend/email-editor/src/renderer";
import { db } from "../db";
import { createHash } from "crypto";
import { env } from "~/env";
@@ -263,7 +263,10 @@ async function processContactEmail(jobData: CampaignEmailJob) {
const renderer = new EmailRenderer(jsonContent);
const unsubscribeUrl = createUnsubUrl(contact.id, emailConfig.campaignId);
const oneClickUnsubUrl = createOneClickUnsubUrl(contact.id, emailConfig.campaignId);
const oneClickUnsubUrl = createOneClickUnsubUrl(
contact.id,
emailConfig.campaignId
);
// Check for suppressed emails before processing
const toEmails = [contact.email];
@@ -303,6 +306,7 @@ async function processContactEmail(jobData: CampaignEmailJob) {
},
linkValues: {
"{{unsend_unsubscribe_url}}": unsubscribeUrl,
"{{usesend_unsubscribe_url}}": unsubscribeUrl,
},
});

View File

@@ -41,7 +41,7 @@ export async function validateDomainFromEmail(email: string, teamId: number) {
if (!domain) {
throw new UnsendApiError({
code: "BAD_REQUEST",
message: `Domain: ${fromDomain} of from email is wrong. Use the domain verified by unsend`,
message: `Domain: ${fromDomain} of from email is wrong. Use the domain verified by useSend`,
});
}
@@ -86,7 +86,8 @@ export async function createDomain(
}
const subdomain = tldts.getSubdomain(name);
const publicKey = await ses.addDomain(name, region, sesTenantId);
const dkimSelector = "usesend";
const publicKey = await ses.addDomain(name, region, sesTenantId, dkimSelector);
const domain = await db.domain.create({
data: {
@@ -96,6 +97,7 @@ export async function createDomain(
subdomain,
region,
sesTenantId,
dkimSelector,
},
});

View File

@@ -3,7 +3,7 @@ import { db } from "../db";
import { UnsendApiError } from "~/server/public-api/api-error";
import { EmailQueueService } from "./email-queue-service";
import { validateDomainFromEmail } from "./domain-service";
import { EmailRenderer } from "@unsend/email-editor/src/renderer";
import { EmailRenderer } from "@usesend/email-editor/src/renderer";
import { logger } from "../logger/log";
import { SuppressionService } from "./suppression-service";
@@ -35,7 +35,7 @@ async function checkIfValidEmail(emailId: string) {
export const replaceVariables = (
text: string,
variables: Record<string, string>
variables: Record<string, string>,
) => {
return Object.keys(variables).reduce((accum, key) => {
const re = new RegExp(`{{${key}}}`, "g");
@@ -48,7 +48,7 @@ export const replaceVariables = (
Send transactional email
*/
export async function sendEmail(
emailContent: EmailContent & { teamId: number; apiKeyId?: number }
emailContent: EmailContent & { teamId: number; apiKeyId?: number },
) {
const {
to,
@@ -84,18 +84,18 @@ export async function sendEmail(
const suppressionResults = await SuppressionService.checkMultipleEmails(
allEmailsToCheck,
teamId
teamId,
);
// Filter each field separately
const filteredToEmails = toEmails.filter(
(email) => !suppressionResults[email]
(email) => !suppressionResults[email],
);
const filteredCcEmails = ccEmails.filter(
(email) => !suppressionResults[email]
(email) => !suppressionResults[email],
);
const filteredBccEmails = bccEmails.filter(
(email) => !suppressionResults[email]
(email) => !suppressionResults[email],
);
// Only block the email if all TO recipients are suppressed
@@ -105,7 +105,7 @@ export async function sendEmail(
to,
teamId,
},
"All TO recipients are suppressed. No emails to send."
"All TO recipients are suppressed. No emails to send.",
);
const email = await db.email.create({
@@ -147,7 +147,7 @@ export async function sendEmail(
filteredCc: filteredCcEmails,
teamId,
},
"Some CC recipients were suppressed and filtered out."
"Some CC recipients were suppressed and filtered out.",
);
}
@@ -158,7 +158,7 @@ export async function sendEmail(
filteredBcc: filteredBccEmails,
teamId,
},
"Some BCC recipients were suppressed and filtered out."
"Some BCC recipients were suppressed and filtered out.",
);
}
@@ -181,7 +181,7 @@ export async function sendEmail(
acc[`{{${key}}}`] = variables?.[key] || "";
return acc;
},
{} as Record<string, string>
{} as Record<string, string>,
),
};
@@ -251,7 +251,7 @@ export async function sendEmail(
domain.region,
true,
undefined,
delay
delay,
);
} catch (error: any) {
await db.emailEvent.create({
@@ -280,7 +280,7 @@ export async function updateEmail(
scheduledAt,
}: {
scheduledAt?: string;
}
},
) {
const { email, domain } = await checkIfValidEmail(emailId);
@@ -344,7 +344,7 @@ export async function sendBulkEmails(
teamId: number;
apiKeyId?: number;
}
>
>,
) {
if (emailContents.length === 0) {
throw new UnsendApiError({
@@ -382,18 +382,18 @@ export async function sendBulkEmails(
const suppressionResults = await SuppressionService.checkMultipleEmails(
allEmailsToCheck,
content.teamId
content.teamId,
);
// Filter each field separately
const filteredToEmails = toEmails.filter(
(email) => !suppressionResults[email]
(email) => !suppressionResults[email],
);
const filteredCcEmails = ccEmails.filter(
(email) => !suppressionResults[email]
(email) => !suppressionResults[email],
);
const filteredBccEmails = bccEmails.filter(
(email) => !suppressionResults[email]
(email) => !suppressionResults[email],
);
// Only consider it suppressed if all TO recipients are suppressed
@@ -410,13 +410,13 @@ export async function sendBulkEmails(
suppressed: hasSuppressedToEmails,
suppressedEmails: toEmails.filter((email) => suppressionResults[email]),
suppressedCcEmails: ccEmails.filter(
(email) => suppressionResults[email]
(email) => suppressionResults[email],
),
suppressedBccEmails: bccEmails.filter(
(email) => suppressionResults[email]
(email) => suppressionResults[email],
),
};
})
}),
);
const validEmails = emailChecks.filter((check) => !check.suppressed);
@@ -433,7 +433,7 @@ export async function sendBulkEmails(
suppressedAddresses: info.suppressedEmails,
})),
},
"Filtered suppressed emails from bulk send"
"Filtered suppressed emails from bulk send",
);
}
@@ -490,7 +490,7 @@ export async function sendBulkEmails(
acc[`{{${key}}}`] = variables?.[key] || "";
return acc;
},
{} as Record<string, string>
{} as Record<string, string>,
),
};
@@ -647,7 +647,7 @@ export async function sendBulkEmails(
acc[`{{${key}}}`] = variables?.[key] || "";
return acc;
},
{} as Record<string, string>
{} as Record<string, string>,
),
};
@@ -709,7 +709,7 @@ export async function sendBulkEmails(
} catch (error: any) {
logger.error(
{ err: error, to },
`Failed to create email record for recipient`
`Failed to create email record for recipient`,
);
// Continue processing other emails
}
@@ -744,7 +744,7 @@ export async function sendBulkEmails(
where: { id: email.email.id },
data: { latestStatus: "FAILED" },
});
})
}),
);
throw error;
}

View File

@@ -53,7 +53,7 @@ export async function parseSesHook(data: SesEvent) {
// Handle race condition: If email not found by sesEmailId, try to find by custom header
if (!email) {
const emailIdHeader = data.mail.headers.find(
(h) => h.name === "X-Unsend-Email-ID"
(h) => h.name === "X-Usesend-Email-ID" || h.name === "X-Unsend-Email-ID",
);
if (emailIdHeader?.value) {
@@ -71,7 +71,7 @@ export async function parseSesHook(data: SesEvent) {
});
logger.info(
{ emailId: email.id, sesEmailId },
"Updated email with sesEmailId from webhook (race condition resolved)"
"Updated email with sesEmailId from webhook (race condition resolved)",
);
}
}
@@ -131,8 +131,8 @@ export async function parseSesHook(data: SesEvent) {
? SuppressionReason.HARD_BOUNCE
: SuppressionReason.COMPLAINT,
source: email.id,
})
)
}),
),
);
logger.info(
@@ -141,7 +141,7 @@ export async function parseSesHook(data: SesEvent) {
recipients: recipientEmails,
reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",
},
"Added emails to suppression list due to bounce/complaint"
"Added emails to suppression list due to bounce/complaint",
);
} catch (error) {
logger.error(
@@ -150,7 +150,7 @@ export async function parseSesHook(data: SesEvent) {
recipients: recipientEmails,
error: error instanceof Error ? error.message : "Unknown error",
},
"Failed to add emails to suppression list"
"Failed to add emails to suppression list",
);
// Don't throw error - continue processing the webhook
}
@@ -251,7 +251,7 @@ export async function parseSesHook(data: SesEvent) {
await updateCampaignAnalytics(
email.campaignId,
mailStatus,
isHardBounced
isHardBounced,
);
}
}
@@ -334,7 +334,7 @@ async function checkUnsubscribe({
event === EmailStatus.BOUNCED
? UnsubscribeReason.BOUNCED
: UnsubscribeReason.COMPLAINED,
})
}),
),
]);
}
@@ -390,13 +390,13 @@ export class SesHookParser {
}),
async () => {
await this.execute(job.data);
}
},
);
},
{
connection: getRedis(),
concurrency: 50,
}
},
);
private static async execute(event: SesEvent) {
@@ -412,7 +412,7 @@ export class SesHookParser {
return await this.sesHookQueue.add(
data.messageId,
data.event,
DEFAULT_QUEUE_OPTIONS
DEFAULT_QUEUE_OPTIONS,
);
}
}

View File

@@ -25,7 +25,7 @@ export class SesSettingsService {
private static initialized = false;
public static async getSetting(
region = env.AWS_DEFAULT_REGION
region = env.AWS_DEFAULT_REGION,
): Promise<SesSetting | null> {
await this.checkInitialized();
@@ -46,19 +46,19 @@ export class SesSettingsService {
}
/**
* Creates a new setting in AWS for the given region and unsendUrl
* Creates a new setting in AWS for the given region and usesendUrl
*
* @param region
* @param unsendUrl
* @param usesendUrl
*/
public static async createSesSetting({
region,
unsendUrl,
usesendUrl,
sendingRateLimit,
transactionalQuota,
}: {
region: string;
unsendUrl: string;
usesendUrl: string;
sendingRateLimit: number;
transactionalQuota: number;
}) {
@@ -67,15 +67,15 @@ export class SesSettingsService {
throw new Error(`SesSetting for region ${region} already exists`);
}
const parsedUrl = unsendUrl.endsWith("/")
? unsendUrl.substring(0, unsendUrl.length - 1)
: unsendUrl;
const parsedUrl = usesendUrl.endsWith("/")
? usesendUrl.substring(0, usesendUrl.length - 1)
: usesendUrl;
const unsendUrlValidation = await isValidUnsendUrl(parsedUrl);
const usesendUrlValidation = await isValidUsesendUrl(parsedUrl);
if (!unsendUrlValidation.isValid) {
if (!usesendUrlValidation.isValid) {
throw new Error(
`Unsend URL: ${unsendUrl} is not valid, status: ${unsendUrlValidation.code} message:${unsendUrlValidation.error}`
`Callback URL: ${usesendUrl} is not valid, status: ${usesendUrlValidation.code} message:${usesendUrlValidation.error}`,
);
}
@@ -105,7 +105,7 @@ export class SesSettingsService {
await sns.subscribeEndpoint(
topicArn!,
`${setting.callbackUrl}`,
setting.region
setting.region,
);
return setting;
@@ -120,14 +120,14 @@ export class SesSettingsService {
EmailQueueService.initializeQueue(
region,
setting.sesEmailRateLimit,
setting.transactionalQuota
setting.transactionalQuota,
);
logger.info(
{
transactionalQueue: EmailQueueService.transactionalQueue,
marketingQueue: EmailQueueService.marketingQueue,
},
"Email queues initialized"
"Email queues initialized",
);
await this.invalidateCache();
@@ -138,7 +138,7 @@ export class SesSettingsService {
} catch (deleteError) {
logger.error(
{ err: deleteError },
"Failed to delete SNS topic after error"
"Failed to delete SNS topic after error",
);
}
}
@@ -172,13 +172,13 @@ export class SesSettingsService {
transactionalQueue: EmailQueueService.transactionalQueue,
marketingQueue: EmailQueueService.marketingQueue,
},
"Email queues before update"
"Email queues before update",
);
EmailQueueService.initializeQueue(
setting.region,
setting.sesEmailRateLimit,
setting.transactionalQuota
setting.transactionalQuota,
);
logger.info(
@@ -186,7 +186,7 @@ export class SesSettingsService {
transactionalQueue: EmailQueueService.transactionalQueue,
marketingQueue: EmailQueueService.marketingQueue,
},
"Email queues after update"
"Email queues after update",
);
await this.invalidateCache();
@@ -229,7 +229,7 @@ async function registerConfigurationSet(setting: SesSetting) {
configGeneral,
setting.topicArn,
GENERAL_EVENTS,
setting.region
setting.region,
);
const configClick = `${setting.idPrefix}-${setting.region}-unsend-click`;
@@ -237,7 +237,7 @@ async function registerConfigurationSet(setting: SesSetting) {
configClick,
setting.topicArn,
[...GENERAL_EVENTS, "CLICK"],
setting.region
setting.region,
);
const configOpen = `${setting.idPrefix}-${setting.region}-unsend-open`;
@@ -245,7 +245,7 @@ async function registerConfigurationSet(setting: SesSetting) {
configOpen,
setting.topicArn,
[...GENERAL_EVENTS, "OPEN"],
setting.region
setting.region,
);
const configFull = `${setting.idPrefix}-${setting.region}-unsend-full`;
@@ -253,7 +253,7 @@ async function registerConfigurationSet(setting: SesSetting) {
configFull,
setting.topicArn,
[...GENERAL_EVENTS, "CLICK", "OPEN"],
setting.region
setting.region,
);
return await db.sesSetting.update({
@@ -273,7 +273,7 @@ async function registerConfigurationSet(setting: SesSetting) {
});
}
async function isValidUnsendUrl(url: string) {
async function isValidUsesendUrl(url: string) {
logger.info({ url }, "Checking if URL is valid");
try {
const response = await fetch(`${url}/api/ses_callback`, {