rebrand to useSend (#210)
This commit is contained in:
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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`, {
|
||||
|
||||
Reference in New Issue
Block a user