Email queue + async sending; legal settings and placeholders
- Add in-memory email queue with rate limiting (MAX_EMAILS_PER_HOUR) - Bulk send to event attendees now queues and returns immediately - Frontend shows 'Emails are being sent in the background' - Legal pages, settings, and placeholders updates Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
defaultTemplates,
|
||||
type DefaultTemplate
|
||||
} from './emailTemplates.js';
|
||||
import { enqueueBulkEmails, type TemplateEmailJobParams } from './emailQueue.js';
|
||||
import nodemailer from 'nodemailer';
|
||||
import type { Transporter } from 'nodemailer';
|
||||
|
||||
@@ -1173,6 +1174,100 @@ export const emailService = {
|
||||
errors,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Queue emails for event attendees (non-blocking).
|
||||
* Adds all matching recipients to the background email queue and returns immediately.
|
||||
* Rate limiting and actual sending is handled by the email queue.
|
||||
*/
|
||||
async queueEventEmails(params: {
|
||||
eventId: string;
|
||||
templateSlug: string;
|
||||
customVariables?: Record<string, any>;
|
||||
recipientFilter?: 'all' | 'confirmed' | 'pending' | 'checked_in';
|
||||
sentBy: string;
|
||||
}): Promise<{ success: boolean; queuedCount: number; error?: string }> {
|
||||
const { eventId, templateSlug, customVariables = {}, recipientFilter = 'confirmed', sentBy } = params;
|
||||
|
||||
// Validate event exists
|
||||
const event = await dbGet<any>(
|
||||
(db as any)
|
||||
.select()
|
||||
.from(events)
|
||||
.where(eq((events as any).id, eventId))
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
return { success: false, queuedCount: 0, error: 'Event not found' };
|
||||
}
|
||||
|
||||
// Validate template exists
|
||||
const template = await this.getTemplate(templateSlug);
|
||||
if (!template) {
|
||||
return { success: false, queuedCount: 0, error: `Template "${templateSlug}" not found` };
|
||||
}
|
||||
|
||||
// Get tickets based on filter
|
||||
let ticketQuery = (db as any)
|
||||
.select()
|
||||
.from(tickets)
|
||||
.where(eq((tickets as any).eventId, eventId));
|
||||
|
||||
if (recipientFilter !== 'all') {
|
||||
ticketQuery = ticketQuery.where(
|
||||
and(
|
||||
eq((tickets as any).eventId, eventId),
|
||||
eq((tickets as any).status, recipientFilter)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const eventTickets = await dbAll<any>(ticketQuery);
|
||||
|
||||
if (eventTickets.length === 0) {
|
||||
return { success: true, queuedCount: 0, error: 'No recipients found' };
|
||||
}
|
||||
|
||||
// Get site timezone for proper date/time formatting
|
||||
const timezone = await this.getSiteTimezone();
|
||||
|
||||
// Build individual email jobs for the queue
|
||||
const jobs: TemplateEmailJobParams[] = eventTickets.map((ticket: any) => {
|
||||
const locale = ticket.preferredLanguage || 'en';
|
||||
const eventTitle = locale === 'es' && event.titleEs ? event.titleEs : event.title;
|
||||
const fullName = `${ticket.attendeeFirstName} ${ticket.attendeeLastName || ''}`.trim();
|
||||
|
||||
return {
|
||||
templateSlug,
|
||||
to: ticket.attendeeEmail,
|
||||
toName: fullName,
|
||||
locale,
|
||||
eventId: event.id,
|
||||
sentBy,
|
||||
variables: {
|
||||
attendeeName: fullName,
|
||||
attendeeEmail: ticket.attendeeEmail,
|
||||
ticketId: ticket.id,
|
||||
eventTitle,
|
||||
eventDate: this.formatDate(event.startDatetime, locale, timezone),
|
||||
eventTime: this.formatTime(event.startDatetime, locale, timezone),
|
||||
eventLocation: event.location,
|
||||
eventLocationUrl: event.locationUrl || '',
|
||||
...customVariables,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Enqueue all emails for background processing
|
||||
enqueueBulkEmails(jobs);
|
||||
|
||||
console.log(`[Email] Queued ${jobs.length} emails for event "${event.title}" (filter: ${recipientFilter})`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
queuedCount: jobs.length,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a custom email (not from template)
|
||||
@@ -1183,10 +1278,11 @@ export const emailService = {
|
||||
subject: string;
|
||||
bodyHtml: string;
|
||||
bodyText?: string;
|
||||
replyTo?: string;
|
||||
eventId?: string;
|
||||
sentBy: string;
|
||||
sentBy?: string | null;
|
||||
}): Promise<{ success: boolean; logId?: string; error?: string }> {
|
||||
const { to, toName, subject, bodyHtml, bodyText, eventId, sentBy } = params;
|
||||
const { to, toName, subject, bodyHtml, bodyText, replyTo, eventId, sentBy = null } = params;
|
||||
|
||||
const allVariables = {
|
||||
...this.getCommonVariables(),
|
||||
@@ -1208,7 +1304,7 @@ export const emailService = {
|
||||
subject,
|
||||
bodyHtml: finalBodyHtml,
|
||||
status: 'pending',
|
||||
sentBy,
|
||||
sentBy: sentBy || null,
|
||||
createdAt: now,
|
||||
});
|
||||
|
||||
@@ -1218,6 +1314,7 @@ export const emailService = {
|
||||
subject,
|
||||
html: finalBodyHtml,
|
||||
text: bodyText,
|
||||
replyTo,
|
||||
});
|
||||
|
||||
// Update log
|
||||
|
||||
Reference in New Issue
Block a user