메일관리 기능 구현 완료

This commit is contained in:
leeheejin
2025-10-13 15:17:34 +09:00
parent b4c5be1f17
commit 95c98cbda3
40 changed files with 2227 additions and 4150 deletions

View File

@@ -109,7 +109,7 @@ export class MailReceiveBasicService {
tls: true,
};
console.log(`📧 IMAP 연결 시도 - 호스트: ${imapConfig.host}, 포트: ${imapConfig.port}, 이메일: ${imapConfig.user}`);
// console.log(`📧 IMAP 연결 시도 - 호스트: ${imapConfig.host}, 포트: ${imapConfig.port}, 이메일: ${imapConfig.user}`);
return new Promise((resolve, reject) => {
const imap = this.createImapConnection(imapConfig);
@@ -117,26 +117,26 @@ export class MailReceiveBasicService {
// 30초 타임아웃 설정
const timeout = setTimeout(() => {
console.error('❌ IMAP 연결 타임아웃 (30초)');
// console.error('❌ IMAP 연결 타임아웃 (30초)');
imap.end();
reject(new Error('IMAP 연결 타임아웃'));
}, 30000);
imap.once('ready', () => {
console.log('✅ IMAP 연결 성공! INBOX 열기 시도...');
// console.log('✅ IMAP 연결 성공! INBOX 열기 시도...');
clearTimeout(timeout);
imap.openBox('INBOX', true, (err: any, box: any) => {
if (err) {
console.error('❌ INBOX 열기 실패:', err);
// console.error('❌ INBOX 열기 실패:', err);
imap.end();
return reject(err);
}
console.log(`📬 INBOX 열림 - 전체 메일 수: ${box.messages.total}`);
// console.log(`📬 INBOX 열림 - 전체 메일 수: ${box.messages.total}`);
const totalMessages = box.messages.total;
if (totalMessages === 0) {
console.log('📭 메일함이 비어있습니다');
// console.log('📭 메일함이 비어있습니다');
imap.end();
return resolve([]);
}
@@ -145,19 +145,19 @@ export class MailReceiveBasicService {
const start = Math.max(1, totalMessages - limit + 1);
const end = totalMessages;
console.log(`📨 메일 가져오기 시작 - 범위: ${start}~${end}`);
// console.log(`📨 메일 가져오기 시작 - 범위: ${start}~${end}`);
const fetch = imap.seq.fetch(`${start}:${end}`, {
bodies: ['HEADER', 'TEXT'],
struct: true,
});
console.log(`📦 fetch 객체 생성 완료`);
// console.log(`📦 fetch 객체 생성 완료`);
let processedCount = 0;
const totalToProcess = end - start + 1;
fetch.on('message', (msg: any, seqno: any) => {
console.log(`📬 메일 #${seqno} 처리 시작`);
// console.log(`📬 메일 #${seqno} 처리 시작`);
let header: string = '';
let body: string = '';
let attributes: any = null;
@@ -207,10 +207,10 @@ export class MailReceiveBasicService {
};
mails.push(mail);
console.log(`✓ 메일 #${seqno} 파싱 완료 (${mails.length}/${totalToProcess})`);
// console.log(`✓ 메일 #${seqno} 파싱 완료 (${mails.length}/${totalToProcess})`);
processedCount++;
} catch (parseError) {
console.error(`메일 #${seqno} 파싱 오류:`, parseError);
// console.error(`메일 #${seqno} 파싱 오류:`, parseError);
processedCount++;
}
}
@@ -219,24 +219,24 @@ export class MailReceiveBasicService {
});
fetch.once('error', (fetchErr: any) => {
console.error('❌ 메일 fetch 에러:', fetchErr);
// console.error('❌ 메일 fetch 에러:', fetchErr);
imap.end();
reject(fetchErr);
});
fetch.once('end', () => {
console.log(`📭 fetch 종료 - 처리 완료 대기 중... (현재: ${mails.length}개)`);
// console.log(`📭 fetch 종료 - 처리 완료 대기 중... (현재: ${mails.length}개)`);
// 모든 메일 처리가 완료될 때까지 대기
const checkComplete = setInterval(() => {
console.log(`⏳ 대기 중 - 처리됨: ${processedCount}/${totalToProcess}, 메일: ${mails.length}`);
// console.log(`⏳ 대기 중 - 처리됨: ${processedCount}/${totalToProcess}, 메일: ${mails.length}개`);
if (processedCount >= totalToProcess) {
clearInterval(checkComplete);
console.log(`✅ 메일 가져오기 완료 - 총 ${mails.length}`);
// console.log(`✅ 메일 가져오기 완료 - 총 ${mails.length}개`);
imap.end();
// 최신 메일이 위로 오도록 정렬
mails.sort((a, b) => b.date.getTime() - a.date.getTime());
console.log(`📤 메일 목록 반환: ${mails.length}`);
// console.log(`📤 메일 목록 반환: ${mails.length}개`);
resolve(mails);
}
}, 100);
@@ -244,7 +244,7 @@ export class MailReceiveBasicService {
// 최대 10초 대기
setTimeout(() => {
clearInterval(checkComplete);
console.log(`⚠️ 타임아웃 - 부분 반환: ${mails.length}/${totalToProcess}`);
// console.log(`⚠️ 타임아웃 - 부분 반환: ${mails.length}/${totalToProcess}개`);
imap.end();
mails.sort((a, b) => b.date.getTime() - a.date.getTime());
resolve(mails);
@@ -254,16 +254,16 @@ export class MailReceiveBasicService {
});
imap.once('error', (imapErr: any) => {
console.error('❌ IMAP 연결 에러:', imapErr.message || imapErr);
// console.error('❌ IMAP 연결 에러:', imapErr.message || imapErr);
clearTimeout(timeout);
reject(imapErr);
});
imap.once('end', () => {
console.log('🔌 IMAP 연결 종료');
// console.log('🔌 IMAP 연결 종료');
});
console.log('🔗 IMAP.connect() 호출...');
// console.log('🔗 IMAP.connect() 호출...');
imap.connect();
});
}
@@ -311,22 +311,36 @@ export class MailReceiveBasicService {
return reject(err);
}
console.log(`📬 INBOX 정보 - 전체 메일: ${box.messages.total}, 요청한 seqno: ${seqno}`);
if (seqno > box.messages.total || seqno < 1) {
console.error(`❌ 유효하지 않은 seqno: ${seqno} (메일 총 개수: ${box.messages.total})`);
imap.end();
return resolve(null);
}
const fetch = imap.seq.fetch(`${seqno}:${seqno}`, {
bodies: '',
struct: true,
});
let mailDetail: MailDetail | null = null;
let parsingComplete = false;
fetch.on('message', (msg: any, seqnum: any) => {
console.log(`📨 메일 메시지 이벤트 발생 - seqnum: ${seqnum}`);
msg.on('body', (stream: any, info: any) => {
console.log(`📝 메일 본문 스트림 시작 - which: ${info.which}`);
let buffer = '';
stream.on('data', (chunk: any) => {
buffer += chunk.toString('utf8');
});
stream.once('end', async () => {
console.log(`✅ 메일 본문 스트림 종료 - 버퍼 크기: ${buffer.length}`);
try {
const parsed = await simpleParser(buffer);
console.log(`✅ 메일 파싱 완료 - 제목: ${parsed.subject}`);
const fromAddress = Array.isArray(parsed.from) ? parsed.from[0] : parsed.from;
const toAddress = Array.isArray(parsed.to) ? parsed.to[0] : parsed.to;
@@ -353,21 +367,48 @@ export class MailReceiveBasicService {
size: att.size || 0,
})),
};
parsingComplete = true;
} catch (parseError) {
console.error('메일 파싱 오류:', parseError);
parsingComplete = true;
}
});
});
// msg 전체가 처리되었을 때 이벤트
msg.once('end', () => {
console.log(`📮 메일 메시지 처리 완료 - seqnum: ${seqnum}`);
});
});
fetch.once('error', (fetchErr: any) => {
console.error(`❌ Fetch 에러:`, fetchErr);
imap.end();
reject(fetchErr);
});
fetch.once('end', () => {
imap.end();
resolve(mailDetail);
console.log(`🏁 Fetch 종료 - parsingComplete: ${parsingComplete}`);
// 비동기 파싱이 완료될 때까지 대기
const waitForParsing = setInterval(() => {
if (parsingComplete) {
clearInterval(waitForParsing);
console.log(`✅ 파싱 완료 대기 종료 - mailDetail이 ${mailDetail ? '존재함' : 'null'}`);
imap.end();
resolve(mailDetail);
}
}, 10); // 10ms마다 체크
// 타임아웃 설정 (10초)
setTimeout(() => {
if (!parsingComplete) {
clearInterval(waitForParsing);
console.error('❌ 파싱 타임아웃');
imap.end();
resolve(mailDetail); // 타임아웃 시에도 현재 상태 반환
}
}, 10000);
});
});
});
@@ -492,6 +533,43 @@ export class MailReceiveBasicService {
}
}
/**
* 오늘 수신한 메일 수 조회 (통계용)
*/
async getTodayReceivedCount(accountId?: string): Promise<number> {
try {
const accounts = accountId
? [await mailAccountFileService.getAccountById(accountId)]
: await mailAccountFileService.getAllAccounts();
const today = new Date();
today.setHours(0, 0, 0, 0);
let totalCount = 0;
for (const account of accounts) {
if (!account) continue;
try {
const mails = await this.fetchMailList(account.id, 100);
const todayMails = mails.filter(mail => {
const mailDate = new Date(mail.date);
return mailDate >= today;
});
totalCount += todayMails.length;
} catch (error) {
// 개별 계정 오류는 무시하고 계속 진행
console.error(`계정 ${account.id} 메일 조회 실패:`, error);
}
}
return totalCount;
} catch (error) {
console.error('오늘 수신 메일 수 조회 실패:', error);
return 0;
}
}
/**
* 첨부파일 다운로드
*/
@@ -533,19 +611,26 @@ export class MailReceiveBasicService {
});
let attachmentResult: { filePath: string; filename: string; contentType: string } | null = null;
let parsingComplete = false;
fetch.on('message', (msg: any, seqnum: any) => {
console.log(`📎 메일 메시지 이벤트 발생 - seqnum: ${seqnum}`);
msg.on('body', (stream: any, info: any) => {
console.log(`📎 메일 본문 스트림 시작`);
let buffer = '';
stream.on('data', (chunk: any) => {
buffer += chunk.toString('utf8');
});
stream.once('end', async () => {
console.log(`📎 메일 본문 스트림 종료 - 버퍼 크기: ${buffer.length}`);
try {
const parsed = await simpleParser(buffer);
console.log(`📎 파싱 완료 - 첨부파일 개수: ${parsed.attachments?.length || 0}`);
if (parsed.attachments && parsed.attachments[attachmentIndex]) {
const attachment = parsed.attachments[attachmentIndex];
console.log(`📎 첨부파일 발견 (index ${attachmentIndex}): ${attachment.filename}`);
// 안전한 파일명 생성
const safeFilename = this.sanitizeFilename(
@@ -557,28 +642,51 @@ export class MailReceiveBasicService {
// 파일 저장
await fs.writeFile(filePath, attachment.content);
console.log(`📎 파일 저장 완료: ${filePath}`);
attachmentResult = {
filePath,
filename: attachment.filename || 'unnamed',
contentType: attachment.contentType || 'application/octet-stream',
};
parsingComplete = true;
} else {
console.log(`❌ 첨부파일 index ${attachmentIndex}를 찾을 수 없음 (총 ${parsed.attachments?.length || 0}개)`);
parsingComplete = true;
}
} catch (parseError) {
console.error('첨부파일 파싱 오류:', parseError);
parsingComplete = true;
}
});
});
});
fetch.once('error', (fetchErr: any) => {
console.error('❌ fetch 오류:', fetchErr);
imap.end();
reject(fetchErr);
});
fetch.once('end', () => {
imap.end();
resolve(attachmentResult);
console.log('📎 fetch.once("end") 호출됨 - 파싱 완료 대기 시작...');
// 파싱 완료를 기다림 (최대 5초)
const checkComplete = setInterval(() => {
if (parsingComplete) {
console.log(`✅ 파싱 완료 확인 - attachmentResult: ${attachmentResult ? '있음' : '없음'}`);
clearInterval(checkComplete);
imap.end();
resolve(attachmentResult);
}
}, 100);
setTimeout(() => {
clearInterval(checkComplete);
console.log(`⚠️ 타임아웃 - attachmentResult: ${attachmentResult ? '있음' : '없음'}`);
imap.end();
resolve(attachmentResult);
}, 5000);
});
});
});