메일관리 기능 구현
This commit is contained in:
parent
3fa410cbe4
commit
bf58e0c878
|
|
@ -178,14 +178,19 @@ export class MailAccountFileController {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
// TODO: 실제 SMTP 연결 테스트 구현
|
const account = await mailAccountFileService.getAccountById(id);
|
||||||
// const account = await mailAccountFileService.getAccountById(id);
|
if (!account) {
|
||||||
// nodemailer로 연결 테스트
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '계정을 찾을 수 없습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.json({
|
// mailSendSimpleService의 testConnection 사용
|
||||||
success: true,
|
const { mailSendSimpleService } = require('../services/mailSendSimpleService');
|
||||||
message: '연결 테스트 성공 (미구현)',
|
const result = await mailSendSimpleService.testConnection(id);
|
||||||
});
|
|
||||||
|
return res.json(result);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@ export class MailSendSimpleController {
|
||||||
*/
|
*/
|
||||||
async sendMail(req: Request, res: Response) {
|
async sendMail(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
|
console.log('📧 메일 발송 요청 수신:', { accountId: req.body.accountId, to: req.body.to, subject: req.body.subject });
|
||||||
const { accountId, templateId, to, subject, variables, customHtml } = req.body;
|
const { accountId, templateId, to, subject, variables, customHtml } = req.body;
|
||||||
|
|
||||||
// 필수 파라미터 검증
|
// 필수 파라미터 검증
|
||||||
if (!accountId || !to || !Array.isArray(to) || to.length === 0) {
|
if (!accountId || !to || !Array.isArray(to) || to.length === 0) {
|
||||||
|
console.log('❌ 필수 파라미터 누락');
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '계정 ID와 수신자 이메일이 필요합니다.',
|
message: '계정 ID와 수신자 이메일이 필요합니다.',
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
import { mailAccountFileService } from './mailAccountFileService';
|
import { mailAccountFileService } from './mailAccountFileService';
|
||||||
import { mailTemplateFileService } from './mailTemplateFileService';
|
import { mailTemplateFileService } from './mailTemplateFileService';
|
||||||
|
import { encryptionService } from './encryptionService';
|
||||||
|
|
||||||
export interface SendMailRequest {
|
export interface SendMailRequest {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
|
@ -56,18 +57,39 @@ class MailSendSimpleService {
|
||||||
throw new Error('메일 내용이 없습니다.');
|
throw new Error('메일 내용이 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. SMTP 연결 생성
|
// 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({
|
const transporter = nodemailer.createTransport({
|
||||||
host: account.smtpHost,
|
host: account.smtpHost,
|
||||||
port: account.smtpPort,
|
port: account.smtpPort,
|
||||||
secure: account.smtpSecure, // SSL/TLS
|
secure: isSecure, // SSL/TLS (포트 465는 자동으로 true)
|
||||||
auth: {
|
auth: {
|
||||||
user: account.smtpUsername,
|
user: account.smtpUsername,
|
||||||
pass: account.smtpPassword,
|
pass: decryptedPassword, // 복호화된 비밀번호 사용
|
||||||
},
|
},
|
||||||
|
// 타임아웃 설정 (30초)
|
||||||
|
connectionTimeout: 30000,
|
||||||
|
greetingTimeout: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. 메일 발송
|
console.log('📧 메일 발송 시도 중...');
|
||||||
|
|
||||||
|
// 6. 메일 발송
|
||||||
const info = await transporter.sendMail({
|
const info = await transporter.sendMail({
|
||||||
from: `"${account.name}" <${account.email}>`,
|
from: `"${account.name}" <${account.email}>`,
|
||||||
to: request.to.join(', '),
|
to: request.to.join(', '),
|
||||||
|
|
@ -75,6 +97,12 @@ class MailSendSimpleService {
|
||||||
html: htmlContent,
|
html: htmlContent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('✅ 메일 발송 성공:', {
|
||||||
|
messageId: info.messageId,
|
||||||
|
accepted: info.accepted,
|
||||||
|
rejected: info.rejected,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
messageId: info.messageId,
|
messageId: info.messageId,
|
||||||
|
|
@ -83,6 +111,8 @@ class MailSendSimpleService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
|
console.error('❌ 메일 발송 실패:', err.message);
|
||||||
|
console.error('❌ 에러 상세:', err);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: err.message,
|
error: err.message,
|
||||||
|
|
@ -178,22 +208,42 @@ class MailSendSimpleService {
|
||||||
*/
|
*/
|
||||||
async testConnection(accountId: string): Promise<{ success: boolean; message: string }> {
|
async testConnection(accountId: string): Promise<{ success: boolean; message: string }> {
|
||||||
try {
|
try {
|
||||||
|
console.log('🔌 SMTP 연결 테스트 시작:', accountId);
|
||||||
|
|
||||||
const account = await mailAccountFileService.getAccountById(accountId);
|
const account = await mailAccountFileService.getAccountById(accountId);
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error('계정을 찾을 수 없습니다.');
|
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({
|
const transporter = nodemailer.createTransport({
|
||||||
host: account.smtpHost,
|
host: account.smtpHost,
|
||||||
port: account.smtpPort,
|
port: account.smtpPort,
|
||||||
secure: account.smtpSecure,
|
secure: isSecure,
|
||||||
auth: {
|
auth: {
|
||||||
user: account.smtpUsername,
|
user: account.smtpUsername,
|
||||||
pass: account.smtpPassword,
|
pass: decryptedPassword, // 복호화된 비밀번호 사용
|
||||||
},
|
},
|
||||||
|
connectionTimeout: 10000, // 10초 타임아웃
|
||||||
|
greetingTimeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('🔌 SMTP 연결 검증 중...');
|
||||||
await transporter.verify();
|
await transporter.verify();
|
||||||
|
console.log('✅ SMTP 연결 검증 성공!');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -201,6 +251,7 @@ class MailSendSimpleService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
|
console.error('❌ SMTP 연결 실패:', err.message);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: `연결 실패: ${err.message}`,
|
message: `연결 실패: ${err.message}`,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
createMailAccount,
|
createMailAccount,
|
||||||
updateMailAccount,
|
updateMailAccount,
|
||||||
deleteMailAccount,
|
deleteMailAccount,
|
||||||
|
testMailAccountConnection,
|
||||||
CreateMailAccountDto,
|
CreateMailAccountDto,
|
||||||
UpdateMailAccountDto,
|
UpdateMailAccountDto,
|
||||||
} from "@/lib/api/mail";
|
} from "@/lib/api/mail";
|
||||||
|
|
@ -104,6 +105,24 @@ export default function MailAccountsPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTestConnection = async (account: MailAccount) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await testMailAccountConnection(account.id);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert(`✅ SMTP 연결 성공!\n\n${result.message || '정상적으로 연결되었습니다.'}`);
|
||||||
|
} else {
|
||||||
|
alert(`❌ SMTP 연결 실패\n\n${result.message || '연결에 실패했습니다.'}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('연결 테스트 실패:', error);
|
||||||
|
alert(`❌ SMTP 연결 테스트 실패\n\n${error.message || '알 수 없는 오류가 발생했습니다.'}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
||||||
|
|
@ -148,6 +167,7 @@ export default function MailAccountsPage() {
|
||||||
onEdit={handleOpenEditModal}
|
onEdit={handleOpenEditModal}
|
||||||
onDelete={handleOpenDeleteModal}
|
onDelete={handleOpenDeleteModal}
|
||||||
onToggleStatus={handleToggleStatus}
|
onToggleStatus={handleToggleStatus}
|
||||||
|
onTestConnection={handleTestConnection}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ interface MailAccountTableProps {
|
||||||
onEdit: (account: MailAccount) => void;
|
onEdit: (account: MailAccount) => void;
|
||||||
onDelete: (account: MailAccount) => void;
|
onDelete: (account: MailAccount) => void;
|
||||||
onToggleStatus: (account: MailAccount) => void;
|
onToggleStatus: (account: MailAccount) => void;
|
||||||
|
onTestConnection: (account: MailAccount) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MailAccountTable({
|
export default function MailAccountTable({
|
||||||
|
|
@ -28,6 +29,7 @@ export default function MailAccountTable({
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onToggleStatus,
|
onToggleStatus,
|
||||||
|
onTestConnection,
|
||||||
}: MailAccountTableProps) {
|
}: MailAccountTableProps) {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [sortField, setSortField] = useState<keyof MailAccount>('createdAt');
|
const [sortField, setSortField] = useState<keyof MailAccount>('createdAt');
|
||||||
|
|
@ -220,6 +222,13 @@ export default function MailAccountTable({
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => onTestConnection(account)}
|
||||||
|
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||||
|
title="SMTP 연결 테스트"
|
||||||
|
>
|
||||||
|
<Zap className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onEdit(account)}
|
onClick={() => onEdit(account)}
|
||||||
className="p-2 text-primary hover:bg-accent rounded-lg transition-colors"
|
className="p-2 text-primary hover:bg-accent rounded-lg transition-colors"
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,15 @@ export async function deleteMailAccount(id: string): Promise<{ success: boolean
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP 연결 테스트
|
||||||
|
*/
|
||||||
|
export async function testMailAccountConnection(id: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
return fetchApi<{ success: boolean; message: string }>(`/accounts/${id}/test-connection`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SMTP 연결 테스트
|
* SMTP 연결 테스트
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue