feat: Add support for custom email recipients in workflow steps
This commit is contained in:
@@ -117,10 +117,25 @@ export async function createEmailWorker() {
|
||||
const fromName = email.fromName || email.project.name;
|
||||
const fromEmail = email.from;
|
||||
|
||||
// Parse custom headers from JSON
|
||||
const customHeaders =
|
||||
email.headers && typeof email.headers === 'object' && !Array.isArray(email.headers)
|
||||
? (email.headers as Record<string, string>)
|
||||
: undefined;
|
||||
|
||||
// Check for custom recipient override in headers
|
||||
const recipientEmail = customHeaders?.['X-Plunk-Recipient-Override'] || email.contact.email;
|
||||
|
||||
// Remove internal headers before sending
|
||||
const publicHeaders = customHeaders ? {...customHeaders} : undefined;
|
||||
if (publicHeaders && 'X-Plunk-Recipient-Override' in publicHeaders) {
|
||||
delete publicHeaders['X-Plunk-Recipient-Override'];
|
||||
}
|
||||
|
||||
// Build recipient with name if available
|
||||
const recipient: {name?: string; email: string} | string = email.toName
|
||||
? {name: email.toName, email: email.contact.email}
|
||||
: email.contact.email;
|
||||
? {name: email.toName, email: recipientEmail}
|
||||
: recipientEmail;
|
||||
|
||||
// Determine tracking based on project settings and email type
|
||||
const shouldTrack = EmailService.shouldTrackEmail(email.project.tracking, email.sourceType);
|
||||
@@ -137,6 +152,7 @@ export async function createEmailWorker() {
|
||||
html: compiledHtml,
|
||||
},
|
||||
reply: email.replyTo || undefined,
|
||||
headers: publicHeaders,
|
||||
tracking: shouldTrack,
|
||||
attachments: email.attachments as {filename: string; content: string; contentType: string}[] | null,
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ interface SendEmailParams {
|
||||
campaignId?: string;
|
||||
workflowExecutionId?: string;
|
||||
workflowStepExecutionId?: string;
|
||||
recipientEmail?: string; // Optional custom recipient email (overrides contact.email)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,7 +194,8 @@ export class EmailService {
|
||||
|
||||
// Check subscription status for marketing emails
|
||||
// Transactional emails should always be sent regardless of subscription status
|
||||
if (sourceType !== EmailSourceType.TRANSACTIONAL) {
|
||||
// Custom recipient emails also bypass subscription checks (they're not in the contact list)
|
||||
if (sourceType !== EmailSourceType.TRANSACTIONAL && !params.recipientEmail) {
|
||||
const contact = await prisma.contact.findUnique({
|
||||
where: {id: params.contactId},
|
||||
select: {subscribed: true},
|
||||
@@ -242,6 +244,12 @@ export class EmailService {
|
||||
signale.warn(`[BILLING_LIMIT] ${limitCheck.message}`);
|
||||
}
|
||||
|
||||
// If custom recipient email is provided, store it in headers for later use
|
||||
const emailHeaders = params.headers ? {...params.headers} : {};
|
||||
if (params.recipientEmail) {
|
||||
emailHeaders['X-Plunk-Recipient-Override'] = params.recipientEmail;
|
||||
}
|
||||
|
||||
const email = await prisma.email.create({
|
||||
data: {
|
||||
projectId: params.projectId,
|
||||
@@ -251,7 +259,7 @@ export class EmailService {
|
||||
from: params.from,
|
||||
fromName: params.fromName,
|
||||
replyTo: params.replyTo,
|
||||
headers: params.headers ? toPrismaJson(params.headers) : undefined,
|
||||
headers: Object.keys(emailHeaders).length > 0 ? toPrismaJson(emailHeaders) : undefined,
|
||||
attachments: params.attachments ? toPrismaJson(params.attachments) : undefined,
|
||||
sourceType,
|
||||
templateId: params.templateId,
|
||||
@@ -361,6 +369,15 @@ export class EmailService {
|
||||
? (email.headers as Record<string, string>)
|
||||
: undefined;
|
||||
|
||||
// Check for custom recipient override in headers
|
||||
const recipientEmail = customHeaders?.['X-Plunk-Recipient-Override'] || email.contact.email;
|
||||
|
||||
// Remove internal headers before sending
|
||||
const publicHeaders = customHeaders ? {...customHeaders} : undefined;
|
||||
if (publicHeaders && 'X-Plunk-Recipient-Override' in publicHeaders) {
|
||||
delete publicHeaders['X-Plunk-Recipient-Override'];
|
||||
}
|
||||
|
||||
// Parse attachments from JSON
|
||||
const attachments =
|
||||
email.attachments && Array.isArray(email.attachments)
|
||||
@@ -376,13 +393,13 @@ export class EmailService {
|
||||
name: fromName,
|
||||
email: fromEmail,
|
||||
},
|
||||
to: [email.contact.email],
|
||||
to: [recipientEmail],
|
||||
content: {
|
||||
subject: formattedEmail.subject,
|
||||
html: compiledHtml,
|
||||
},
|
||||
reply: email.replyTo || undefined,
|
||||
headers: customHeaders,
|
||||
headers: publicHeaders,
|
||||
attachments: attachments,
|
||||
tracking: shouldTrack,
|
||||
});
|
||||
|
||||
@@ -519,18 +519,25 @@ export class WorkflowExecutionService {
|
||||
}
|
||||
|
||||
/**
|
||||
* SEND_EMAIL step - Send an email to the contact
|
||||
* SEND_EMAIL step - Send an email to the contact or a custom recipient
|
||||
*/
|
||||
private static async executeSendEmail(
|
||||
step: WorkflowStepWithTemplate,
|
||||
execution: WorkflowExecutionWithRelations,
|
||||
stepExecution: WorkflowStepExecution,
|
||||
_config: StepConfig,
|
||||
config: StepConfig,
|
||||
): Promise<StepResult> {
|
||||
if (!step.template) {
|
||||
throw new Error('No template configured for SEND_EMAIL step');
|
||||
}
|
||||
|
||||
// Parse step config to get recipient configuration
|
||||
const stepConfig = config && typeof config === 'object' && !Array.isArray(config) ? config : {};
|
||||
const recipientConfig =
|
||||
stepConfig.recipient && typeof stepConfig.recipient === 'object' && !Array.isArray(stepConfig.recipient)
|
||||
? (stepConfig.recipient as {type?: string; customEmail?: string})
|
||||
: {type: 'CONTACT'};
|
||||
|
||||
// Get contact data for variable substitution
|
||||
const contact = execution.contact;
|
||||
const contactData =
|
||||
@@ -558,10 +565,20 @@ export class WorkflowExecutionService {
|
||||
const renderedSubject = this.renderTemplate(step.template.subject, variables);
|
||||
const renderedBody = this.renderTemplate(step.template.body, variables);
|
||||
|
||||
// Determine recipient email
|
||||
let recipientEmail = contact.email;
|
||||
let recipientContactId = contact.id;
|
||||
|
||||
if (recipientConfig.type === 'CUSTOM' && recipientConfig.customEmail) {
|
||||
recipientEmail = recipientConfig.customEmail;
|
||||
// For custom recipients, we don't associate with a contact
|
||||
recipientContactId = contact.id; // Keep original contact for tracking
|
||||
}
|
||||
|
||||
// Send email via EmailService
|
||||
const email = await EmailService.sendWorkflowEmail({
|
||||
projectId: execution.workflow.projectId,
|
||||
contactId: contact.id,
|
||||
contactId: recipientContactId,
|
||||
workflowExecutionId: execution.id,
|
||||
workflowStepExecutionId: stepExecution.id, // Use stepExecution.id, not step.id
|
||||
templateId: step.template.id,
|
||||
@@ -570,11 +587,15 @@ export class WorkflowExecutionService {
|
||||
from: step.template.from,
|
||||
fromName: step.template.fromName || undefined,
|
||||
replyTo: step.template.replyTo || undefined,
|
||||
// Pass custom recipient email if specified
|
||||
recipientEmail: recipientConfig.type === 'CUSTOM' ? recipientEmail : undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
emailId: email.id,
|
||||
sentAt: email.createdAt,
|
||||
recipientType: recipientConfig.type,
|
||||
recipientEmail,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user