feat: enable List-Unsubscribe headers in campaign emails (#96)

* feat: add `precendence: bulk` header for campaign emails

* feat: create and pass unsubUrl to to email queue for campaign

* fix: add correct `List-Unsubscribe-Post` header value
This commit is contained in:
Marc Seitz
2025-01-22 21:16:12 +01:00
committed by GitHub
parent 2137ba8eab
commit 88ba41059e
3 changed files with 30 additions and 12 deletions

View File

@@ -114,6 +114,7 @@ export async function sendEmailThroughSes({
region, region,
configurationSetName, configurationSetName,
unsubUrl, unsubUrl,
isBulk,
}: Partial<EmailContent> & { }: Partial<EmailContent> & {
region: string; region: string;
configurationSetName: string; configurationSetName: string;
@@ -121,6 +122,7 @@ export async function sendEmailThroughSes({
bcc?: string[]; bcc?: string[];
replyTo?: string[]; replyTo?: string[];
to?: string[]; to?: string[];
isBulk?: boolean;
}) { }) {
const sesClient = getSesClient(region); const sesClient = getSesClient(region);
const command = new SendEmailCommand({ const command = new SendEmailCommand({
@@ -155,14 +157,21 @@ export async function sendEmailThroughSes({
} }
: undefined, : undefined,
}, },
...(unsubUrl
? {
Headers: [ Headers: [
// Spread in any unsubscribe headers if unsubUrl is defined
...(unsubUrl
? [
{ Name: "List-Unsubscribe", Value: `<${unsubUrl}>` }, { Name: "List-Unsubscribe", Value: `<${unsubUrl}>` },
{ Name: "List-Unsubscribe-Post", Value: "One-Click" }, { Name: "List-Unsubscribe-Post", Value: "List-Unsubscribe=One-Click" },
]
: []
),
// Spread in the precedence header if present
...(isBulk
? [{ Name: "Precedence", Value: "bulk" }]
: []
),
], ],
}
: {}),
}, },
}, },
ConfigurationSetName: configurationSetName, ConfigurationSetName: configurationSetName,

View File

@@ -270,9 +270,10 @@ export async function sendCampaignEmail(
// Queue emails // Queue emails
await Promise.all( await Promise.all(
emails.map((email) => emails.map((email) => {
EmailQueueService.queueEmail(email.id, domain.region, false) const unsubscribeUrl = createUnsubUrl(email.contactId, campaignId);
) EmailQueueService.queueEmail(email.id, domain.region, false, unsubscribeUrl);
})
); );
} }

View File

@@ -100,12 +100,13 @@ export class EmailQueueService {
const queue = transactional const queue = transactional
? this.transactionalQueue.get(region) ? this.transactionalQueue.get(region)
: this.marketingQueue.get(region); : this.marketingQueue.get(region);
const isBulk = !transactional;
if (!queue) { if (!queue) {
throw new Error(`Queue for region ${region} not found`); throw new Error(`Queue for region ${region} not found`);
} }
queue.add( queue.add(
emailId, emailId,
{ emailId, timestamp: Date.now(), unsubUrl }, { emailId, timestamp: Date.now(), unsubUrl, isBulk },
{ jobId: emailId, delay } { jobId: emailId, delay }
); );
} }
@@ -169,7 +170,12 @@ export class EmailQueueService {
} }
async function executeEmail( async function executeEmail(
job: Job<{ emailId: string; timestamp: number; unsubUrl?: string }> job: Job<{
emailId: string;
timestamp: number;
unsubUrl?: string;
isBulk?: boolean;
}>
) { ) {
console.log( console.log(
`[EmailQueueService]: Executing email job ${job.data.emailId}, time elapsed: ${Date.now() - job.data.timestamp}ms` `[EmailQueueService]: Executing email job ${job.data.emailId}, time elapsed: ${Date.now() - job.data.timestamp}ms`
@@ -207,6 +213,7 @@ async function executeEmail(
console.log(`[EmailQueueService]: Sending email ${email.id}`); console.log(`[EmailQueueService]: Sending email ${email.id}`);
const unsubUrl = job.data.unsubUrl; const unsubUrl = job.data.unsubUrl;
const isBulk = job.data.isBulk;
const text = email.text const text = email.text
? email.text ? email.text
@@ -240,6 +247,7 @@ async function executeEmail(
configurationSetName, configurationSetName,
attachments, attachments,
unsubUrl, unsubUrl,
isBulk,
}); });
// Delete attachments after sending the email // Delete attachments after sending the email