/** * 간단한 메일 발송 서비스 (쿼리 제외) * Nodemailer를 사용한 직접 발송 */ import nodemailer from 'nodemailer'; import { mailAccountFileService } from './mailAccountFileService'; import { mailTemplateFileService } from './mailTemplateFileService'; export interface SendMailRequest { accountId: string; templateId?: string; to: string[]; // 수신자 이메일 배열 subject: string; variables?: Record; // 템플릿 변수 치환 customHtml?: string; // 템플릿 없이 직접 HTML 작성 시 } export interface SendMailResult { success: boolean; messageId?: string; accepted?: string[]; rejected?: string[]; error?: string; } class MailSendSimpleService { /** * 단일 메일 발송 또는 소규모 발송 */ async sendMail(request: SendMailRequest): Promise { try { // 1. 계정 조회 const account = await mailAccountFileService.getAccountById(request.accountId); if (!account) { throw new Error('메일 계정을 찾을 수 없습니다.'); } // 2. 계정 활성화 확인 if (account.status !== 'active') { throw new Error('비활성 상태의 계정입니다.'); } // 3. HTML 생성 (템플릿 또는 커스텀) let htmlContent = request.customHtml || ''; if (!htmlContent && request.templateId) { const template = await mailTemplateFileService.getTemplateById(request.templateId); if (!template) { throw new Error('템플릿을 찾을 수 없습니다.'); } htmlContent = this.renderTemplate(template, request.variables); } if (!htmlContent) { throw new Error('메일 내용이 없습니다.'); } // 4. SMTP 연결 생성 const transporter = nodemailer.createTransport({ host: account.smtpHost, port: account.smtpPort, secure: account.smtpSecure, // SSL/TLS auth: { user: account.smtpUsername, pass: account.smtpPassword, }, }); // 5. 메일 발송 const info = await transporter.sendMail({ from: `"${account.name}" <${account.email}>`, to: request.to.join(', '), subject: this.replaceVariables(request.subject, request.variables), html: htmlContent, }); return { success: true, messageId: info.messageId, accepted: info.accepted as string[], rejected: info.rejected as string[], }; } catch (error) { const err = error as Error; return { success: false, error: err.message, }; } } /** * 템플릿 렌더링 (간단 버전) */ private renderTemplate( template: any, variables?: Record ): string { let html = '
'; template.components.forEach((component: any) => { switch (component.type) { case 'text': let content = component.content || ''; if (variables) { content = this.replaceVariables(content, variables); } html += `
${content}
`; break; case 'button': let buttonText = component.text || 'Button'; if (variables) { buttonText = this.replaceVariables(buttonText, variables); } html += ` ${buttonText} `; break; case 'image': html += ``; break; case 'spacer': html += `
`; break; } }); html += '
'; return html; } /** * 변수 치환 */ private replaceVariables(text: string, variables?: Record): string { if (!variables) return text; let result = text; Object.entries(variables).forEach(([key, value]) => { const regex = new RegExp(`\\{${key}\\}`, 'g'); result = result.replace(regex, value); }); return result; } /** * 스타일 객체를 CSS 문자열로 변환 */ private styleObjectToString(styles?: Record): string { if (!styles) return ''; return Object.entries(styles) .map(([key, value]) => `${this.camelToKebab(key)}: ${value}`) .join('; '); } /** * camelCase를 kebab-case로 변환 */ private camelToKebab(str: string): string { return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); } /** * SMTP 연결 테스트 */ async testConnection(accountId: string): Promise<{ success: boolean; message: string }> { try { const account = await mailAccountFileService.getAccountById(accountId); if (!account) { throw new Error('계정을 찾을 수 없습니다.'); } const transporter = nodemailer.createTransport({ host: account.smtpHost, port: account.smtpPort, secure: account.smtpSecure, auth: { user: account.smtpUsername, pass: account.smtpPassword, }, }); await transporter.verify(); return { success: true, message: 'SMTP 연결 성공!', }; } catch (error) { const err = error as Error; return { success: false, message: `연결 실패: ${err.message}`, }; } } } export const mailSendSimpleService = new MailSendSimpleService();