naver-mail-mcp-basic
by Hoodess
README.md
# 네이버 메일 MCP 서버 — 기본형 (naver-mail-mcp-basic)
**네이버 메일 조회·검색·첨부 정리·발송준비**를 하는 MCP 서버입니다.
Claude Desktop 에 stdio MCP 로 붙여서 씁니다 (Claude Code / Cowork 도 동일하게 동작).
> 이 레포는 원본 `naver-mail-mcp` 에서 **메일 수발신 기능만 분리한 기본형(수업용)** 입니다.
> 원본은 이후 사건별 자동 분류 파이프라인·수발신 대장·문서 도구가 추가된 확장판으로 발전했습니다.
> 두 서버는 이름·Claude Desktop 등록 키·상태 폴더(`~/.naver-mail-mcp-basic`)가 분리되어
> 같은 컴퓨터에 함께 설치해도 서로 간섭하지 않습니다.
> 🤖 **AI 에이전트에게**: 사용자가 이 레포 주소를 주며 "이 MCP 설치해줘"라고 요청했다면
> **5절의 방법 C** 를 따르세요. 계정 정보(비밀번호)는 채팅으로 묻지도, 대신 입력하지도 마세요.
> 👉 **컴퓨터가 익숙하지 않은 분은 [사용설명서.md](사용설명서.md) 를 보세요.**
> 전문용어 없이 설치부터 사용까지 따라 할 수 있게 쓴 문서입니다. 이 README 는 개발/관리자용입니다.
> 1차 목표는 **첨부 다운로드 + 폴더 정리**입니다.
> 메일 발송은 **절대 자동으로 하지 않습니다.** 발송대기 폴더에 준비만 하고, 사람이 확인 후 직접 보냅니다.
---
## 목차
1. [무엇을 할 수 있나](#1-무엇을-할-수-있나)
2. [네이버 준비 작업 (IMAP 사용 설정 + 앱 비밀번호 발급)](#2-네이버-준비-작업)
3. [설치](#3-설치)
4. [설정](#4-설정)
5. [Claude 에 등록하기](#5-claude-에-등록하기)
6. [수동 테스트 방법](#6-수동-테스트-방법)
7. [MCP 툴 레퍼런스](#7-mcp-툴-레퍼런스)
8. [변호사 사무실 시나리오 예시 프롬프트](#8-변호사-사무실-시나리오-예시-프롬프트)
9. [보안 설계](#9-보안-설계)
10. [설계 가정 및 알려진 제약](#10-설계-가정-및-알려진-제약)
11. [문제 해결](#11-문제-해결)
12. [프로젝트 구조](#12-프로젝트-구조)
---
## 1. 무엇을 할 수 있나
| 기능 | 툴 |
|---|---|
| 읽지 않은 / 새로 온 메일 목록 (첨부 파일명 포함) | `list_unread_mails` |
| 메일 본문·헤더·첨부 메타데이터 조회 | `get_mail` |
| 첨부파일을 로컬 디스크에 저장 + 확장자별 분류 + 이름 규칙 적용 | `download_attachments` |
| **새 메일 확인 → 첨부 다운로드 → 폴더 정리 → rename 을 한 번에** (증분 처리) | `process_new_mails_with_attachments` |
| 발신자 / 제목 / 날짜 / 첨부 유무·확장자로 검색 | `search_mails` |
| 발송 준비 (발송대기 폴더 또는 임시보관함 초안) — **발송 안 함** | `save_draft_or_prepare_send` |
| 발송대기 목록 확인 | `list_prepared_mails` |
| 메일함 목록 / 현재 설정 확인 | `list_mailboxes` |
기본 분류 폴더:
```
<DOWNLOAD_BASE_DIR>/
├── pdf/
├── hwp/
├── hwpx/
├── docx/ ← doc, docx
├── xlsx/ ← xls, xlsx, xlsm, csv
├── images/ ← jpg, jpeg, png, gif, bmp, webp, heic, tif …
├── etc/ ← 그 외 전부 (zip, pptx, html …)
└── _발송대기/ ← save_draft_or_prepare_send 결과
```
---
## 2. 네이버 준비 작업
⚠️ **로그인 비밀번호로는 절대 접속되지 않습니다.** 반드시 아래 두 가지를 먼저 하세요.
### 2-1. IMAP/SMTP 사용 설정
1. PC 웹브라우저로 [네이버 메일](https://mail.naver.com) 접속 후 로그인
2. 왼쪽 아래 **환경설정** (톱니바퀴) 클릭
3. **POP3/IMAP 설정** 탭 → **IMAP/SMTP 설정** 탭 선택
4. **IMAP/SMTP 사용** 을 `사용함` 으로 변경
5. 맨 아래 **확인** 버튼 클릭 (이걸 안 누르면 저장되지 않습니다)
> "IMAP 동기화 메일 제한" 값도 함께 보입니다. 기본 250통이면 오래된 메일이 IMAP 으로 안 보일 수 있으니
> 사무실 상황에 맞게 500~1000통으로 올려두는 것을 권장합니다.
### 2-2. 애플리케이션 비밀번호 발급
2단계 인증을 쓰든 안 쓰든, 외부 메일 프로그램용 비밀번호가 따로 필요합니다.
1. [네이버 내정보](https://nid.naver.com/user2/help/myInfo) → **보안 설정**
2. **2단계 인증** 메뉴로 이동 (꺼져 있으면 먼저 켭니다)
3. **애플리케이션 비밀번호 관리** 클릭
4. **애플리케이션 추가** → 이름을 `naver-mail-mcp-basic` 등으로 입력
5. 생성된 **12자리 비밀번호**를 복사 (창을 닫으면 다시 볼 수 없습니다)
이 12자리가 아래 설정의 `NAVER_APP_PASSWORD` 입니다.
### 2-3. 접속 정보 (참고)
| 항목 | 값 |
|---|---|
| IMAP 서버 | `imap.naver.com` |
| IMAP 포트 | `993` (SSL/TLS 필수) |
| SMTP 서버 | `smtp.naver.com` |
| SMTP 포트 | `587` (STARTTLS) 또는 `465` (SSL) |
| 로그인 ID | 메일주소의 `@` 앞부분 (예: `hong@naver.com` → `hong`) |
> 이 값들은 코드에 하드코딩되어 있지 않습니다. 전부 환경변수 기본값이며 필요하면 바꿀 수 있습니다.
---
## 3. 설치
**요구사항: Node.js 20 이상**
```bash
cd /경로/naver-mail-mcp-basic
npm install
npm run build
```
빌드 결과는 `dist/index.js` 에 생성되며, 이 파일이 MCP 서버 진입점입니다.
---
## 4. 설정
계정 정보를 주는 방법은 두 가지입니다. **둘 중 하나만** 하면 됩니다.
### 방법 A — `.env` 파일 (간단)
```bash
cp .env.example .env
```
`.env` 를 열어 최소 세 줄만 채우면 됩니다.
```dotenv
NAVER_EMAIL=hong@naver.com
NAVER_APP_PASSWORD=여기에_12자리_앱비밀번호
DOWNLOAD_BASE_DIR=/Users/내이름/사건첨부
```
`.env` 는 `.gitignore` 에 들어 있어 커밋되지 않습니다. 파일 권한은 `chmod 600 .env` 로 조여두세요.
### 방법 B — 암호화 저장소 (권장, 평문 비밀번호 없음)
```bash
npm run setup
```
- ID 와 앱 비밀번호를 물어봅니다. 비밀번호는 화면에 표시되지 않습니다.
- `~/.naver-mail-mcp-basic/credentials.enc` 에 **AES-256-GCM** 으로 암호화 저장됩니다.
- 이 경우 `.env` 에서 `NAVER_APP_PASSWORD` 줄은 지워도 됩니다.
암호화 키는 다음 순서로 정해집니다.
1. 환경변수 `NAVER_MCP_MASTER_KEY` 가 있으면 그 값에서 scrypt 로 유도 (**가장 안전**, 키 파일이 디스크에 남지 않음)
2. 없으면 `~/.naver-mail-mcp-basic/master.key` (32바이트 랜덤, 권한 0600)를 자동 생성해 사용
> 2번 방식은 "노트북을 통째로 가져간 공격자"까지 막지는 못합니다(키가 암호문 옆에 있음).
> 다만 백업·동기화·실수로 평문 비밀번호가 유출되는 상황은 확실히 막아줍니다.
> 더 강한 보호가 필요하면 1번 방식을 쓰고 `NAVER_MCP_MASTER_KEY` 를 macOS 키체인에서 주입하세요.
### 접속 확인
```bash
npm run doctor
```
설정 요약 → IMAP 접속 → 메일함 목록 → 읽지 않은 메일 수 → SMTP 인증까지 확인합니다.
어떤 출력에도 비밀번호는 나오지 않습니다.
---
## 5. Claude 에 등록하기
### 방법 A — 자동 설치 (macOS, 권장)
프로젝트 폴더의 **`설치.command`** 를 더블클릭하면 아래를 한 번에 처리합니다.
1. Node.js 확인 → 의존성 설치 → 빌드
2. 네이버 계정 입력 (비밀번호는 화면에 `*` 표시, AES-256-GCM 암호화 저장)
3. 첨부 저장 폴더 지정 → `.env` 갱신 (비밀번호는 `.env` 에 쓰지 않음)
4. **Claude Desktop 설정 파일에 자동 등록** (기존 설정은 타임스탬프 백업 후 병합)
5. IMAP/SMTP 실접속 진단
터미널에서 직접 실행하려면:
```bash
bash 설치.command
```
계정/폴더만 다시 잡고 싶으면 (빌드 생략):
```bash
npm run install-desktop
```
### 방법 B — Claude Desktop 수동 등록
설정 파일에 아래를 추가합니다.
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"naver-mail-mcp-basic": {
"command": "/절대경로/node",
"args": ["/절대경로/naver-mail-mcp-basic/dist/index.js"],
"env": {
"NAVER_MAIL_MCP_ENV": "/절대경로/naver-mail-mcp-basic/.env"
}
}
}
}
```
⚠️ **`command` 에는 node 의 절대경로를 쓰세요** (`which node` 로 확인).
Claude Desktop 은 셸 PATH 없이 실행되기 때문에 그냥 `"node"` 라고 쓰면
nvm/Homebrew 로 설치한 node 를 찾지 못하는 경우가 많습니다. 자동 설치(방법 A)는 이를 알아서 처리합니다.
저장 후 Claude Desktop 을 **완전히 종료(메뉴 → Quit)했다가 다시 실행**하세요.
> MCP 서버는 임의의 작업 디렉터리에서 실행되므로 `NAVER_MAIL_MCP_ENV` 로
> `.env` 절대경로를 지정하는 것이 가장 확실합니다.
> (지정하지 않으면 실행 디렉터리 → 패키지 루트 순으로 `.env` 를 찾습니다.)
### 방법 C — AI 에이전트(코워크 등)에게 맡기기
사용자가 이 레포 주소와 함께 설치를 요청했다면, 에이전트는 아래 순서대로 진행하세요.
**터미널 대화형 절차(`설치.command`, `npm run setup`)는 쓰지 않습니다** — 계정 입력은
사람이 파일을 직접 열어서 합니다.
1. **받기** — 사용자가 폴더를 지정하지 않았으면 홈 폴더 아래에 클론합니다:
`git clone https://github.com/Hoodess/naver-mail-mcp-basic.git`
(git 이 없으면 GitHub 에서 zip 을 내려받아 풉니다.)
2. **빌드** — 레포 폴더에서 `npm install` → `npm run build`.
Node.js 가 없거나 20 미만이면 여기서 중단하고 https://nodejs.org 의 LTS 설치를 안내하세요.
3. **`.env` 만들기** — `.env.example` 을 복사해 `.env` 를 만듭니다.
- `NAVER_EMAIL` 과 `NAVER_APP_PASSWORD` 는 **자리표시자 그대로** 둡니다.
⚠️ 이 두 값을 채팅으로 묻거나 대신 채우지 마세요 — 비밀번호는 사람이 직접 넣습니다.
- `DOWNLOAD_BASE_DIR` 은 비밀이 아니므로, 사용자에게 원하는 저장 폴더를 물어 채우거나
홈 폴더 아래 `NaverMailMCP/attachments` 로 정해 채우세요 (폴더도 만들어 두세요).
4. **Claude Desktop 등록** — 방법 B 의 JSON 형식대로 `claude_desktop_config.json` 에
추가합니다. `command` 는 node **절대경로**, `NAVER_MAIL_MCP_ENV` 는 방금 만든 `.env`
**절대경로**. 기존 설정 파일이 있으면 백업해 두고 병합하세요.
5. **사용자 안내** — 마지막에 두 가지를 안내하고 마칩니다:
- "`{.env 절대경로}` 파일을 텍스트 편집기(메모장 등)로 열어 `NAVER_EMAIL` 과
`NAVER_APP_PASSWORD` 를 본인 것으로 바꿔 저장하세요. 앱 비밀번호 발급 방법은
README 2절을 보세요."
- "저장했으면 Claude Desktop 을 완전히 종료(메뉴 → Quit)했다가 다시 실행하세요."
6. **(선택) 확인** — 사용자가 입력을 마쳤다고 하면 `npm run doctor` 로 접속을 확인하세요.
어떤 출력에도 비밀번호는 나오지 않습니다.
### (참고) Claude Code CLI
```bash
claude mcp add naver-mail-mcp-basic -- node /절대경로/naver-mail-mcp-basic/dist/index.js
```
## 6. 수동 테스트 방법
### 6-1. 명령줄에서 (Claude 없이)
```bash
npm run doctor
```
```bash
npm run smoke
```
`scripts/smoke-test.mjs` 는 실제 MCP 서버를 stdio 로 띄우고 JSON-RPC 로 전체 흐름을 검증합니다.
(툴 목록 → 읽지 않은 메일 → 검색 → 본문 조회 → 첨부 다운로드 → 중복 방지 → 경로 이탈 차단 →
증분 워크플로우 → 발송 준비). 저장 폴더/상태를 임시 디렉터리에 격리해 실행하므로 여러 번 돌려도 실제 환경을 건드리지 않습니다.
> 첨부 유무 필터가 걸린 조회는 메일함 전체의 구조를 훑어야 해서 계정에 따라 수 분이 걸릴 수 있습니다.
> 자세한 이유는 [10. 설계 가정 및 알려진 제약](#10-설계-가정-및-알려진-제약) 참고.
### 6-2. Claude 대화창에서 (권장 순서)
**① 읽지 않은 메일 확인**
> 네이버 메일에서 읽지 않은 메일 중 첨부 있는 것만 10통 보여줘
`list_unread_mails` 가 호출되고 각 메일의 `uid`, 제목, 발신자, 첨부 파일명이 나옵니다.
**② 그중 하나의 첨부 내려받기**
> 그중 uid 12345 메일 첨부를 받아서 종류별로 정리해줘
`download_attachments` 가 호출되고 저장된 파일의 **절대경로 목록**이 반환됩니다.
Finder/탐색기에서 `DOWNLOAD_BASE_DIR` 아래에 `pdf/`, `hwp/` 같은 폴더가 생겼는지 확인하세요.
**③ 같은 명령을 한 번 더 실행**
동일 파일은 내용 해시가 같으므로 `중복 건너뜀` 으로 표시되고 `_1`, `_2` 사본이 생기지 않아야 정상입니다.
---
## 7. MCP 툴 레퍼런스
### `list_unread_mails`
읽지 않은 메일 목록. **읽음 처리하지 않습니다.**
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| `limit` | number | 30 | 최대 개수 |
| `since_date` | string | – | `today` / `yesterday` / `7d` / `2026-08-01` |
| `mailbox` | string | `INBOX` | 한글 별칭(`보낸메일함`, `임시보관함` 등) 인식 |
| `only_with_attachments` | boolean | `false` | 첨부 있는 메일만 |
| `include_inline_images` | boolean | `false` | 서명 로고 등 인라인 이미지도 첨부로 셀지 |
| `scan_limit` | number | 200 | 첨부 필터 시 훑어볼 최대 메일 수 |
반환: 제목, 발신자, 수신일, `uid`, `messageId`, 첨부 여부, **첨부 파일명 목록**, 크기
---
### `get_mail`
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| `uid` | number | – | `uid` 또는 `message_id` 중 하나 필수 |
| `message_id` | string | – | RFC822 Message-ID |
| `mailbox` | string | `INBOX` | |
| `max_body_chars` | number | 20000 | 본문 최대 길이 |
| `include_html` | boolean | `false` | 원본 HTML 도 반환 |
반환: 본문(텍스트, HTML 은 자동으로 평문 변환), 헤더, 첨부 메타데이터(파일명/크기/MIME 타입/파트번호)
---
### `download_attachments`
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| `uids` | number[] | – | `uids` 또는 `message_ids` 중 하나 필수 |
| `message_ids` | string[] | – | 느리므로 `uids` 권장 |
| `mailbox` | string | `INBOX` | |
| `target_dir` | string | `DOWNLOAD_BASE_DIR` | **기준 폴더 기준 상대경로 권장.** 밖으로 나가면 거부 |
| `organize_by_type` | boolean | `true` | 확장자별 하위 폴더 생성 |
| `rename_template` | string | – | 아래 토큰 표 참고 |
| `extensions` | string[] | – | 예: `["pdf","hwp"]` |
| `include_inline_images` | boolean | `false` | |
| `on_duplicate` | `rename`\|`skip`\|`overwrite` | `rename` | |
| `group_by_mail` | boolean | `false` | 메일별 하위 폴더 한 단계 더 |
**rename_template 토큰**
| 토큰 | 예시 값 |
|---|---|
| `{date}` | `2026-08-03` |
| `{date_compact}` | `20260803` |
| `{time}` | `1430` |
| `{datetime}` | `2026-08-03_1430` |
| `{from}` | `김변호사` (표시명, 없으면 메일 ID) |
| `{from_email}` | `lawyer@example.com` |
| `{subject}` | 제목 (60자로 자름) |
| `{original}` | 원본 파일명 (`소장.pdf`) |
| `{basename}` | 확장자 뺀 원본 이름 (`소장`) |
| `{ext}` | `pdf` |
| `{uid}` / `{index}` / `{mailbox}` | UID / 메일 내 첨부 순번 / 메일함 |
예: `"{date}_{from}_{original}"` → `2026-08-03_김변호사_소장.pdf`
템플릿에 확장자가 없으면 원본 확장자를 자동으로 붙입니다.
**반환**: `savedPaths` (저장된 절대경로 배열), 메일별 상세 결과, 분류별 개수, 합계
---
### `process_new_mails_with_attachments` (핵심 워크플로우)
"새 메일 확인 → 첨부 다운로드 → 종류별 정리 → rename" 을 한 번에 수행합니다.
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| `since` | string | `last_run` | `last_run` / `today` / `yesterday` / `7d` / `2026-08-01` |
| `base_dir` | string | `DOWNLOAD_BASE_DIR` | 저장 폴더 |
| `only_with_attachments` | boolean | `true` | |
| `mark_as_seen` | boolean | **`false`** | 로펌 기본값: 읽음 처리 안 함 |
| `mailbox` | string | `INBOX` | |
| `organize_by_type` | boolean | `true` | |
| `rename_template` | string | – | |
| `extensions` | string[] | – | |
| `group_by_mail` | boolean | `false` | |
| `unread_only` | boolean | `false` | |
| `limit` | number | 50 | 한 번에 처리할 최대 메일 수 |
| `dry_run` | boolean | `false` | 저장하지 않고 대상만 보고 |
| `update_state` | boolean | `true` | `last_run` 커서 갱신 여부 |
**증분 처리 방식**
- 메일함별 마지막 처리 UID 를 `~/.naver-mail-mcp-basic/state.json` 에 저장합니다.
- 날짜가 아니라 UID 기준이라, 같은 날 여러 번 실행해도 중복 처리되지 않습니다.
- 오래된 메일부터 처리하고 처리한 것까지만 커서를 전진시키므로 `limit` 에 걸려도 **누락이 없습니다.**
(남은 메일은 같은 툴을 한 번 더 실행하면 이어서 처리됩니다)
- 서버의 `UIDVALIDITY` 가 바뀌면 자동으로 상태를 초기화하고 경고를 반환합니다.
- **첫 실행**에는 기록이 없으므로 안전을 위해 "오늘"부터 시작합니다.
과거 메일까지 훑으려면 `since: "30d"` 처럼 명시하세요.
---
### `search_mails`
| 파라미터 | 설명 |
|---|---|
| `from` / `to` / `subject` | 부분 일치 (한글 가능) |
| `body` | 본문 키워드 (한글은 네이버 서버가 지원하지 않을 수 있음) |
| `since` / `before` | 날짜 범위 |
| `has_attachment` | `true` = 첨부 있는 것만 |
| `attachment_extensions` | 예: `["pdf","hwp"]` |
| `unread_only` / `mailbox` / `limit` / `scan_limit` | |
한글 키워드는 서버 SEARCH 가 실패해도 날짜·상태 조건으로 좁힌 뒤 클라이언트에서 필터링해 결과를 냅니다.
---
### `save_draft_or_prepare_send` (발송 준비 — 발송 안 함)
| 파라미터 | 타입 | 설명 |
|---|---|---|
| `to` / `cc` / `bcc` | string[] | 받는사람 필수 |
| `subject` | string | 제목 |
| `body_text` / `body_html` | string | 본문 |
| `attachment_paths` | string[] | 첨부할 로컬 파일 경로 |
| `mode` | `prepare`\|`imap_draft` | 기본 `prepare` |
| `note` | string | 담당자 확인용 메모 |
`mode: "prepare"` (기본) 이면 `OUTBOX_DIR` 아래에 이런 폴더를 만듭니다.
```
_발송대기/2026-08-03_143012_자료송부의건/
├── meta.json ← 수신자·제목·첨부 목록·상태(대기중)
├── body.txt
├── message.eml ← 메일 클라이언트로 바로 열 수 있는 완성된 메일
└── attachments/ ← 첨부 사본
```
`mode: "imap_draft"` 이고 `ALLOW_IMAP_DRAFT=true` 이면 네이버 **임시보관함**에도 초안으로 올립니다.
**어떤 경우에도 이 툴은 메일을 발송하지 않습니다.** 실제 발송은 사람이 직접:
```bash
npm run send-prepared -- "/경로/_발송대기/2026-08-03_143012_자료송부의건" --confirm
```
`--confirm` 없이 실행하면 내용만 보여주고 종료합니다.
첨부로 붙일 수 있는 파일은 기본적으로 `DOWNLOAD_BASE_DIR` 과 `OUTBOX_DIR` 안쪽으로 제한됩니다.
사건기록 폴더 등을 추가하려면 `.env` 에 `ATTACH_ALLOWED_ROOTS` 를 설정하세요.
---
## 8. 변호사 사무실 시나리오 예시 프롬프트
### 시나리오 1 — "오늘 온 네이버 메일 첨부파일 전부 받아서 종류별로 정리해줘"
> 오늘 온 네이버 메일 첨부파일 전부 받아서 종류별로 정리해줘.
> 저장은 `2026-08` 폴더에 하고, 파일명은 `날짜_보낸사람_원본파일명` 형식으로 해줘.
> 읽음 처리는 하지 마.
Claude 가 호출하는 것:
```json
{
"tool": "process_new_mails_with_attachments",
"arguments": {
"since": "today",
"base_dir": "2026-08",
"organize_by_type": true,
"rename_template": "{date}_{from}_{original}",
"mark_as_seen": false
}
}
```
결과: `<DOWNLOAD_BASE_DIR>/2026-08/pdf/2026-08-03_서울중앙지방법원_소장부본.pdf` 같은 경로들.
---
### 시나리오 2 — "○○ 사건 관련 메일 첨부만 pdf 폴더로 모아줘"
> "김철수" 관련 메일을 찾아서, 그 메일들의 PDF 첨부만 `사건/김철수` 폴더에 모아줘.
Claude 가 호출하는 것 (2단계):
```json
{ "tool": "search_mails",
"arguments": { "subject": "김철수", "has_attachment": true, "limit": 30, "scan_limit": 1000 } }
```
```json
{ "tool": "download_attachments",
"arguments": {
"uids": [23011, 23044, 23090],
"target_dir": "사건/김철수",
"extensions": ["pdf"],
"organize_by_type": true,
"rename_template": "{date}_{subject}_{original}"
} }
```
> 발신자로 찾는 편이 더 정확하면 `from: "kimcs@lawfirm.co.kr"` 을 쓰라고 알려주세요.
> 오래된 사건이면 `scan_limit` 을 올리라고 함께 지시하는 것이 좋습니다.
---
### 시나리오 3 — "고객 발송용 파일 모아서 발송대기 폴더에 준비해줘"
> `사건/김철수/pdf` 에 있는 파일들을 첨부해서, 의뢰인(kim@example.com)에게 보낼 메일을
> 발송대기 폴더에 준비해줘. 제목은 "[○○법률사무소] 김철수님 사건 자료 송부",
> 본문은 자료 확인 요청 내용으로. **절대 발송하지는 마.**
Claude 가 호출하는 것:
```json
{
"tool": "save_draft_or_prepare_send",
"arguments": {
"to": ["kim@example.com"],
"subject": "[○○법률사무소] 김철수님 사건 자료 송부",
"body_text": "안녕하세요, ○○법률사무소입니다.\n요청하신 사건 관련 자료를 첨부하여 송부드립니다.\n확인 후 회신 부탁드립니다.",
"attachment_paths": [
"/Users/내이름/사건첨부/사건/김철수/pdf/2026-08-03_소장부본.pdf"
],
"mode": "prepare",
"note": "담당 변호사 검토 후 발송"
}
}
```
결과: `_발송대기/...` 폴더에 `meta.json` + `message.eml` + 첨부 사본이 생성되고,
**발송은 되지 않습니다.** 담당자가 내용을 확인한 뒤 터미널에서 `npm run send-prepared -- "<폴더>" --confirm` 로 보냅니다.
---
## 9. 보안 설계
| 위협 | 대응 |
|---|---|
| 평문 비밀번호가 디스크에 남음 | 환경변수 또는 AES-256-GCM 암호화 저장소(`credentials.enc`, 0600) |
| 로그·에러 메시지에 비밀번호 노출 | 전역 마스킹 (`util/redact.ts`) — 원문/base64/URL 인코딩 형태까지 치환. 모든 로그와 툴 응답이 이 필터를 거침 |
| 첨부 파일명을 이용한 경로 조작 (`../../etc/passwd`) | 파일명에서 경로 구분자·제어문자 제거, 마지막 경로 요소만 사용 |
| `target_dir` 로 샌드박스 탈출 | 모든 저장 경로를 `DOWNLOAD_BASE_DIR` 기준으로 resolve 후 하위 여부 검증 |
| 심볼릭 링크로 우회 | 저장 직전 `realpath` 로 다시 검증 (base 와 target 을 동일 방식으로 해석) |
| 통신 도청 | IMAP TLS 필수 (`NAVER_IMAP_SECURE=false` 는 설정 단계에서 거부), SMTP 는 STARTTLS/SSL, `minVersion: TLSv1.2` |
| 의도치 않은 메일 발송 | MCP 툴에는 **발송 코드 자체가 없음**. 발송은 별도 CLI + `--confirm` 필요 |
| 의도치 않은 읽음 처리 | 조회는 전부 `EXAMINE`(읽기 전용)으로 메일함을 염. `mark_as_seen` 기본 `false` |
| 임의 파일 첨부를 통한 유출 | 첨부 소스 경로를 허용 루트(`DOWNLOAD_BASE_DIR`, `OUTBOX_DIR`, `ATTACH_ALLOWED_ROOTS`)로 제한 |
| 대용량 첨부로 인한 메모리 고갈 | 첨부는 메모리에 올리지 않고 스트리밍으로 디스크에 기록. `MAX_ATTACHMENT_MB` 초과 시 건너뜀 |
| stdout 오염으로 MCP 프로토콜 깨짐 | 모든 로그는 stderr 로만 출력 |
생성되는 파일 권한: 설정/상태 폴더 `0700`, 자격증명·상태·첨부 파일 `0600`.
---
## 10. 설계 가정 및 알려진 제약
요구사항에 명시되지 않아 **합리적으로 가정한 것들**입니다.
### 가정
1. **런타임은 Node.js 20+ / TypeScript 를 선택했습니다.**
`imapflow` 가 MIME 파트 단위 스트리밍 다운로드를 지원해 대용량 첨부에 안전하고,
MCP TypeScript SDK 가 가장 성숙하며, 요청하신 등록 명령(`claude mcp add ... -- node dist/index.js`)과 그대로 맞기 때문입니다.
2. **`docx/` 폴더에는 `.doc` 도 함께 넣습니다.** `xlsx/` 에는 `.xls`, `.xlsm`, `.csv` 를 함께 넣습니다.
실무에서 구버전 포맷을 따로 찾아다니는 것이 더 불편하다고 판단했습니다.
`.pptx`, `.zip`, `.html` 등은 명세대로 `etc/` 로 갑니다. 분류표는 `.env` 의 `ATTACHMENT_CATEGORY_MAP` 으로 바꿀 수 있습니다.
3. **본문에 삽입된 인라인 이미지(cid 를 가진 서명 로고·배너)는 기본적으로 첨부로 보지 않습니다.**
그렇게 하지 않으면 로고 파일이 `images/` 에 수백 개 쌓입니다. `include_inline_images: true` 로 켤 수 있습니다.
4. **내용이 완전히 같은 파일(sha256 일치)은 `_1`, `_2` 사본을 만들지 않고 건너뜁니다.**
명세는 "중복 시 `_1`, `_2`" 였지만, 그대로 두면 워크플로우를 재실행할 때마다 사본이 무한 증식합니다.
*이름만* 같고 내용이 다르면 명세대로 `_1`, `_2` 가 붙습니다.
5. **`process_new_mails_with_attachments` 의 첫 실행은 "오늘"부터 시작합니다.**
기록이 없다고 메일함 전체(수천 통)를 내려받으면 사고이기 때문입니다. 경고 메시지로 안내하고, `since` 로 넓힐 수 있습니다.
6. **`save_draft_or_prepare_send` 는 SMTP 로 초안을 저장하지 않습니다.**
SMTP 프로토콜에는 초안 저장 기능이 없습니다. 대신 (a) 발송대기 폴더 + `.eml` 생성, (b) IMAP APPEND 로 네이버 임시보관함에 초안 저장 — 두 가지를 제공합니다.
7. **실제 발송 기능은 MCP 툴로 제공하지 않습니다.** 별도 CLI(`npm run send-prepared -- <폴더> --confirm`)로만 가능합니다.
8. **`list_mailboxes` 툴을 추가했습니다.** 명세에는 없지만, 네이버 메일함 이름이 한글이거나 modified-UTF7 로 인코딩되어 있어 다른 툴의 `mailbox` 값을 채우려면 사실상 필요합니다.
### 알려진 제약
- **첨부 유무/확장자 필터는 느립니다.**
IMAP 에는 "첨부 있는 메일" 검색 조건이 없어서 후보 메일들의 `BODYSTRUCTURE` 를 하나씩 받아 판별해야 합니다.
네이버 서버 응답이 느린 편이라 200통 훑는 데 1~2분이 걸릴 수 있습니다.
→ **날짜 조건(`since`)을 함께 주면 훨씬 빠릅니다.** 기본 스캔 범위는 최근 200통(`CLIENT_SCAN_LIMIT`)이며,
더 넓게 보려면 툴 인자 `scan_limit` 을 올리세요. 범위에 걸리면 응답에 경고가 함께 나옵니다.
- **한글 본문 검색(`body`)은 네이버 IMAP 이 제대로 처리하지 못할 수 있습니다.**
제목·발신자 검색은 서버가 실패해도 클라이언트에서 필터링하지만, 본문은 전체 메일을 받아야 해서 폴백하지 않습니다.
- **깨진 첨부 파일명 복구.** 국내 금융기관 메일은 파일명을 RFC 2047 인코딩 없이 EUC-KR 원시 바이트로 보내는 경우가 많고, 이때 IMAP 라이브러리가 UTF-8 로 잘못 디코딩합니다.
이 서버는 이름이 깨진 첨부에 한해 원본 MIME 헤더(`BODY[n.MIME]`)를 다시 받아 CP949 로 복구합니다.
(실제 계정 테스트에서 `보안암호첨부.html` 정상 복구 확인)
- **IMAP 동기화 제한.** 네이버 환경설정의 "IMAP 동기화 메일 제한"보다 오래된 메일은 서버가 아예 보여주지 않습니다.
- **네이버 첨부 용량.** 대용량 첨부(네이버 클라우드 링크)는 실제 파일이 아니라 본문 링크이므로 다운로드되지 않습니다.
---
## 11. 문제 해결
| 증상 | 원인/해결 |
|---|---|
| `네이버 IMAP 로그인에 실패했습니다` | ① 메일 환경설정에서 IMAP/SMTP `사용함` + **확인 버튼** 눌렀는지 ② 로그인 비밀번호가 아니라 **앱 비밀번호**인지 ③ 앱 비밀번호를 재발급했는지 |
| `설정 오류: 네이버 계정 정보를 찾을 수 없습니다` | `.env` 위치를 못 찾은 경우가 대부분. `NAVER_MAIL_MCP_ENV` 에 `.env` 절대경로를 지정하세요 |
| Claude 에서 툴이 안 보임 | `npm run build` 를 했는지, `dist/index.js` 경로가 **절대경로**인지 확인. Claude Desktop 은 완전 종료 후 재시작 필요 |
| 오래된 메일이 안 보임 | 네이버 환경설정의 "IMAP 동기화 메일 제한"을 올리세요 |
| 첨부 검색이 너무 오래 걸림 | 날짜 조건을 주거나 `scan_limit` 을 낮추세요 |
| `허용되지 않은 경로입니다` | `target_dir` 이 `DOWNLOAD_BASE_DIR` 밖입니다. 상대경로를 쓰거나 `DOWNLOAD_BASE_DIR` 을 바꾸세요 |
| `자격증명 복호화에 실패했습니다` | `master.key` 가 사라졌거나 `NAVER_MCP_MASTER_KEY` 가 바뀐 경우. `npm run setup` 으로 다시 저장 |
| 로그를 더 보고 싶다 | `.env` 에 `LOG_LEVEL=debug` (로그는 stderr 로만 나갑니다) |
---
## 12. 프로젝트 구조
```
naver_mail_mcp/
├── src/
│ ├── index.ts # MCP 서버 진입점 (stdio, 툴 등록)
│ ├── config.ts # 환경변수/설정 로딩, 확장자 분류표
│ ├── secrets.ts # AES-256-GCM 자격증명 저장소
│ ├── imap.ts # IMAP 연결 관리 (재사용/재연결/직렬화/메일함 별칭)
│ ├── attachments.ts # ★ 첨부 다운로드 + 분류 + rename 엔진
│ ├── outbox.ts # 발송 준비 (.eml 생성, 임시보관함 초안) — 발송 없음
│ ├── state.ts # 증분 처리 상태(last_run)
│ ├── mail/
│ │ ├── service.ts # 검색/목록/본문 조회 (서버검색 + 클라이언트 폴백)
│ │ ├── message.ts # bodyStructure 해석, 본문/첨부 파트 판별
│ │ ├── mime.ts # RFC2047/RFC2231 파일명 디코딩, charset 처리
│ │ ├── repair.ts # EUC-KR 원시 바이트 파일명 복구
│ │ ├── rename.ts # rename_template 처리, 확장자→폴더 매핑
│ │ └── html-text.ts # HTML → 평문 변환
│ ├── tools/ # MCP 툴 정의 (스키마 + 핸들러)
│ ├── cli/
│ │ ├── setup.ts # npm run setup — 자격증명 암호화 저장
│ │ ├── doctor.ts # npm run doctor — 연결 진단
│ │ └── send-prepared.ts # npm run send-prepared — 사람이 직접 발송
│ └── util/ # 경로 보안, 로깅, 비밀정보 마스킹, 날짜
├── scripts/
│ ├── smoke-test.mjs # MCP stdio 종단 테스트 (임시 폴더에 격리 실행)
│ └── install-desktop.mjs # Claude Desktop 자동 등록 마법사
├── 설치.command # macOS 더블클릭 설치 (빌드→계정→등록→진단)
├── 사용설명서.md # 비전문가용 사용설명서
├── .env.example
└── dist/ # 빌드 결과 (npm run build)
```
### 참고
아키텍처·IMAP 연결·증분 처리·자격증명 암호화 패턴은
[youngsooco/k-mail-mcp](https://github.com/youngsooco/k-mail-mcp) 을 참고했습니다.
다만 이 프로젝트의 핵심인 **첨부 로컬 저장 / 확장자별 분류 / 파일명 규칙 / 경로 샌드박스**는
새로 설계·구현했습니다.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues