Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ RESEND_API_KEY="" # API key from Resend for email authentication / invites
RESEND_FROM_MARKETING=""
RESEND_FROM_SYSTEM=""
RESEND_FROM_DEFAULT=""
RESEND_FROM_TRUST_PORTAL="" # Sender for Trust Portal access/NDA emails (falls back to RESEND_FROM_SYSTEM)
RESEND_TO_TEST=""
RESEND_REPLY_TO_MARKETING=""
REVALIDATION_SECRET="" # openssl rand -base64 32
Expand Down
1 change: 1 addition & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ GROQ_API_KEY=
RESEND_API_KEY=
RESEND_FROM_SYSTEM= # e.g., noreply@mail.trycomp.ai
RESEND_FROM_DEFAULT= # e.g., hello@mail.trycomp.ai
RESEND_FROM_TRUST_PORTAL= # e.g., Comp AI <noreply@mail.trust.inc> — sender for Trust Portal access/NDA emails (falls back to RESEND_FROM_SYSTEM)

# Background checks
BACKGROUND_CHECK_API_BASE_URL=https://glad-sturgeon-729.convex.site
Expand Down
7 changes: 2 additions & 5 deletions apps/api/src/email/email.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,12 @@ export class EmailController {
})
@ApiResponse({ status: 200, description: 'Email task triggered' })
async sendEmail(@Body() dto: SendEmailDto) {
const fromAddress = dto.system
? (process.env.RESEND_FROM_SYSTEM ?? process.env.RESEND_FROM_DEFAULT)
: (dto.from ?? process.env.RESEND_FROM_DEFAULT);

const handle = await tasks.trigger<typeof sendEmailTask>('send-email', {
to: dto.to,
subject: dto.subject,
html: dto.html,
from: fromAddress,
from: dto.from,
channel: dto.system ? 'system' : 'default',
cc: dto.cc,
scheduledAt: dto.scheduledAt,
attachments: dto.attachments,
Expand Down
28 changes: 17 additions & 11 deletions apps/api/src/email/trigger-email.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
import { render } from '@react-email/render';
import { tasks } from '@trigger.dev/sdk';
import type { ReactElement } from 'react';
import type { sendEmailTask } from '../trigger/email/send-email';
import type { EmailChannel, sendEmailTask } from '../trigger/email/send-email';
import type { EmailAttachment } from './resend';

type TriggerEmailFlags = {
marketing?: boolean;
system?: boolean;
trustPortal?: boolean;
};

function resolveChannel(flags: TriggerEmailFlags): EmailChannel {
if (flags.trustPortal) return 'trustPortal';
if (flags.marketing) return 'marketing';
if (flags.system) return 'system';
return 'default';
}

export async function triggerEmail(params: {
to: string;
subject: string;
react: ReactElement;
marketing?: boolean;
system?: boolean;
trustPortal?: boolean;
cc?: string | string[];
scheduledAt?: string;
attachments?: EmailAttachment[];
}): Promise<{ id: string }> {
try {
const html = await render(params.react);

const fromMarketing = process.env.RESEND_FROM_MARKETING;
const fromSystem = process.env.RESEND_FROM_SYSTEM;
const fromDefault = process.env.RESEND_FROM_DEFAULT;

const fromAddress = params.marketing
? fromMarketing
: params.system
? fromSystem
: fromDefault;
const channel = resolveChannel(params);

const handle = await tasks.trigger<typeof sendEmailTask>('send-email', {
to: params.to,
subject: params.subject,
html,
from: fromAddress ?? undefined,
channel,
cc: params.cc,
scheduledAt: params.scheduledAt,
attachments: params.attachments?.map((att) => ({
Expand Down
39 changes: 37 additions & 2 deletions apps/api/src/trigger/email/send-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,36 @@ const emailQueue = queue({
concurrencyLimit: 10,
});

export const emailChannelSchema = z.enum([
'marketing',
'system',
'trustPortal',
'default',
]);
export type EmailChannel = z.infer<typeof emailChannelSchema>;

function resolveFromAddressForChannel(
channel: EmailChannel | undefined,
): string | undefined {
const fromMarketing = process.env.RESEND_FROM_MARKETING;
const fromSystem = process.env.RESEND_FROM_SYSTEM;
const fromDefault = process.env.RESEND_FROM_DEFAULT;
const fromTrustPortal = process.env.RESEND_FROM_TRUST_PORTAL;

switch (channel) {
case 'trustPortal':
return fromTrustPortal ?? fromSystem;
case 'marketing':
return fromMarketing;
case 'system':
return fromSystem;
case 'default':
return fromDefault;
default:
return undefined;
}
}

export const sendEmailTask = schemaTask({
id: 'send-email',
queue: emailQueue,
Expand All @@ -18,6 +48,7 @@ export const sendEmailTask = schemaTask({
to: z.string(),
subject: z.string(),
html: z.string(),
channel: emailChannelSchema.optional(),
from: z.string().optional(),
cc: z.union([z.string(), z.array(z.string())]).optional(),
scheduledAt: z.string().optional(),
Expand All @@ -40,11 +71,15 @@ export const sendEmailTask = schemaTask({
throw new Error('Resend not initialized - missing API key');
}

const toTest = process.env.RESEND_TO_TEST;
const fromSystem = process.env.RESEND_FROM_SYSTEM;
const fromDefault = process.env.RESEND_FROM_DEFAULT;
const toTest = process.env.RESEND_TO_TEST;

const fromAddress = params.from ?? fromSystem ?? fromDefault;
const fromAddress =
params.from ??
resolveFromAddressForChannel(params.channel) ??
fromSystem ??
fromDefault;
const toAddress = toTest ?? params.to;

if (!fromAddress) {
Expand Down
8 changes: 4 additions & 4 deletions apps/api/src/trust-portal/email.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class TrustEmailService {
organizationName,
ndaSigningLink,
}),
system: true,
trustPortal: true,
});

this.logger.log(`NDA signing email sent to ${toEmail} (ID: ${id})`);
Expand All @@ -49,7 +49,7 @@ export class TrustEmailService {
expiresAt,
portalUrl,
}),
system: true,
trustPortal: true,
});

this.logger.log(`Access granted email sent to ${toEmail} (ID: ${id})`);
Expand All @@ -73,7 +73,7 @@ export class TrustEmailService {
accessLink,
expiresAt,
}),
system: true,
trustPortal: true,
});

this.logger.log(`Access reclaim email sent to ${toEmail} (ID: ${id})`);
Expand Down Expand Up @@ -115,7 +115,7 @@ export class TrustEmailService {
requestedDurationDays,
reviewUrl,
}),
system: true,
trustPortal: true,
});

this.logger.log(
Expand Down
Loading