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
@@ -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,
},
});
@@ -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,
},
});
+26 -26
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;
}
+11 -11
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,
);
}
}
@@ -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`, {