265 lines
8.0 KiB
TypeScript
265 lines
8.0 KiB
TypeScript
/**
|
|
* 간단한 메일 발송 서비스 (쿼리 제외)
|
|
* Nodemailer를 사용한 직접 발송
|
|
*/
|
|
|
|
import nodemailer from 'nodemailer';
|
|
import { mailAccountFileService } from './mailAccountFileService';
|
|
import { mailTemplateFileService } from './mailTemplateFileService';
|
|
import { encryptionService } from './encryptionService';
|
|
|
|
export interface SendMailRequest {
|
|
accountId: string;
|
|
templateId?: string;
|
|
to: string[]; // 수신자 이메일 배열
|
|
subject: string;
|
|
variables?: Record<string, string>; // 템플릿 변수 치환
|
|
customHtml?: string; // 템플릿 없이 직접 HTML 작성 시
|
|
}
|
|
|
|
export interface SendMailResult {
|
|
success: boolean;
|
|
messageId?: string;
|
|
accepted?: string[];
|
|
rejected?: string[];
|
|
error?: string;
|
|
}
|
|
|
|
class MailSendSimpleService {
|
|
/**
|
|
* 단일 메일 발송 또는 소규모 발송
|
|
*/
|
|
async sendMail(request: SendMailRequest): Promise<SendMailResult> {
|
|
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. 비밀번호 복호화
|
|
const decryptedPassword = encryptionService.decrypt(account.smtpPassword);
|
|
console.log('🔐 비밀번호 복호화 완료');
|
|
console.log('🔐 암호화된 비밀번호 (일부):', account.smtpPassword.substring(0, 30) + '...');
|
|
console.log('🔐 복호화된 비밀번호 길이:', decryptedPassword.length);
|
|
|
|
// 5. SMTP 연결 생성
|
|
// 포트 465는 SSL/TLS를 사용해야 함
|
|
const isSecure = account.smtpPort === 465 ? true : (account.smtpSecure || false);
|
|
|
|
console.log('📧 SMTP 연결 설정:', {
|
|
host: account.smtpHost,
|
|
port: account.smtpPort,
|
|
secure: isSecure,
|
|
user: account.smtpUsername,
|
|
});
|
|
|
|
const transporter = nodemailer.createTransport({
|
|
host: account.smtpHost,
|
|
port: account.smtpPort,
|
|
secure: isSecure, // SSL/TLS (포트 465는 자동으로 true)
|
|
auth: {
|
|
user: account.smtpUsername,
|
|
pass: decryptedPassword, // 복호화된 비밀번호 사용
|
|
},
|
|
// 타임아웃 설정 (30초)
|
|
connectionTimeout: 30000,
|
|
greetingTimeout: 30000,
|
|
});
|
|
|
|
console.log('📧 메일 발송 시도 중...');
|
|
|
|
// 6. 메일 발송
|
|
const info = await transporter.sendMail({
|
|
from: `"${account.name}" <${account.email}>`,
|
|
to: request.to.join(', '),
|
|
subject: this.replaceVariables(request.subject, request.variables),
|
|
html: htmlContent,
|
|
});
|
|
|
|
console.log('✅ 메일 발송 성공:', {
|
|
messageId: info.messageId,
|
|
accepted: info.accepted,
|
|
rejected: info.rejected,
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
messageId: info.messageId,
|
|
accepted: info.accepted as string[],
|
|
rejected: info.rejected as string[],
|
|
};
|
|
} catch (error) {
|
|
const err = error as Error;
|
|
console.error('❌ 메일 발송 실패:', err.message);
|
|
console.error('❌ 에러 상세:', err);
|
|
return {
|
|
success: false,
|
|
error: err.message,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 템플릿 렌더링 (간단 버전)
|
|
*/
|
|
private renderTemplate(
|
|
template: any,
|
|
variables?: Record<string, string>
|
|
): string {
|
|
let html = '<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">';
|
|
|
|
template.components.forEach((component: any) => {
|
|
switch (component.type) {
|
|
case 'text':
|
|
let content = component.content || '';
|
|
if (variables) {
|
|
content = this.replaceVariables(content, variables);
|
|
}
|
|
html += `<div style="${this.styleObjectToString(component.styles)}">${content}</div>`;
|
|
break;
|
|
|
|
case 'button':
|
|
let buttonText = component.text || 'Button';
|
|
if (variables) {
|
|
buttonText = this.replaceVariables(buttonText, variables);
|
|
}
|
|
html += `
|
|
<a href="${component.url || '#'}" style="
|
|
display: inline-block;
|
|
padding: 12px 24px;
|
|
background-color: ${component.styles?.backgroundColor || '#007bff'};
|
|
color: ${component.styles?.color || 'white'};
|
|
text-decoration: none;
|
|
border-radius: 4px;
|
|
${this.styleObjectToString(component.styles)}
|
|
">${buttonText}</a>
|
|
`;
|
|
break;
|
|
|
|
case 'image':
|
|
html += `<img src="${component.src || ''}" style="max-width: 100%; ${this.styleObjectToString(component.styles)}" />`;
|
|
break;
|
|
|
|
case 'spacer':
|
|
html += `<div style="height: ${component.height || 20}px;"></div>`;
|
|
break;
|
|
}
|
|
});
|
|
|
|
html += '</div>';
|
|
return html;
|
|
}
|
|
|
|
/**
|
|
* 변수 치환
|
|
*/
|
|
private replaceVariables(text: string, variables?: Record<string, string>): 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, string>): 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 {
|
|
console.log('🔌 SMTP 연결 테스트 시작:', accountId);
|
|
|
|
const account = await mailAccountFileService.getAccountById(accountId);
|
|
if (!account) {
|
|
throw new Error('계정을 찾을 수 없습니다.');
|
|
}
|
|
|
|
// 비밀번호 복호화
|
|
const decryptedPassword = encryptionService.decrypt(account.smtpPassword);
|
|
console.log('🔐 비밀번호 복호화 완료');
|
|
|
|
// 포트 465는 SSL/TLS를 사용해야 함
|
|
const isSecure = account.smtpPort === 465 ? true : (account.smtpSecure || false);
|
|
|
|
console.log('🔌 SMTP 연결 설정:', {
|
|
host: account.smtpHost,
|
|
port: account.smtpPort,
|
|
secure: isSecure,
|
|
user: account.smtpUsername,
|
|
});
|
|
|
|
const transporter = nodemailer.createTransport({
|
|
host: account.smtpHost,
|
|
port: account.smtpPort,
|
|
secure: isSecure,
|
|
auth: {
|
|
user: account.smtpUsername,
|
|
pass: decryptedPassword, // 복호화된 비밀번호 사용
|
|
},
|
|
connectionTimeout: 10000, // 10초 타임아웃
|
|
greetingTimeout: 10000,
|
|
});
|
|
|
|
console.log('🔌 SMTP 연결 검증 중...');
|
|
await transporter.verify();
|
|
console.log('✅ SMTP 연결 검증 성공!');
|
|
|
|
return {
|
|
success: true,
|
|
message: 'SMTP 연결 성공!',
|
|
};
|
|
} catch (error) {
|
|
const err = error as Error;
|
|
console.error('❌ SMTP 연결 실패:', err.message);
|
|
return {
|
|
success: false,
|
|
message: `연결 실패: ${err.message}`,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
export const mailSendSimpleService = new MailSendSimpleService();
|
|
|