Skill: mupengi-bot/mufi-email-manager (version 1.0.0)

--- name: mufi-email-manager description: 한국형 이메일 통합 관리 도구. Gmail, 네이버, 다음, 카카오 메일을 IMAP/SMTP로 통합 관리. 읽지 않은 메일 요약, 키워드 필터링, 자동 답장, 일일 다이제스트 생성. --- # mufi-email-manager 한국 주요 이메일 서비스(Gmail, 네이버, 다음, 카카오)를 통합 관리하는 IMAP/SMTP 기반 도구입니다. ## 주요 기능 - 📬 **다중 계정 통합**: Gmail, 네이버, 다음, 카카오 메일 동시 관리 - 📊 **스마트 요약**: 읽지 않은 메일 자동 요약 및 분류 - 🔍 **키워드 필터링**: 중요 메일 자동 탐지 (업무, 결제, 보안 등) - 🤖 **자동 답장**: 템플릿 기반 빠른 답장 - 📰 **일일 다이제스트**: 하루 메일 요약 리포트 생성 ## 설정 ### 환경 변수 설정 스킬 폴더에 `.env` 파일을 생성하거나 환경 변수로 설정: ```bash # 기본 계정 (필수) DEFAULT_ACCOUNT=gmail # gmail, naver, daum, kakao 중 선택 # Gmail 계정 GMAIL_USER=[REDACTED_SECRET] GMAIL_PASS=your_app_password GMAIL_IMAP_HOST=imap.gmail.com GMAIL_IMAP_PORT=993 GMAIL_SMTP_HOST=smtp.gmail.com GMAIL_SMTP_PORT=587 # 네이버 메일 NAVER_USER=[REDACTED_SECRET] NAVER_PASS=your_password NAVER_IMAP_HOST=imap.naver.com NAVER_IMAP_PORT=993 NAVER_SMTP_HOST=smtp.naver.com NAVER_SMTP_PORT=587 # 다음(Daum) 메일 DAUM_USER=[REDACTED_SECRET] DAUM_PASS=your_password DAUM_IMAP_HOST=imap.daum.net DAUM_IMAP_PORT=993 DAUM_SMTP_HOST=smtp.daum.net DAUM_SMTP_PORT=465 # 카카오(Kakao) 메일 KAKAO_USER=[REDACTED_SECRET] KAKAO_PASS=your_password KAKAO_IMAP_HOST=imap.kakao.com KAKAO_IMAP_PORT=993 KAKAO_SMTP_HOST=smtp.kakao.com KAKAO_SMTP_PORT=465 # 필터 키워드 (쉼표로 구분) IMPORTANT_KEYWORDS=결제,청구,납부,계약,승인,보안,비밀번호,urgent,invoice SPAM_KEYWORDS=광고,홍보,이벤트,쿠폰,할인 # 다이제스트 설정 DIGEST_ENABLED=true DIGEST_TIME=09:00 DIGEST_RECIPIENTS=[REDACTED_SECRET] ``` ## 한국 이메일 서버 정보 | 서비스 | IMAP 서버 | IMAP 포트 | SMTP 서버 | SMTP 포트 | 비고 | |--------|-----------|-----------|-----------|-----------|------| | Gmail | imap.gmail.com | 993 | smtp.gmail.com | 587 | 2단계 인증 시 앱 비밀번호 필요 | | 네이버 | imap.naver.com | 993 | smtp.naver.com | 587 | IMAP/SMTP 설정 활성화 필요 | | 다음 | imap.daum.net | 993 | smtp.daum.net | 465 | SSL 사용 | | 카카오 | imap.kakao.com | 993 | smtp.kakao.com | 465 | SSL 사용 | | 한메일 | imap.daum.net | 993 | smtp.daum.net | 465 | 다음과 동일 | **중요:** - **Gmail**: 2단계 인증 사용 시 앱 비밀번호 필수 - **네이버**: 메일 설정에서 IMAP/SMTP 사용 설정 필요 - **다음/카카오**: SMTP 포트 465 (SSL 직접 연결) ## 명령어 ### 1. 통합 메일 확인 모든 계정의 읽지 않은 메일 확인: ```bash node scripts/check-all.js [--limit 20] ``` 특정 계정만 확인: ```bash node scripts/check.js --account gmail [--limit 10] node scripts/check.js --account naver [--limit 10] ``` ### 2. 스마트 요약 읽지 않은 메일을 키워드 기반으로 분류하여 요약: ```bash node scripts/summary.js [--account gmail] [--recent 24h] ``` 출력 예시: ``` 📬 읽지 않은 메일 요약 (Gmail) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🔴 중요 (3건) - [결제] 카드 승인 내역 안내 (신한카드) - [보안] 새로운 기기에서 로그인 시도 (Google) - [업무] 프로젝트 마감 알림 (팀장님) 🟡 일반 (12건) - 주간 뉴스레터 (Tech News) - 배송 완료 알림 (쿠팡) ... 🔵 읽을만한 (5건) - 친구 초대장 (Facebook) ... ``` ### 3. 중요 메일 필터링 키워드로 중요 메일만 추출: ```bash node scripts/filter.js --keywords "결제,청구,승인" [--account all] [--recent 7d] ``` ### 4. 자동 답장 템플릿 기반 빠른 답장: ```bash # 템플릿 목록 node scripts/reply.js --list # 템플릿 사용 node scripts/reply.js --uid 12345 --template thanks --account gmail # 커스텀 답장 node scripts/reply.js --uid 12345 --body "감사합니다." --account gmail ``` 기본 템플릿: - `thanks`: 감사 인사 - `confirm`: 확인 완료 - `meeting`: 미팅 일정 조율 - `ooo`: 부재중 자동 응답 ### 5. 일일 다이제스트 하루 메일 요약 리포트 생성: ```bash node scripts/digest.js [--date 2026-02-16] [--accounts gmail,naver] ``` 출력 형식: - 텍스트 리포트 - HTML 이메일로 전송 옵션 - JSON 형식 지원 ### 6. 메일 발송 통합 발송 인터페이스: ```bash node scripts/send.js \ --account gmail \ --to [REDACTED_SECRET] \ --subject "안녕하세요" \ --body "메일 본문입니다." \ [--attach file.pdf] ``` ### 7. 검색 모든 계정에서 통합 검색: ```bash node scripts/search.js \ --query "프로젝트" \ [--accounts gmail,naver] \ [--recent 30d] \ [--limit 50] ``` ## 템플릿 커스터마이징 `scripts/templates.json` 파일에서 답장 템플릿 수정 가능: ```json { "thanks": { "subject": "Re: {original_subject}", "body": "안녕하세요,\n\n메일 감사합니다.\n확인 후 회신 드리겠습니다.\n\n감사합니다." }, "confirm": { "subject": "Re: {original_subject}", "body": "확인 완료했습니다.\n추가 문의 사항 있으시면 연락 주세요." } } ``` ## 크론 작업 예시 매일 아침 9시 다이제스트 발송: ```bash 0 9 * * * cd /path/to/mufi-email-manager && node scripts/digest.js --send ``` 30분마다 중요 메일 체크: ```bash */30 * * * * cd /path/to/mufi-email-manager && node scripts/filter.js --keywords "긴급,urgent" --notify ``` ## 설치 ```bash cd skills/mufi-email-manager npm install ``` ## 보안 주의사항 - `.env` 파일을 `.gitignore`에 추가 - Gmail은 앱 비밀번호 사용 권장 - 비밀번호를 코드에 하드코딩하지 말 것 - 중요 메일은 자동 삭제하지 않도록 주의 ## 트러블슈팅 **연결 실패:** - 서버 주소와 포트 확인 - 방화벽 설정 확인 **인증 실패:** - 이메일 주소와 비밀번호 재확인 - Gmail: 앱 비밀번호 사용 여부 확인 - 네이버: IMAP/SMTP 설정 활성화 여부 확인 **TLS/SSL 에러:** - 다음/카카오는 SMTP 포트 465 (SSL 직접 연결) - Gmail/네이버는 SMTP 포트 587 (STARTTLS) ## 라이센스 MIT

path: package.json
content: {
  "name": "mufi-email-manager",
  "version": "1.0.0",
  "description": "한국형 이메일 통합 관리 도구 (Gmail, 네이버, 다음, 카카오)",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "email",
    "imap",
    "smtp",
    "gmail",
    "naver",
    "daum",
    "kakao",
    "korea"
  ],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "imap": "^0.8.19",
    "mailparser": "^3.7.1",
    "nodemailer": "^6.9.16",
    "dotenv": "^16.4.7"
  }
}


path: scripts/check-all.js
content: #!/usr/bin/env node

/**
 * 모든 계정의 읽지 않은 메일 확인
 */

const { getAllConfiguredAccounts, getAccountConfig } = require('./lib/config');
const ImapClient = require('./lib/imap-client');

function parseArgs() {
  const args = process.argv.slice(2);
  const options = { limit: 20 };

  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--limit' && args[i + 1]) {
      options.limit = parseInt(args[i + 1]);
      i++;
    }
  }

  return options;
}

async function checkAccount(accountName, limit) {
  const config = getAccountConfig(accountName);
  const client = new ImapClient(config.imap);

  try {
    await client.connect();
    const messages = await client.getUnreadMessages(limit);
    
    console.log(`\n📬 ${accountName.toUpperCase()}: ${messages.length}건의 읽지 않은 메일`);
    console.log('━'.repeat(50));

    if (messages.length === 0) {
      console.log('  (읽지 않은 메일 없음)');
    } else {
      messages.forEach((msg, idx) => {
        const date = new Date(msg.date).toLocaleString('ko-KR');
        const subject = msg.subject || '(제목 없음)';
        const from = msg.from || '(발신자 없음)';
        
        console.log(`  ${idx + 1}. [${date}]`);
        console.log(`     제목: ${subject}`);
        console.log(`     발신: ${from}`);
        console.log(`     UID: ${msg.uid}`);
        console.log('');
      });
    }

    await client.disconnect();
    return messages.length;
  } catch (err) {
    console.error(`❌ ${accountName} 오류:`, err.message);
    return 0;
  }
}

async function main() {
  const options = parseArgs();
  const accounts = getAllConfiguredAccounts();

  if (accounts.length === 0) {
    console.error('❌ 설정된 계정이 없습니다. .env 파일을 확인하세요.');
    process.exit(1);
  }

  console.log('🔍 전체 계정 메일 확인 중...\n');

  let totalUnread = 0;
  for (const account of accounts) {
    const count = await checkAccount(account, options.limit);
    totalUnread += count;
  }

  console.log('\n' + '='.repeat(50));
  console.log(`📊 총 ${totalUnread}건의 읽지 않은 메일`);
  console.log('='.repeat(50) + '\n');
}

main().catch(err => {
  console.error('오류:', err.message);
  process.exit(1);
});


path: scripts/check.js
content: #!/usr/bin/env node

/**
 * 특정 계정의 읽지 않은 메일 확인
 */

const { getAccountConfig, DEFAULT_ACCOUNT } = require('./lib/config');
const ImapClient = require('./lib/imap-client');

function parseArgs() {
  const args = process.argv.slice(2);
  const options = { 
    account: DEFAULT_ACCOUNT,
    limit: 10 
  };

  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--account' && args[i + 1]) {
      options.account = args[i + 1];
      i++;
    } else if (args[i] === '--limit' && args[i + 1]) {
      options.limit = parseInt(args[i + 1]);
      i++;
    }
  }

  return options;
}

async function main() {
  const options = parseArgs();
  const config = getAccountConfig(options.account);
  const client = new ImapClient(config.imap);

  try {
    console.log(`🔍 ${options.account.toUpperCase()} 메일 확인 중...\n`);
    
    await client.connect();
    const messages = await client.getUnreadMessages(options.limit);
    
    console.log(`📬 ${messages.length}건의 읽지 않은 메일`);
    console.log('━'.repeat(50) + '\n');

    if (messages.length === 0) {
      console.log('  (읽지 않은 메일 없음)\n');
    } else {
      messages.forEach((msg, idx) => {
        const date = new Date(msg.date).toLocaleString('ko-KR');
        const subject = msg.subject || '(제목 없음)';
        const from = msg.from || '(발신자 없음)';
        
        console.log(`${idx + 1}. [${date}]`);
        console.log(`   제목: ${subject}`);
        console.log(`   발신: ${from}`);
        console.log(`   UID: ${msg.uid}`);
        console.log('');
      });
    }

    await client.disconnect();
  } catch (err) {
    console.error('❌ 오류:', err.message);
    process.exit(1);
  }
}

main().catch(err => {
  console.error('오류:', err.message);
  process.exit(1);
});


path: scripts/filter.js
content: #!/usr/bin/env node

/**
 * 키워드 기반 메일 필터링
 */

const { getAccountConfig, getAllConfiguredAccounts, DEFAULT_ACCOUNT } = require('./lib/config');
const ImapClient = require('./lib/imap-client');

function parseArgs() {
  const args = process.argv.slice(2);
  const options = { 
    account: 'all',
    keywords: '',
    recent: '7d',
    limit: 50
  };

  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--account' && args[i + 1]) {
      options.account = args[i + 1];
      i++;
    } else if (args[i] === '--keywords' && args[i + 1]) {
      options.keywords = args[i + 1];
      i++;
    } else if (args[i] === '--recent' && args[i + 1]) {
      options.recent = args[i + 1];
      i++;
    } else if (args[i] === '--limit' && args[i + 1]) {
      options.limit = parseInt(args[i + 1]);
      i++;
    }
  }

  return options;
}

function parseTimeString(timeStr) {
  const match = timeStr.match(/^(\d+)([mhd])$/);
  if (!match) return 7 * 24;

  const value = parseInt(match[1]);
  const unit = match[2];

  switch (unit) {
    case 'm': return value / 60;
    case 'h': return value;
    case 'd': return value * 24;
    default: return 7 * 24;
  }
}

async function filterAccount(accountName, keywords, hours, limit) {
  const config = getAccountConfig(accountName);
  const client = new ImapClient(config.imap);

  try {
    await client.connect();
    
    const keywordList = keywords.split(',').map(k => k.trim());
    const messages = await client.searchByKeywords(keywordList, limit);
    
    // 시간 필터링
    const cutoffTime = Date.now() - hours * 60 * 60 * 1000;
    const recentMessages = messages.filter(msg => new Date(msg.date).getTime() > cutoffTime);
    
    await client.disconnect();
    return recentMessages;
  } catch (err) {
    console.error(`❌ ${accountName} 오류:`, err.message);
    return [];
  }
}

async function main() {
  const options = parseArgs();
  
  if (!options.keywords) {
    console.error('❌ --keywords 옵션이 필요합니다.');
    console.error('예: node filter.js --keywords "결제,청구,승인"');
    process.exit(1);
  }

  const accounts = options.account === 'all' 
    ? getAllConfiguredAccounts() 
    : [options.account];

  if (accounts.length === 0) {
    console.error('❌ 설정된 계정이 없습니다.');
    process.exit(1);
  }

  const hours = parseTimeString(options.recent);
  console.log(`🔍 키워드 필터링 중: "${options.keywords}"`);
  console.log(`   기간: 최근 ${options.recent}\n`);

  let allMatches = [];

  for (const account of accounts) {
    console.log(`📂 ${account.toUpperCase()} 검색 중...`);
    const messages = await filterAccount(account, options.keywords, hours, options.limit);
    
    if (messages.length > 0) {
      allMatches.push({ account, messages });
      console.log(`   ✅ ${messages.length}건 발견\n`);
    } else {
      console.log(`   (일치하는 메일 없음)\n`);
    }
  }

  // 결과 출력
  if (allMatches.length === 0) {
    console.log('━'.repeat(50));
    console.log('🔍 일치하는 메일이 없습니다.');
    console.log('━'.repeat(50) + '\n');
    return;
  }

  console.log('━'.repeat(50));
  console.log('📋 필터링 결과\n');

  allMatches.forEach(({ account, messages }) => {
    console.log(`📬 ${account.toUpperCase()} (${messages.length}건)`);
    messages.forEach((msg, idx) => {
      const date = new Date(msg.date).toLocaleString('ko-KR');
      const subject = msg.subject || '(제목 없음)';
      const from = msg.from.split('<')[0].trim();
      
      console.log(`  ${idx + 1}. [${date}]`);
      console.log(`     ${subject}`);
      console.log(`     발신: ${from}`);
      console.log(`     UID: ${msg.uid}`);
      console.log('');
    });
  });

  const totalCount = allMatches.reduce((sum, { messages }) => sum + messages.length, 0);
  console.log('━'.repeat(50));
  console.log(`📊 총 ${totalCount}건 발견`);
  console.log('━'.repeat(50) + '\n');
}

main().catch(err => {
  console.error('오류:', err.message);
  process.exit(1);
});


path: scripts/lib/config.js
content: const path = require('path');
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });

// 계정 설정 정보
const ACCOUNTS = {
  gmail: {
    imap: {
      host: process.env.GMAIL_IMAP_HOST || 'imap.gmail.com',
      port: parseInt(process.env.GMAIL_IMAP_PORT) || 993,
      user: process.env.GMAIL_USER,
      [REDACTED_SECRET],
      tls: true,
      tlsOptions: { rejectUnauthorized: true }
    },
    smtp: {
      host: process.env.GMAIL_SMTP_HOST || 'smtp.gmail.com',
      port: parseInt(process.env.GMAIL_SMTP_PORT) || 587,
      secure: false, // STARTTLS
      auth: {
        user: process.env.GMAIL_USER,
        pass: process.env.GMAIL_PASS
      }
    }
  },
  naver: {
    imap: {
      host: process.env.NAVER_IMAP_HOST || 'imap.naver.com',
      port: parseInt(process.env.NAVER_IMAP_PORT) || 993,
      user: process.env.NAVER_USER,
      [REDACTED_SECRET],
      tls: true,
      tlsOptions: { rejectUnauthorized: true }
    },
    smtp: {
      host: process.env.NAVER_SMTP_HOST || 'smtp.naver.com',
      port: parseInt(process.env.NAVER_SMTP_PORT) || 587,
      secure: false, // STARTTLS
      auth: {
        user: process.env.NAVER_USER,
        pass: process.env.NAVER_PASS
      }
    }
  },
  daum: {
    imap: {
      host: process.env.DAUM_IMAP_HOST || 'imap.daum.net',
      port: parseInt(process.env.DAUM_IMAP_PORT) || 993,
      user: process.env.DAUM_USER,
      [REDACTED_SECRET],
      tls: true,
      tlsOptions: { rejectUnauthorized: true }
    },
    smtp: {
      host: process.env.DAUM_SMTP_HOST || 'smtp.daum.net',
      port: parseInt(process.env.DAUM_SMTP_PORT) || 465,
      secure: true, // SSL
      auth: {
        user: process.env.DAUM_USER,
        pass: process.env.DAUM_PASS
      }
    }
  },
  kakao: {
    imap: {
      host: process.env.KAKAO_IMAP_HOST || 'imap.kakao.com',
      port: parseInt(process.env.KAKAO_IMAP_PORT) || 993,
      user: process.env.KAKAO_USER,
      [REDACTED_SECRET],
      tls: true,
      tlsOptions: { rejectUnauthorized: true }
    },
    smtp: {
      host: process.env.KAKAO_SMTP_HOST || 'smtp.kakao.com',
      port: parseInt(process.env.KAKAO_SMTP_PORT) || 465,
      secure: true, // SSL
      auth: {
        user: process.env.KAKAO_USER,
        pass: process.env.KAKAO_PASS
      }
    }
  }
};

// 필터 키워드
const IMPORTANT_KEYWORDS = (process.env.IMPORTANT_KEYWORDS || '결제,청구,납부,계약,승인,보안,비밀번호,urgent,invoice').split(',');
const SPAM_KEYWORDS = (process.env.SPAM_KEYWORDS || '광고,홍보,이벤트,쿠폰,할인').split(',');

// 기본 계정
const DEFAULT_ACCOUNT = process.env.DEFAULT_ACCOUNT || 'gmail';

function getAccountConfig(accountName) {
  const account = ACCOUNTS[accountName];
  if (!account) {
    throw new Error(`Unknown account: ${accountName}. Available: ${Object.keys(ACCOUNTS).join(', ')}`);
  }
  if (!account.imap.user || !account.imap.password) {
    throw new Error(`Account ${accountName} not configured. Set ${accountName.toUpperCase()}_USER and ${accountName.toUpperCase()}_PASS in .env`);
  }
  return account;
}

function getAllConfiguredAccounts() {
  return Object.keys(ACCOUNTS).filter(name => {
    const account = ACCOUNTS[name];
    return account.imap.user && account.imap.password;
  });
}

module.exports = {
  ACCOUNTS,
  IMPORTANT_KEYWORDS,
  SPAM_KEYWORDS,
  DEFAULT_ACCOUNT,
  getAccountConfig,
  getAllConfiguredAccounts
};


path: scripts/lib/imap-client.js
content: const Imap = require('imap');
const { simpleParser } = require('mailparser');

class ImapClient {
  constructor(config) {
    this.config = config;
    this.imap = null;
  }

  async connect() {
    return new Promise((resolve, reject) => {
      this.imap = new Imap({
        ...this.config,
        connTimeout: 10000,
        authTimeout: 10000
      });

      this.imap.once('ready', () => resolve());
      this.imap.once('error', (err) => reject(new Error(`IMAP connection failed: ${err.message}`)));
      this.imap.connect();
    });
  }

  async disconnect() {
    if (this.imap) {
      this.imap.end();
    }
  }

  async openBox(mailbox = 'INBOX', readOnly = true) {
    return new Promise((resolve, reject) => {
      this.imap.openBox(mailbox, readOnly, (err, box) => {
        if (err) reject(err);
        else resolve(box);
      });
    });
  }

  async search(criteria, mailbox = 'INBOX') {
    await this.openBox(mailbox, true);
    return new Promise((resolve, reject) => {
      this.imap.search(criteria, (err, uids) => {
        if (err) reject(err);
        else resolve(uids || []);
      });
    });
  }

  async fetchMessages(uids, options = {}) {
    if (!uids || uids.length === 0) return [];

    const messages = [];
    const fetch = this.imap.fetch(uids, {
      bodies: '',
      struct: true,
      ...options
    });

    return new Promise((resolve, reject) => {
      fetch.on('message', (msg, seqno) => {
        let buffer = '';
        let attrs = null;

        msg.on('body', (stream) => {
          stream.on('data', (chunk) => {
            buffer += chunk.toString('utf8');
          });
        });

        msg.once('attributes', (a) => {
          attrs = a;
        });

        msg.once('end', async () => {
          try {
            const parsed = await simpleParser(buffer);
            messages.push({
              uid: attrs.uid,
              flags: attrs.flags,
              date: attrs.date,
              subject: parsed.subject,
              from: parsed.from?.text || '',
              to: parsed.to?.text || '',
              text: parsed.text || '',
              html: parsed.html || '',
              attachments: parsed.attachments || []
            });
          } catch (err) {
            console.error(`Error parsing message ${seqno}:`, err.message);
          }
        });
      });

      fetch.once('error', reject);
      fetch.once('end', () => resolve(messages));
    });
  }

  async getUnreadMessages(limit = 10, mailbox = 'INBOX') {
    const uids = await this.search(['UNSEEN'], mailbox);
    const limitedUids = uids.slice(-limit);
    return this.fetchMessages(limitedUids);
  }

  async getRecentMessages(hours = 24, limit = 20, mailbox = 'INBOX') {
    const sinceDate = new Date(Date.now() - hours * 60 * 60 * 1000);
    const uids = await this.search(['SINCE', sinceDate], mailbox);
    const limitedUids = uids.slice(-limit);
    return this.fetchMessages(limitedUids);
  }

  async searchByKeywords(keywords, limit = 50, mailbox = 'INBOX') {
    const allMessages = [];
    
    for (const keyword of keywords) {
      const subjectUids = await this.search(['SUBJECT', keyword], mailbox);
      const bodyUids = await this.search(['BODY', keyword], mailbox);
      const combinedUids = [...new Set([...subjectUids, ...bodyUids])];
      
      if (combinedUids.length > 0) {
        const messages = await this.fetchMessages(combinedUids.slice(-limit));
        allMessages.push(...messages);
      }
    }

    // 중복 제거 (uid 기준)
    const uniqueMessages = Array.from(
      new Map(allMessages.map(m => [m.uid, m])).values()
    );

    return uniqueMessages.slice(-limit);
  }

  async markAsRead(uids, mailbox = 'INBOX') {
    await this.openBox(mailbox, false); // read-write mode
    return new Promise((resolve, reject) => {
      this.imap.addFlags(uids, '\\Seen', (err) => {
        if (err) reject(err);
        else resolve();
      });
    });
  }

  async markAsUnread(uids, mailbox = 'INBOX') {
    await this.openBox(mailbox, false);
    return new Promise((resolve, reject) => {
      this.imap.delFlags(uids, '\\Seen', (err) => {
        if (err) reject(err);
        else resolve();
      });
    });
  }
}

module.exports = ImapClient;


path: scripts/lib/smtp-client.js
content: const nodemailer = require('nodemailer');

class SmtpClient {
  constructor(config) {
    this.config = config;
    this.transporter = null;
  }

  async connect() {
    this.transporter = nodemailer.createTransport(this.config);
    await this.transporter.verify();
  }

  async sendMail(options) {
    if (!this.transporter) {
      await this.connect();
    }

    const mailOptions = {
      from: this.config.auth.user,
      ...options
    };

    return this.transporter.sendMail(mailOptions);
  }

  async sendSimple(to, subject, body, options = {}) {
    return this.sendMail({
      to,
      subject,
      text: body,
      ...options
    });
  }

  async sendHtml(to, subject, html, options = {}) {
    return this.sendMail({
      to,
      subject,
      html,
      ...options
    });
  }

  async reply(originalMessage, body, options = {}) {
    const replySubject = originalMessage.subject.startsWith('Re:') 
      ? originalMessage.subject 
      : `Re: ${originalMessage.subject}`;

    return this.sendMail({
      to: originalMessage.from,
      subject: replySubject,
      text: body,
      inReplyTo: originalMessage.messageId,
      references: originalMessage.references || originalMessage.messageId,
      ...options
    });
  }
}

module.exports = SmtpClient;


path: scripts/reply.js
content: #!/usr/bin/env node

/**
 * 자동 답장 (템플릿 기반)
 */

const fs = require('fs');
const path = require('path');
const { getAccountConfig, DEFAULT_ACCOUNT } = require('./lib/config');
const ImapClient = require('./lib/imap-client');
const SmtpClient = require('./lib/smtp-client');

const TEMPLATES_FILE = path.join(__dirname, 'templates.json');

function parseArgs() {
  const args = process.argv.slice(2);
  const options = { 
    account: DEFAULT_ACCOUNT,
    uid: null,
    template: null,
    body: null,
    list: false
  };

  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--account' && args[i + 1]) {
      options.account = args[i + 1];
      i++;
    } else if (args[i] === '--uid' && args[i + 1]) {
      options.uid = parseInt(args[i + 1]);
      i++;
    } else if (args[i] === '--template' && args[i + 1]) {
      options.template = args[i + 1];
      i++;
    } else if (args[i] === '--body' && args[i + 1]) {
      options.body = args[i + 1];
      i++;
    } else if (args[i] === '--list') {
      options.list = true;
    }
  }

  return options;
}

function loadTemplates() {
  if (!fs.existsSync(TEMPLATES_FILE)) {
    return {};
  }
  return JSON.parse(fs.readFileSync(TEMPLATES_FILE, 'utf8'));
}

function listTemplates() {
  const templates = loadTemplates();
  
  console.log('📝 사용 가능한 답장 템플릿\n');
  console.log('━'.repeat(50));
  
  Object.entries(templates).forEach(([name, template]) => {
    console.log(`\n🏷️  ${name}`);
    console.log(`   제목: ${template.subject}`);
    console.log(`   내용:`);
    template.body.split('\n').forEach(line => {
      console.log(`     ${line}`);
    });
  });
  
  console.log('\n' + '━'.repeat(50));
  console.log(`\n사용법: node reply.js --uid <UID> --template ${Object.keys(templates)[0]} --account gmail\n`);
}

async function main() {
  const options = parseArgs();

  if (options.list) {
    listTemplates();
    return;
  }

  if (!options.uid) {
    console.error('❌ --uid 옵션이 필요합니다.');
    console.error('예: node reply.js --uid 12345 --template thanks --account gmail');
    process.exit(1);
  }

  if (!options.template && !options.body) {
    console.error('❌ --template 또는 --body 옵션이 필요합니다.');
    console.error('템플릿 목록: node reply.js --list');
    process.exit(1);
  }

  const config = getAccountConfig(options.account);
  const imapClient = new ImapClient(config.imap);
  const smtpClient = new SmtpClient(config.smtp);

  try {
    // 원본 메일 조회
    console.log(`🔍 원본 메일 조회 중... (UID: ${options.uid})\n`);
    await imapClient.connect();
    const messages = await imapClient.fetchMessages([options.uid]);
    
    if (messages.length === 0) {
      console.error('❌ 메일을 찾을 수 없습니다.');
      await imapClient.disconnect();
      process.exit(1);
    }

    const originalMessage = messages[0];
    console.log(`📧 원본 메일:`);
    console.log(`   제목: ${originalMessage.subject}`);
    console.log(`   발신: ${originalMessage.from}\n`);

    // 답장 내용 준비
    let replyBody = options.body;
    let replySubject = `Re: ${originalMessage.subject}`;

    if (options.template) {
      const templates = loadTemplates();
      const template = templates[options.template];
      
      if (!template) {
        console.error(`❌ 템플릿 '${options.template}'을 찾을 수 없습니다.`);
        console.error('템플릿 목록: node reply.js --list');
        await imapClient.disconnect();
        process.exit(1);
      }

      replyBody = template.body.replace('{original_subject}', originalMessage.subject);
      replySubject = template.subject.replace('{original_subject}', originalMessage.subject);
    }

    // 답장 발송
    console.log(`📤 답장 발송 중...\n`);
    await smtpClient.sendMail({
      to: originalMessage.from,
      subject: replySubject,
      text: replyBody
    });

    console.log('✅ 답장을 성공적으로 발송했습니다.\n');
    console.log(`━`.repeat(50));
    console.log(`제목: ${replySubject}`);
    console.log(`수신: ${originalMessage.from}`);
    console.log(`내용:\n${replyBody}`);
    console.log(`━`.repeat(50) + '\n');

    await imapClient.disconnect();
  } catch (err) {
    console.error('❌ 오류:', err.message);
    process.exit(1);
  }
}

main().catch(err => {
  console.error('오류:', err.message);
  process.exit(1);
});


path: scripts/summary.js
content: #!/usr/bin/env node

/**
 * 스마트 메일 요약 - 키워드 기반 분류
 */

const { getAccountConfig, DEFAULT_ACCOUNT, IMPORTANT_KEYWORDS, SPAM_KEYWORDS } = require('./lib/config');
const ImapClient = require('./lib/imap-client');

function parseArgs() {
  const args = process.argv.slice(2);
  const options = { 
    account: DEFAULT_ACCOUNT,
    recent: '24h'
  };

  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--account' && args[i + 1]) {
      options.account = args[i + 1];
      i++;
    } else if (args[i] === '--recent' && args[i + 1]) {
      options.recent = args[i + 1];
      i++;
    }
  }

  return options;
}

function parseTimeString(timeStr) {
  const match = timeStr.match(/^(\d+)([mhd])$/);
  if (!match) return 24; // default 24 hours

  const value = parseInt(match[1]);
  const unit = match[2];

  switch (unit) {
    case 'm': return value / 60;
    case 'h': return value;
    case 'd': return value * 24;
    default: return 24;
  }
}

function categorizeMessage(msg) {
  const text = `${msg.subject} ${msg.from}`.toLowerCase();
  
  // 중요 메일 체크
  for (const keyword of IMPORTANT_KEYWORDS) {
    if (text.includes(keyword.toLowerCase())) {
      return 'important';
    }
  }
  
  // 스팸/광고 체크
  for (const keyword of SPAM_KEYWORDS) {
    if (text.includes(keyword.toLowerCase())) {
      return 'spam';
    }
  }
  
  return 'normal';
}

async function main() {
  const options = parseArgs();
  const config = getAccountConfig(options.account);
  const client = new ImapClient(config.imap);

  try {
    console.log(`🔍 ${options.account.toUpperCase()} 메일 요약 생성 중...\n`);
    
    await client.connect();
    const hours = parseTimeString(options.recent);
    const messages = await client.getRecentMessages(hours, 100);
    
    // 읽지 않은 메일만 필터링
    const unreadMessages = messages.filter(msg => !msg.flags.includes('\\Seen'));
    
    // 카테고리별 분류
    const categorized = {
      important: [],
      normal: [],
      spam: []
    };

    unreadMessages.forEach(msg => {
      const category = categorizeMessage(msg);
      categorized[category].push(msg);
    });

    // 출력
    console.log(`📬 읽지 않은 메일 요약 (${options.account.toUpperCase()})`);
    console.log('━'.repeat(50) + '\n');

    // 중요 메일
    console.log(`🔴 중요 (${categorized.important.length}건)`);
    if (categorized.important.length > 0) {
      categorized.important.forEach((msg, idx) => {
        const subject = msg.subject || '(제목 없음)';
        const from = msg.from.split('<')[0].trim();
        console.log(`  ${idx + 1}. ${subject} (${from})`);
      });
    } else {
      console.log('  (없음)');
    }
    console.log('');

    // 일반 메일
    console.log(`🟡 일반 (${categorized.normal.length}건)`);
    if (categorized.normal.length > 0) {
      categorized.normal.slice(0, 10).forEach((msg, idx) => {
        const subject = msg.subject || '(제목 없음)';
        const from = msg.from.split('<')[0].trim();
        console.log(`  ${idx + 1}. ${subject} (${from})`);
      });
      if (categorized.normal.length > 10) {
        console.log(`  ... 외 ${categorized.normal.length - 10}건`);
      }
    } else {
      console.log('  (없음)');
    }
    console.log('');

    // 광고/스팸
    console.log(`🔵 광고/프로모션 (${categorized.spam.length}건)`);
    if (categorized.spam.length > 0) {
      categorized.spam.slice(0, 5).forEach((msg, idx) => {
        const subject = msg.subject || '(제목 없음)';
        console.log(`  ${idx + 1}. ${subject}`);
      });
      if (categorized.spam.length > 5) {
        console.log(`  ... 외 ${categorized.spam.length - 5}건`);
      }
    } else {
      console.log('  (없음)');
    }
    console.log('');

    console.log('━'.repeat(50));
    console.log(`📊 총 ${unreadMessages.length}건 (최근 ${options.recent})`);
    console.log('━'.repeat(50) + '\n');

    await client.disconnect();
  } catch (err) {
    console.error('❌ 오류:', err.message);
    process.exit(1);
  }
}

main().catch(err => {
  console.error('오류:', err.message);
  process.exit(1);
});


path: scripts/templates.json
content: {
  "thanks": {
    "subject": "Re: {original_subject}",
    "body": "안녕하세요,\n\n메일 감사합니다.\n확인 후 회신 드리겠습니다.\n\n감사합니다."
  },
  "confirm": {
    "subject": "Re: {original_subject}",
    "body": "안녕하세요,\n\n확인 완료했습니다.\n추가 문의 사항 있으시면 언제든 연락 주세요.\n\n감사합니다."
  },
  "meeting": {
    "subject": "Re: {original_subject}",
    "body": "안녕하세요,\n\n미팅 일정 관련하여 아래 시간대 중 가능하신 시간 알려주시면 감사하겠습니다:\n\n- [시간1]\n- [시간2]\n- [시간3]\n\n감사합니다."
  },
  "ooo": {
    "subject": "자동 응답: {original_subject}",
    "body": "안녕하세요,\n\n현재 부재중입니다.\n긴급한 용건은 [연락처]로 연락 주시기 바랍니다.\n\n감사합니다."
  },
  "received": {
    "subject": "Re: {original_subject}",
    "body": "안녕하세요,\n\n메일 잘 받았습니다.\n검토 후 다시 연락드리겠습니다.\n\n감사합니다."
  }
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)