Add unsend campaign feature (#45)
* Add unsend email editor Add email editor Add more email editor Add renderer partial Add more marketing email features * Add more campaign feature * Add variables * Getting there * campaign is there mfs * Add migration
This commit is contained in:
@@ -5,54 +5,120 @@ import { getConfigurationSetName } from "~/utils/ses-utils";
|
||||
import { db } from "../db";
|
||||
import { sendEmailThroughSes, sendEmailWithAttachments } from "../aws/ses";
|
||||
import { getRedis } from "../redis";
|
||||
import { createUnsubUrl } from "./campaign-service";
|
||||
|
||||
function createQueueAndWorker(region: string, quota: number, suffix: string) {
|
||||
const connection = getRedis();
|
||||
|
||||
const queueName = `${region}-${suffix}`;
|
||||
|
||||
const queue = new Queue(queueName, { connection });
|
||||
|
||||
const worker = new Worker(queueName, executeEmail, {
|
||||
concurrency: quota,
|
||||
connection,
|
||||
});
|
||||
|
||||
return { queue, worker };
|
||||
}
|
||||
|
||||
export class EmailQueueService {
|
||||
private static initialized = false;
|
||||
private static regionQueue = new Map<string, Queue>();
|
||||
private static regionWorker = new Map<string, Worker>();
|
||||
public static transactionalQueue = new Map<string, Queue>();
|
||||
private static transactionalWorker = new Map<string, Worker>();
|
||||
public static marketingQueue = new Map<string, Queue>();
|
||||
private static marketingWorker = new Map<string, Worker>();
|
||||
|
||||
public static initializeQueue(region: string, quota: number) {
|
||||
const connection = getRedis();
|
||||
public static initializeQueue(
|
||||
region: string,
|
||||
quota: number,
|
||||
transactionalQuotaPercentage: number
|
||||
) {
|
||||
console.log(`[EmailQueueService]: Initializing queue for region ${region}`);
|
||||
|
||||
const queueName = `${region}-transaction`;
|
||||
const transactionalQuota = Math.floor(
|
||||
(quota * transactionalQuotaPercentage) / 100
|
||||
);
|
||||
const marketingQuota = quota - transactionalQuota;
|
||||
|
||||
const queue = new Queue(queueName, { connection });
|
||||
console.log(
|
||||
"is transactional queue",
|
||||
this.transactionalQueue.has(region),
|
||||
"is marketing queue",
|
||||
this.marketingQueue.has(region)
|
||||
);
|
||||
|
||||
const worker = new Worker(queueName, executeEmail, {
|
||||
limiter: {
|
||||
max: quota,
|
||||
duration: 1000,
|
||||
},
|
||||
concurrency: quota,
|
||||
connection,
|
||||
});
|
||||
if (this.transactionalQueue.has(region)) {
|
||||
console.log(
|
||||
`[EmailQueueService]: Updating transactional quota for region ${region} to ${transactionalQuota}`
|
||||
);
|
||||
const transactionalWorker = this.transactionalWorker.get(region);
|
||||
if (transactionalWorker) {
|
||||
transactionalWorker.concurrency = transactionalQuota;
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`[EmailQueueService]: Creating transactional queue for region ${region} with quota ${transactionalQuota}`
|
||||
);
|
||||
const { queue: transactionalQueue, worker: transactionalWorker } =
|
||||
createQueueAndWorker(region, transactionalQuota ?? 1, "transaction");
|
||||
this.transactionalQueue.set(region, transactionalQueue);
|
||||
this.transactionalWorker.set(region, transactionalWorker);
|
||||
}
|
||||
|
||||
this.regionQueue.set(region, queue);
|
||||
this.regionWorker.set(region, worker);
|
||||
if (this.marketingQueue.has(region)) {
|
||||
console.log(
|
||||
`[EmailQueueService]: Updating marketing quota for region ${region} to ${marketingQuota}`
|
||||
);
|
||||
const marketingWorker = this.marketingWorker.get(region);
|
||||
if (marketingWorker) {
|
||||
marketingWorker.concurrency = marketingQuota;
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`[EmailQueueService]: Creating marketing queue for region ${region} with quota ${marketingQuota}`
|
||||
);
|
||||
const { queue: marketingQueue, worker: marketingWorker } =
|
||||
createQueueAndWorker(region, marketingQuota ?? 1, "marketing");
|
||||
this.marketingQueue.set(region, marketingQueue);
|
||||
this.marketingWorker.set(region, marketingWorker);
|
||||
}
|
||||
}
|
||||
|
||||
public static async queueEmail(emailId: string, region: string) {
|
||||
public static async queueEmail(
|
||||
emailId: string,
|
||||
region: string,
|
||||
transactional: boolean,
|
||||
unsubUrl?: string
|
||||
) {
|
||||
if (!this.initialized) {
|
||||
await this.init();
|
||||
}
|
||||
const queue = this.regionQueue.get(region);
|
||||
const queue = transactional
|
||||
? this.transactionalQueue.get(region)
|
||||
: this.marketingQueue.get(region);
|
||||
if (!queue) {
|
||||
throw new Error(`Queue for region ${region} not found`);
|
||||
}
|
||||
queue.add("send-email", { emailId, timestamp: Date.now() });
|
||||
queue.add("send-email", { emailId, timestamp: Date.now(), unsubUrl });
|
||||
}
|
||||
|
||||
public static async init() {
|
||||
const sesSettings = await db.sesSetting.findMany();
|
||||
for (const sesSetting of sesSettings) {
|
||||
this.initializeQueue(sesSetting.region, sesSetting.sesEmailRateLimit);
|
||||
this.initializeQueue(
|
||||
sesSetting.region,
|
||||
sesSetting.sesEmailRateLimit,
|
||||
sesSetting.transactionalQuota
|
||||
);
|
||||
}
|
||||
this.initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeEmail(job: Job<{ emailId: string; timestamp: number }>) {
|
||||
async function executeEmail(
|
||||
job: Job<{ emailId: string; timestamp: number; unsubUrl?: string }>
|
||||
) {
|
||||
console.log(
|
||||
`[EmailQueueService]: Executing email job ${job.data.emailId}, time elapsed: ${Date.now() - job.data.timestamp}ms`
|
||||
);
|
||||
@@ -88,13 +154,15 @@ async function executeEmail(job: Job<{ emailId: string; timestamp: number }>) {
|
||||
}
|
||||
|
||||
console.log(`[EmailQueueService]: Sending email ${email.id}`);
|
||||
const unsubUrl = job.data.unsubUrl;
|
||||
|
||||
try {
|
||||
const messageId = attachments.length
|
||||
? await sendEmailWithAttachments({
|
||||
to: email.to,
|
||||
from: email.from,
|
||||
subject: email.subject,
|
||||
text: email.text ?? undefined,
|
||||
text: email.text ?? "",
|
||||
html: email.html ?? undefined,
|
||||
region: domain?.region ?? env.AWS_DEFAULT_REGION,
|
||||
configurationSetName,
|
||||
@@ -105,11 +173,12 @@ async function executeEmail(job: Job<{ emailId: string; timestamp: number }>) {
|
||||
from: email.from,
|
||||
subject: email.subject,
|
||||
replyTo: email.replyTo ?? undefined,
|
||||
text: email.text ?? undefined,
|
||||
text: email.text ?? "",
|
||||
html: email.html ?? undefined,
|
||||
region: domain?.region ?? env.AWS_DEFAULT_REGION,
|
||||
configurationSetName,
|
||||
attachments,
|
||||
unsubUrl,
|
||||
});
|
||||
|
||||
// Delete attachments after sending the email
|
||||
|
Reference in New Issue
Block a user