setup email
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import 'server-only';
|
||||
|
||||
import type { Transporter } from 'nodemailer';
|
||||
import type SMTPTransport from 'nodemailer/lib/smtp-transport';
|
||||
import { getEmailConfigurationForUse } from '@/features/foundation/email/server/email-config.service';
|
||||
import { SmtpEmailProvider } from '@/features/foundation/email/server/smtp-provider';
|
||||
import type {
|
||||
NotificationChannel,
|
||||
NotificationProvider,
|
||||
@@ -9,126 +9,59 @@ import type {
|
||||
NotificationProviderSendResult,
|
||||
} from '../types';
|
||||
|
||||
type SmtpConfig = {
|
||||
host: string;
|
||||
port: number;
|
||||
secure: boolean;
|
||||
user?: string;
|
||||
pass?: string;
|
||||
from: string;
|
||||
};
|
||||
|
||||
function parseBoolean(value: string | undefined, fallback: boolean) {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return value === 'true' || value === '1';
|
||||
}
|
||||
|
||||
function getSmtpConfig(): SmtpConfig {
|
||||
const host = process.env.SMTP_HOST?.trim() ?? '';
|
||||
const port = Number(process.env.SMTP_PORT ?? '587');
|
||||
const secure = parseBoolean(process.env.SMTP_SECURE, port === 465);
|
||||
const user = process.env.SMTP_USER?.trim();
|
||||
const pass = process.env.SMTP_PASS?.trim();
|
||||
const from = process.env.SMTP_FROM?.trim() ?? '';
|
||||
|
||||
return { host, port, secure, user, pass, from };
|
||||
}
|
||||
|
||||
async function createTransporter() {
|
||||
const nodemailer = await import('nodemailer');
|
||||
const config = getSmtpConfig();
|
||||
|
||||
return nodemailer.createTransport({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
secure: config.secure,
|
||||
auth: config.user ? { user: config.user, pass: config.pass } : undefined,
|
||||
} satisfies SMTPTransport.Options);
|
||||
}
|
||||
|
||||
function formatRecipients(
|
||||
recipients: NotificationProviderSendInput['to'],
|
||||
): string[] {
|
||||
return recipients.map((recipient) =>
|
||||
recipient.name?.trim()
|
||||
? `"${recipient.name.trim()}" <${recipient.email}>`
|
||||
: recipient.email,
|
||||
);
|
||||
}
|
||||
|
||||
export class EmailNotificationProvider implements NotificationProvider {
|
||||
readonly channel: NotificationChannel = 'email';
|
||||
readonly key = 'smtp';
|
||||
|
||||
constructor(private readonly organizationId: string) {}
|
||||
|
||||
private async resolveConfig() {
|
||||
const config = await getEmailConfigurationForUse(this.organizationId, 'smtp');
|
||||
if (!config) {
|
||||
throw new Error('No active SMTP email configuration was found for this organization.');
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async validate() {
|
||||
const config = getSmtpConfig();
|
||||
|
||||
if (!config.host) {
|
||||
throw new Error('SMTP host is not configured.');
|
||||
}
|
||||
|
||||
if (!Number.isFinite(config.port) || config.port <= 0) {
|
||||
throw new Error('SMTP port is invalid.');
|
||||
}
|
||||
|
||||
if (!config.from) {
|
||||
throw new Error('SMTP from address is not configured.');
|
||||
}
|
||||
await this.resolveConfig();
|
||||
}
|
||||
|
||||
async healthCheck() {
|
||||
try {
|
||||
await this.validate();
|
||||
const transporter = await createTransporter();
|
||||
await transporter.verify();
|
||||
const config = await this.resolveConfig();
|
||||
const provider = new SmtpEmailProvider();
|
||||
const result = await provider.testConnection(config);
|
||||
|
||||
return { ok: true as const };
|
||||
return result.ok
|
||||
? { ok: true }
|
||||
: { ok: false, detail: result.detail ?? 'SMTP health check failed.' };
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false as const,
|
||||
ok: false,
|
||||
detail: error instanceof Error ? error.message : 'Unknown SMTP health check failure',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async send(
|
||||
input: NotificationProviderSendInput,
|
||||
): Promise<NotificationProviderSendResult> {
|
||||
await this.validate();
|
||||
const transporter = (await createTransporter()) as Transporter;
|
||||
const config = getSmtpConfig();
|
||||
async send(input: NotificationProviderSendInput): Promise<NotificationProviderSendResult> {
|
||||
const config = await this.resolveConfig();
|
||||
const provider = new SmtpEmailProvider();
|
||||
|
||||
const info = await transporter.sendMail({
|
||||
from: config.from,
|
||||
to: formatRecipients(input.to),
|
||||
cc: input.cc.length ? formatRecipients(input.cc) : undefined,
|
||||
bcc: input.bcc.length ? formatRecipients(input.bcc) : undefined,
|
||||
replyTo: input.replyTo ?? undefined,
|
||||
return provider.sendEmail(config, {
|
||||
subject: input.subject,
|
||||
text: input.bodyText,
|
||||
html: input.bodyHtml ?? undefined,
|
||||
html: input.bodyHtml,
|
||||
to: input.to,
|
||||
cc: input.cc,
|
||||
bcc: input.bcc,
|
||||
replyTo: input.replyTo,
|
||||
attachments: input.attachments.map((attachment, index) => ({
|
||||
filename: attachment.fileName,
|
||||
content: input.attachmentBuffers[index],
|
||||
fileName: attachment.fileName,
|
||||
contentType: attachment.contentType,
|
||||
buffer: input.attachmentBuffers[index] ?? Buffer.alloc(0),
|
||||
})),
|
||||
});
|
||||
|
||||
return {
|
||||
providerMessageId: info.messageId ?? null,
|
||||
acceptedRecipients: Array.isArray(info.accepted)
|
||||
? info.accepted.map((value: string | { address: string }) => String(value))
|
||||
: [],
|
||||
rejectedRecipients: Array.isArray(info.rejected)
|
||||
? info.rejected.map((value: string | { address: string }) => String(value))
|
||||
: [],
|
||||
metadata: {
|
||||
envelope: info.envelope,
|
||||
response: info.response ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user