@@ -79,10 +79,8 @@ export function hasEmailService(): boolean {
7979
8080export async function sendEmail ( options : EmailOptions ) : Promise < SendEmailResult > {
8181 try {
82- // Check if user has unsubscribed (skip for critical transactional emails)
8382 if ( options . emailType !== 'transactional' ) {
8483 const unsubscribeType = options . emailType as 'marketing' | 'updates' | 'notifications'
85- // For arrays, check the first email address (batch emails typically go to similar recipients)
8684 const primaryEmail = Array . isArray ( options . to ) ? options . to [ 0 ] : options . to
8785 const hasUnsubscribed = await isUnsubscribed ( primaryEmail , unsubscribeType )
8886 if ( hasUnsubscribed ) {
@@ -99,10 +97,8 @@ export async function sendEmail(options: EmailOptions): Promise<SendEmailResult>
9997 }
10098 }
10199
102- // Process email data with unsubscribe tokens and headers
103100 const processedData = await processEmailData ( options )
104101
105- // Try Resend first if configured
106102 if ( resend ) {
107103 try {
108104 return await sendWithResend ( processedData )
@@ -111,7 +107,6 @@ export async function sendEmail(options: EmailOptions): Promise<SendEmailResult>
111107 }
112108 }
113109
114- // Fallback to Azure Communication Services if configured
115110 if ( azureEmailClient ) {
116111 try {
117112 return await sendWithAzure ( processedData )
@@ -124,7 +119,6 @@ export async function sendEmail(options: EmailOptions): Promise<SendEmailResult>
124119 }
125120 }
126121
127- // No email service configured
128122 logger . info ( 'Email not sent (no email service configured):' , {
129123 to : options . to ,
130124 subject : options . subject ,
@@ -144,6 +138,32 @@ export async function sendEmail(options: EmailOptions): Promise<SendEmailResult>
144138 }
145139}
146140
141+ interface UnsubscribeData {
142+ headers : Record < string , string >
143+ html ?: string
144+ text ?: string
145+ }
146+
147+ function addUnsubscribeData (
148+ recipientEmail : string ,
149+ emailType : string ,
150+ html ?: string ,
151+ text ?: string
152+ ) : UnsubscribeData {
153+ const unsubscribeToken = generateUnsubscribeToken ( recipientEmail , emailType )
154+ const baseUrl = getBaseUrl ( )
155+ const unsubscribeUrl = `${ baseUrl } /unsubscribe?token=${ unsubscribeToken } &email=${ encodeURIComponent ( recipientEmail ) } `
156+
157+ return {
158+ headers : {
159+ 'List-Unsubscribe' : `<${ unsubscribeUrl } >` ,
160+ 'List-Unsubscribe-Post' : 'List-Unsubscribe=One-Click' ,
161+ } ,
162+ html : html ?. replace ( / \{ \{ U N S U B S C R I B E _ T O K E N \} \} / g, unsubscribeToken ) ,
163+ text : text ?. replace ( / \{ \{ U N S U B S C R I B E _ T O K E N \} \} / g, unsubscribeToken ) ,
164+ }
165+ }
166+
147167async function processEmailData ( options : EmailOptions ) : Promise < ProcessedEmailData > {
148168 const {
149169 to,
@@ -159,27 +179,16 @@ async function processEmailData(options: EmailOptions): Promise<ProcessedEmailDa
159179
160180 const senderEmail = from || getFromEmailAddress ( )
161181
162- // Generate unsubscribe token and add to content
163182 let finalHtml = html
164183 let finalText = text
165- const headers : Record < string , string > = { }
184+ let headers : Record < string , string > = { }
166185
167186 if ( includeUnsubscribe && emailType !== 'transactional' ) {
168- // For arrays, use the first email for unsubscribe (batch emails typically go to similar recipients)
169187 const primaryEmail = Array . isArray ( to ) ? to [ 0 ] : to
170- const unsubscribeToken = generateUnsubscribeToken ( primaryEmail , emailType )
171- const baseUrl = getBaseUrl ( )
172- const unsubscribeUrl = `${ baseUrl } /unsubscribe?token=${ unsubscribeToken } &email=${ encodeURIComponent ( primaryEmail ) } `
173-
174- headers [ 'List-Unsubscribe' ] = `<${ unsubscribeUrl } >`
175- headers [ 'List-Unsubscribe-Post' ] = 'List-Unsubscribe=One-Click'
176-
177- if ( html ) {
178- finalHtml = html . replace ( / \{ \{ U N S U B S C R I B E _ T O K E N \} \} / g, unsubscribeToken )
179- }
180- if ( text ) {
181- finalText = text . replace ( / \{ \{ U N S U B S C R I B E _ T O K E N \} \} / g, unsubscribeToken )
182- }
188+ const unsubData = addUnsubscribeData ( primaryEmail , emailType , html , text )
189+ headers = unsubData . headers
190+ finalHtml = unsubData . html
191+ finalText = unsubData . text
183192 }
184193
185194 return {
@@ -234,13 +243,10 @@ async function sendWithResend(data: ProcessedEmailData): Promise<SendEmailResult
234243async function sendWithAzure ( data : ProcessedEmailData ) : Promise < SendEmailResult > {
235244 if ( ! azureEmailClient ) throw new Error ( 'Azure Communication Services not configured' )
236245
237- // Azure Communication Services requires at least one content type
238246 if ( ! data . html && ! data . text ) {
239247 throw new Error ( 'Azure Communication Services requires either HTML or text content' )
240248 }
241249
242- // For Azure, use just the email address part (no display name)
243- // Azure will use the display name configured in the portal for the sender address
244250 const senderEmailOnly = data . senderEmail . includes ( '<' )
245251 ? data . senderEmail . match ( / < ( .+ ) > / ) ?. [ 1 ] || data . senderEmail
246252 : data . senderEmail
@@ -281,7 +287,6 @@ export async function sendBatchEmails(options: BatchEmailOptions): Promise<Batch
281287 try {
282288 const results : SendEmailResult [ ] = [ ]
283289
284- // Try Resend first for batch emails if available
285290 if ( resend ) {
286291 try {
287292 return await sendBatchWithResend ( options . emails )
@@ -290,7 +295,6 @@ export async function sendBatchEmails(options: BatchEmailOptions): Promise<Batch
290295 }
291296 }
292297
293- // Fallback to individual sends (works with both Azure and Resend)
294298 logger . info ( 'Sending batch emails individually' )
295299 for ( const email of options . emails ) {
296300 try {
@@ -328,17 +332,57 @@ async function sendBatchWithResend(emails: EmailOptions[]): Promise<BatchSendEma
328332 if ( ! resend ) throw new Error ( 'Resend not configured' )
329333
330334 const results : SendEmailResult [ ] = [ ]
331- const batchEmails = emails . map ( ( email ) => {
335+ const skippedIndices : number [ ] = [ ]
336+ const batchEmails : any [ ] = [ ]
337+
338+ for ( let i = 0 ; i < emails . length ; i ++ ) {
339+ const email = emails [ i ]
340+ const { emailType = 'transactional' , includeUnsubscribe = true } = email
341+
342+ if ( emailType !== 'transactional' ) {
343+ const unsubscribeType = emailType as 'marketing' | 'updates' | 'notifications'
344+ const primaryEmail = Array . isArray ( email . to ) ? email . to [ 0 ] : email . to
345+ const hasUnsubscribed = await isUnsubscribed ( primaryEmail , unsubscribeType )
346+ if ( hasUnsubscribed ) {
347+ skippedIndices . push ( i )
348+ results . push ( {
349+ success : true ,
350+ message : 'Email skipped (user unsubscribed)' ,
351+ data : { id : 'skipped-unsubscribed' } ,
352+ } )
353+ continue
354+ }
355+ }
356+
332357 const senderEmail = email . from || getFromEmailAddress ( )
333358 const emailData : any = {
334359 from : senderEmail ,
335360 to : email . to ,
336361 subject : email . subject ,
337362 }
363+
338364 if ( email . html ) emailData . html = email . html
339365 if ( email . text ) emailData . text = email . text
340- return emailData
341- } )
366+
367+ if ( includeUnsubscribe && emailType !== 'transactional' ) {
368+ const primaryEmail = Array . isArray ( email . to ) ? email . to [ 0 ] : email . to
369+ const unsubData = addUnsubscribeData ( primaryEmail , emailType , email . html , email . text )
370+ emailData . headers = unsubData . headers
371+ if ( unsubData . html ) emailData . html = unsubData . html
372+ if ( unsubData . text ) emailData . text = unsubData . text
373+ }
374+
375+ batchEmails . push ( emailData )
376+ }
377+
378+ if ( batchEmails . length === 0 ) {
379+ return {
380+ success : true ,
381+ message : 'All batch emails skipped (users unsubscribed)' ,
382+ results,
383+ data : { count : 0 } ,
384+ }
385+ }
342386
343387 try {
344388 const response = await resend . batch . send ( batchEmails as any )
@@ -347,7 +391,6 @@ async function sendBatchWithResend(emails: EmailOptions[]): Promise<BatchSendEma
347391 throw new Error ( response . error . message || 'Resend batch API error' )
348392 }
349393
350- // Success - create results for each email
351394 batchEmails . forEach ( ( _ , index ) => {
352395 results . push ( {
353396 success : true ,
@@ -358,12 +401,15 @@ async function sendBatchWithResend(emails: EmailOptions[]): Promise<BatchSendEma
358401
359402 return {
360403 success : true ,
361- message : 'All batch emails sent successfully via Resend' ,
404+ message :
405+ skippedIndices . length > 0
406+ ? `${ batchEmails . length } emails sent, ${ skippedIndices . length } skipped (unsubscribed)`
407+ : 'All batch emails sent successfully via Resend' ,
362408 results,
363- data : { count : results . length } ,
409+ data : { count : batchEmails . length } ,
364410 }
365411 } catch ( error ) {
366412 logger . error ( 'Resend batch send failed:' , error )
367- throw error // Let the caller handle fallback
413+ throw error
368414 }
369415}
0 commit comments