feat(harness): zioinfo-mail webmail harness — backend/frontend/infra agents + orchestrator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
19dd2c0c09
commit
60be2f9375
80
.claude/agents/mail-backend-dev.md
Normal file
80
.claude/agents/mail-backend-dev.md
Normal file
@ -0,0 +1,80 @@
|
||||
# mail-backend-dev
|
||||
|
||||
## 핵심 역할
|
||||
zioinfo-mail 웹메일 시스템의 FastAPI 백엔드를 구현한다. 기존 Postfix(SMTP) + Dovecot(IMAP)와 연동하여 메일 읽기·쓰기·검색·폴더 관리 API를 제공한다.
|
||||
|
||||
## 구현 범위
|
||||
|
||||
### API 엔드포인트
|
||||
```
|
||||
POST /auth/login → IMAP 인증 → JWT 발급
|
||||
POST /auth/logout → 세션 종료
|
||||
|
||||
GET /mail/folders → 폴더 목록 (INBOX, Sent, Drafts, Trash, Spam)
|
||||
GET /mail/messages → 메일 목록 (폴더, 페이지, 검색)
|
||||
GET /mail/messages/{uid} → 메일 상세 + 첨부파일 목록
|
||||
GET /mail/attachments/{uid}/{part} → 첨부파일 다운로드
|
||||
|
||||
POST /mail/send → 메일 발송 (SMTP)
|
||||
POST /mail/draft → 임시저장
|
||||
PUT /mail/messages/{uid}/read → 읽음 처리
|
||||
PUT /mail/messages/{uid}/move → 폴더 이동
|
||||
DELETE /mail/messages/{uid} → 삭제 (Trash 이동)
|
||||
DELETE /mail/messages/{uid}/force → 영구 삭제
|
||||
|
||||
GET /mail/search?q= → 전문 검색 (IMAP SEARCH)
|
||||
```
|
||||
|
||||
### 기술 스택
|
||||
```python
|
||||
# 핵심 의존성
|
||||
aioimaplib==0.9.2 # async IMAP4 클라이언트
|
||||
aiosmtplib==3.0.1 # async SMTP 클라이언트
|
||||
python-jose==3.3.0 # JWT
|
||||
email-parser # 메일 파싱
|
||||
python-multipart # 첨부파일 업로드
|
||||
```
|
||||
|
||||
### IMAP 연결 설정
|
||||
```python
|
||||
IMAP_HOST = "localhost"
|
||||
IMAP_PORT = 993 # SSL
|
||||
SMTP_HOST = "localhost"
|
||||
SMTP_PORT = 587 # STARTTLS
|
||||
SMTP_USER = "{user}@zioinfo.co.kr"
|
||||
```
|
||||
|
||||
### 인증 방식
|
||||
- 사용자가 입력한 `user@zioinfo.co.kr` + 비밀번호로 IMAP 로그인
|
||||
- 성공 시 JWT 발급 (IMAP 자격증명을 암호화하여 토큰에 포함)
|
||||
- 이후 모든 요청은 JWT에서 IMAP 자격증명 복호화하여 사용
|
||||
|
||||
### 메일 파싱
|
||||
- `email.parser` 표준 라이브러리 사용
|
||||
- HTML/텍스트 멀티파트 처리
|
||||
- 첨부파일: Content-Disposition 파싱, 인라인 이미지 CID 처리
|
||||
- 한글 인코딩: `chardet` + `email.header.decode_header`
|
||||
|
||||
## 파일 구조
|
||||
```
|
||||
workspace/zioinfo-mail/backend/
|
||||
├── main.py # FastAPI 앱 (포트 8026)
|
||||
├── auth.py # IMAP 인증 + JWT
|
||||
├── imap_client.py # IMAP 연결 풀 + 메일 조회
|
||||
├── smtp_client.py # SMTP 메일 발송
|
||||
├── mail_parser.py # 메일 파싱 유틸
|
||||
├── models.py # Pydantic 스키마
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
## 보안 원칙
|
||||
1. IMAP 비밀번호는 JWT 페이로드에 AES 암호화 저장
|
||||
2. 첨부파일 경로 순회 방지 (`..` 차단)
|
||||
3. HTML 메일 내 스크립트 태그 sanitize
|
||||
4. CORS: 허용 origin을 `mail.zioinfo.co.kr`로 제한
|
||||
|
||||
## 팀 통신 프로토콜
|
||||
- **수신**: orchestrator로부터 구현 시작 신호
|
||||
- **발신**: mail-frontend-dev에게 API 스펙 (`_workspace/api-spec.md`)
|
||||
- **발신**: mail-infra-setup에게 서비스 포트, systemd 파일 요청
|
||||
- **보고**: 완료 후 orchestrator에게 엔드포인트 목록 전달
|
||||
90
.claude/agents/mail-frontend-dev.md
Normal file
90
.claude/agents/mail-frontend-dev.md
Normal file
@ -0,0 +1,90 @@
|
||||
# mail-frontend-dev
|
||||
|
||||
## 핵심 역할
|
||||
zioinfo-mail 웹메일 시스템의 React 18 SPA 프론트엔드를 구현한다. 깔끔한 3-패널 메일 클라이언트 UI (폴더 트리 + 메일 목록 + 메일 본문)를 구축한다.
|
||||
|
||||
## UI 레이아웃
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ 🔵 zioinfo MAIL [검색창] [작성] [로그아웃] │ ← Header
|
||||
├───────────┬──────────────────┬──────────────────────────┤
|
||||
│ 폴더트리 │ 메일 목록 │ 메일 본문 │
|
||||
│ │ │ │
|
||||
│ 📥 받은 │ ─ 보낸사람 ─ │ 제목: ... │
|
||||
│ 메함 │ 제목 미리보기 │ 보낸사람: ... │
|
||||
│ 📤 보낸 │ 날짜 · 크기 │ 받는사람: ... │
|
||||
│ 메함 │ │ ───────────────── │
|
||||
│ 📝 임시 │ [읽음][삭제] │ 본문 내용 │
|
||||
│ 보관함 │ [이동][스팸] │ │
|
||||
│ 🗑️ 휴지통 │ │ [첨부파일 목록] │
|
||||
│ ⚠️ 스팸 │ │ │
|
||||
└───────────┴──────────────────┴──────────────────────────┘
|
||||
```
|
||||
|
||||
## 화면 구성
|
||||
|
||||
### 주요 컴포넌트
|
||||
```
|
||||
src/
|
||||
├── App.tsx
|
||||
├── pages/
|
||||
│ ├── Login.tsx # 로그인 (user@zioinfo.co.kr)
|
||||
│ └── Mail.tsx # 메인 메일 클라이언트
|
||||
├── components/
|
||||
│ ├── FolderTree.tsx # 좌측 폴더 목록 + 안읽음 수
|
||||
│ ├── MailList.tsx # 중앙 메일 목록 + 페이지네이션
|
||||
│ ├── MailView.tsx # 우측 메일 본문 + 첨부파일
|
||||
│ ├── Compose.tsx # 작성/답장/전달 (모달)
|
||||
│ └── SearchBar.tsx # 전문 검색
|
||||
├── api/
|
||||
│ └── mailApi.ts # axios 기반 API 클라이언트
|
||||
├── store/
|
||||
│ └── mailStore.ts # Zustand 상태 관리
|
||||
└── styles/
|
||||
└── mail.css # 메일 클라이언트 스타일
|
||||
```
|
||||
|
||||
### 핵심 기능
|
||||
- **3-패널 레이아웃**: 폴더/목록/본문 분할 뷰
|
||||
- **메일 작성**: To/CC/BCC, 에디터(기본 textarea), 첨부파일 드래그앤드롭
|
||||
- **답장/전달**: 인용 포함 자동 구성
|
||||
- **HTML 메일**: DOMPurify로 sanitize 후 iframe 렌더링
|
||||
- **페이지네이션**: 폴더당 50건 기본
|
||||
- **실시간 새 메일**: 30초 폴링
|
||||
|
||||
### 기술 스택
|
||||
```json
|
||||
{
|
||||
"react": "^18",
|
||||
"typescript": "^5",
|
||||
"vite": "^5",
|
||||
"axios": "^1",
|
||||
"zustand": "^4",
|
||||
"dompurify": "^3",
|
||||
"date-fns": "^3"
|
||||
}
|
||||
```
|
||||
|
||||
### 디자인 원칙
|
||||
- 색상: 지오정보기술 브랜드 (#003366 딥블루, #00A0C8 스카이블루)
|
||||
- 폰트: Pretendard (기존 시스템과 통일)
|
||||
- 반응형: 모바일에서 2-패널 전환 (폴더 숨김)
|
||||
- 다크모드 불필요 (라이트 전용)
|
||||
|
||||
## 파일 구조
|
||||
```
|
||||
workspace/zioinfo-mail/frontend/
|
||||
├── index.html
|
||||
├── package.json
|
||||
├── vite.config.ts # outDir: '../dist'
|
||||
├── tsconfig.json
|
||||
└── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 팀 통신 프로토콜
|
||||
- **수신**: mail-backend-dev로부터 `_workspace/api-spec.md` (API 스펙)
|
||||
- **수신**: orchestrator로부터 구현 시작 신호
|
||||
- **발신**: orchestrator에게 빌드 완료 + dist 경로 보고
|
||||
- **협업**: API 스펙 불명확 시 mail-backend-dev에게 SendMessage로 질의
|
||||
114
.claude/agents/mail-infra-setup.md
Normal file
114
.claude/agents/mail-infra-setup.md
Normal file
@ -0,0 +1,114 @@
|
||||
# mail-infra-setup
|
||||
|
||||
## 핵심 역할
|
||||
zioinfo-mail 웹메일 시스템의 서버 인프라를 구성한다. nginx 설정, systemd 서비스 등록, Postfix/Dovecot 연동 검증, Gitea 저장소 생성, 배포 파이프라인 연결을 담당한다.
|
||||
|
||||
## 인프라 구성 목표
|
||||
|
||||
### 서비스 구조
|
||||
```
|
||||
Client → nginx:8025 (HTTPS) → FastAPI:8026 (backend API)
|
||||
→ /var/www/mail/ (React SPA)
|
||||
```
|
||||
|
||||
### nginx 설정 (`/etc/nginx/sites-available/zioinfo-mail`)
|
||||
```nginx
|
||||
server {
|
||||
listen 8025 ssl;
|
||||
server_name mail.zioinfo.co.kr;
|
||||
ssl_certificate /etc/letsencrypt/live/zioinfo.co.kr/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/zioinfo.co.kr/privkey.pem;
|
||||
root /var/www/mail;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control no-cache;
|
||||
}
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8026;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
location ~* \.(js|css|png|ico|woff2)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### systemd 서비스 (`/etc/systemd/system/zioinfo-mail.service`)
|
||||
```ini
|
||||
[Unit]
|
||||
Description=ZioInfo Webmail Backend
|
||||
After=network.target postfix.service dovecot.service
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
WorkingDirectory=/opt/mail/backend
|
||||
ExecStart=/opt/mail/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8026 --workers 2
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=append:/var/log/zioinfo/mail.log
|
||||
StandardError=append:/var/log/zioinfo/mail.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
## 구현 작업 목록
|
||||
|
||||
1. **Postfix/Dovecot 연동 검증**
|
||||
- IMAP localhost:993 접속 테스트 (ythong@zioinfo.co.kr)
|
||||
- SMTP localhost:587 발송 테스트
|
||||
|
||||
2. **서버 디렉토리 생성**
|
||||
```bash
|
||||
mkdir -p /opt/mail/backend /opt/mail/venv /var/www/mail /var/log/zioinfo
|
||||
```
|
||||
|
||||
3. **Python venv + 패키지 설치**
|
||||
```bash
|
||||
python3 -m venv /opt/mail/venv
|
||||
/opt/mail/venv/bin/pip install -r requirements.txt
|
||||
```
|
||||
|
||||
4. **nginx 설정 등록 + 포트 오픈**
|
||||
```bash
|
||||
ln -sf /etc/nginx/sites-available/zioinfo-mail /etc/nginx/sites-enabled/
|
||||
nginx -t && systemctl reload nginx
|
||||
ufw allow 8025/tcp
|
||||
```
|
||||
|
||||
5. **systemd 등록 + 시작**
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl enable zioinfo-mail
|
||||
systemctl start zioinfo-mail
|
||||
```
|
||||
|
||||
6. **Gitea 저장소 생성** (`zio/zioinfo-mail`)
|
||||
- Gitea API: `POST /api/v1/user/repos`
|
||||
|
||||
7. **deploy_server.py에 zioinfo-mail 배포 함수 추가**
|
||||
- repo: `zioinfo-mail`
|
||||
- 단계: git pull → npm build → copy dist → pip install → restart
|
||||
|
||||
## 검증 체크리스트
|
||||
- [ ] `curl -f http://localhost:8026/health` → 200
|
||||
- [ ] `curl -f http://localhost:8025/` → 200 (nginx)
|
||||
- [ ] IMAP 로그인 성공 (ythong@zioinfo.co.kr)
|
||||
- [ ] SMTP 발송 성공
|
||||
- [ ] `systemctl is-active zioinfo-mail` → active
|
||||
|
||||
## 접속 정보
|
||||
- 서버: 101.79.17.164 (root, paramiko)
|
||||
- Gitea: `base64(zio:Zio@Admin2026!)`
|
||||
- IMAP: localhost:993 (SSL)
|
||||
- SMTP: localhost:587 (STARTTLS)
|
||||
|
||||
## 팀 통신 프로토콜
|
||||
- **수신**: orchestrator로부터 "인프라 준비 시작" + backend/frontend 완료 신호
|
||||
- **발신**: orchestrator에게 포트/경로 확정 정보 전달
|
||||
- **발신**: deploy-server.py 업데이트 완료 보고
|
||||
247
.claude/skills/zioinfo-mail-orchestrator/SKILL.md
Normal file
247
.claude/skills/zioinfo-mail-orchestrator/SKILL.md
Normal file
@ -0,0 +1,247 @@
|
||||
---
|
||||
name: zioinfo-mail-orchestrator
|
||||
description: >
|
||||
zioinfo-mail 웹메일 시스템 구축 오케스트레이터.
|
||||
지오정보기술 SMTP 서버(Postfix + Dovecot)를 활용한 React+FastAPI 웹메일 클라이언트를
|
||||
workspace/zioinfo-mail/ 에 구축하고 서버(mail.zioinfo.co.kr:8025)에 배포한다.
|
||||
FastAPI IMAP/SMTP 프록시 백엔드, React 3-패널 메일 UI 프론트엔드, nginx+systemd 인프라를
|
||||
에이전트 팀으로 병렬 구현한다.
|
||||
다음 상황에서 반드시 사용:
|
||||
(1) 'webmail', '웹메일', 'zioinfo-mail', '메일 시스템 구축';
|
||||
(2) '메일 클라이언트', '이메일 UI', 'IMAP 연동', 'SMTP 연동';
|
||||
(3) mail.zioinfo.co.kr 관련 작업;
|
||||
(4) 다시 실행, 업데이트, 수정, 보완.
|
||||
---
|
||||
|
||||
# zioinfo-mail 웹메일 시스템 오케스트레이터
|
||||
|
||||
**실행 모드:** 하이브리드
|
||||
- Phase 1 (인프라 검증): 서브 에이전트 (mail-infra-setup)
|
||||
- Phase 2 (Backend + Frontend 구현): **병렬 서브 에이전트**
|
||||
- Phase 3 (통합 배포): 에이전트 팀 (3명 협업)
|
||||
|
||||
---
|
||||
|
||||
## 시스템 개요
|
||||
|
||||
```
|
||||
사용자 브라우저
|
||||
↓ HTTPS:8025
|
||||
nginx (/var/www/mail) → React SPA
|
||||
↓ /api/
|
||||
FastAPI (127.0.0.1:8026)
|
||||
↓ IMAP:993 (SSL) ↓ SMTP:587 (STARTTLS)
|
||||
Dovecot Postfix
|
||||
(읽기) (발송)
|
||||
```
|
||||
|
||||
**기존 인프라:**
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| Postfix | active, `mail.zioinfo.co.kr` |
|
||||
| Dovecot | active, IMAP/POP3, maildir:~/Maildir |
|
||||
| TLS cert | `/etc/ssl/guardia/server.crt` |
|
||||
| 계정 | ythong / info / admin @zioinfo.co.kr |
|
||||
| 웹메일 URL | `https://mail.zioinfo.co.kr:8025` |
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: 컨텍스트 확인
|
||||
|
||||
```
|
||||
workspace/zioinfo-mail/ 존재 여부:
|
||||
- 없음 → 초기 구현 (Phase 1부터 전체)
|
||||
- 있음 + backend/만 요청 → mail-backend-dev만 재실행
|
||||
- 있음 + frontend/만 요청 → mail-frontend-dev만 재실행
|
||||
- 있음 + 배포 요청 → Phase 3만 실행
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 인프라 사전 검증 (서브 에이전트)
|
||||
|
||||
**mail-infra-setup** 에이전트 실행 (읽기 전용 검증):
|
||||
|
||||
```python
|
||||
# 검증 항목
|
||||
1. IMAP localhost:993 접속 테스트
|
||||
2. SMTP localhost:587 접속 테스트
|
||||
3. 포트 8025/8026 사용 가능 여부
|
||||
4. /opt/mail/, /var/www/mail/ 생성 가능 여부
|
||||
5. Gitea zio/zioinfo-mail 저장소 존재 여부
|
||||
```
|
||||
|
||||
결과를 `_workspace/infra-check.json`에 저장.
|
||||
IMAP/SMTP 접속 실패 시 → Postfix/Dovecot 설정 점검 후 재시도.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Backend + Frontend 병렬 구현 (서브 에이전트)
|
||||
|
||||
**mail-backend-dev** + **mail-frontend-dev** 동시 실행:
|
||||
|
||||
### mail-backend-dev 작업 목록
|
||||
|
||||
```
|
||||
workspace/zioinfo-mail/backend/
|
||||
├── main.py ← FastAPI 앱 (포트 8026)
|
||||
├── auth.py ← IMAP 로그인 → JWT 발급
|
||||
├── imap_client.py ← aioimaplib 연결 풀
|
||||
├── smtp_client.py ← aiosmtplib 발송
|
||||
├── mail_parser.py ← 메일 파싱 (한글, 첨부파일)
|
||||
├── models.py ← Pydantic 스키마
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
**핵심 구현 포인트:**
|
||||
- JWT 페이로드에 IMAP 자격증명 AES 암호화 포함
|
||||
- IMAP 연결 풀: 사용자당 1개 재사용
|
||||
- 한글 제목/본문 인코딩: `chardet` + `email.header.decode_header`
|
||||
- 첨부파일: `/tmp/mail_attach_{uid}/` 임시 저장 후 스트리밍
|
||||
|
||||
### mail-frontend-dev 작업 목록
|
||||
|
||||
```
|
||||
workspace/zioinfo-mail/frontend/
|
||||
├── package.json ← react, typescript, vite, axios, zustand, dompurify
|
||||
├── vite.config.ts ← outDir: '../dist', proxy: /api → :8026
|
||||
├── src/
|
||||
│ ├── App.tsx
|
||||
│ ├── pages/Login.tsx ← 로그인
|
||||
│ ├── pages/Mail.tsx ← 3-패널 메인
|
||||
│ ├── components/
|
||||
│ │ ├── FolderTree.tsx ← 좌측 폴더 트리
|
||||
│ │ ├── MailList.tsx ← 중앙 목록
|
||||
│ │ ├── MailView.tsx ← 우측 본문
|
||||
│ │ └── Compose.tsx ← 작성 모달
|
||||
│ ├── api/mailApi.ts ← axios 클라이언트
|
||||
│ └── store/mailStore.ts ← zustand
|
||||
```
|
||||
|
||||
**Backend API 스펙** (`_workspace/api-spec.md` 참조):
|
||||
- Backend 완료 후 API 스펙 파일 생성 → Frontend에서 읽어 구현
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 통합 배포 (에이전트 팀)
|
||||
|
||||
3명 팀 구성: mail-backend-dev + mail-frontend-dev + mail-infra-setup
|
||||
|
||||
### 3-1. Frontend 빌드
|
||||
```bash
|
||||
cd workspace/zioinfo-mail/frontend && npm run build
|
||||
# dist/ → workspace/zioinfo-mail/dist/
|
||||
```
|
||||
|
||||
### 3-2. 서버 업로드 (mail-infra-setup 담당)
|
||||
```bash
|
||||
# Backend: paramiko sftp → /opt/mail/backend/
|
||||
# Frontend dist: bundle → /var/www/mail/
|
||||
# Python venv: pip install requirements.txt
|
||||
```
|
||||
|
||||
### 3-3. systemd + nginx 설정
|
||||
```bash
|
||||
# /etc/systemd/system/zioinfo-mail.service 작성
|
||||
# /etc/nginx/sites-available/zioinfo-mail 작성
|
||||
# systemctl enable + start zioinfo-mail
|
||||
# nginx -t && systemctl reload nginx
|
||||
# ufw allow 8025/tcp
|
||||
```
|
||||
|
||||
### 3-4. Gitea repo 생성 + push
|
||||
```bash
|
||||
# Gitea API로 zio/zioinfo-mail repo 생성
|
||||
# repos/zioinfo-mail/ 로컬 git init
|
||||
# bundle → server → push
|
||||
```
|
||||
|
||||
### 3-5. deploy_server.py에 zioinfo-mail 추가
|
||||
```python
|
||||
# /opt/zioinfo/deploy_server.py에 zioinfo-mail 배포 함수 추가
|
||||
elif repo == "zioinfo-mail":
|
||||
steps = [
|
||||
("git pull", [...]),
|
||||
("npm build", [...]),
|
||||
("copy dist", ["bash", "-c", "cp -r {SRC}/dist/. /var/www/mail/"]),
|
||||
("pip install", [...]),
|
||||
("restart", ["systemctl", "restart", "zioinfo-mail"]),
|
||||
("health check", [...]),
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 검증
|
||||
|
||||
```bash
|
||||
# 1. 서비스 상태
|
||||
systemctl is-active zioinfo-mail
|
||||
curl -f http://localhost:8026/health
|
||||
|
||||
# 2. IMAP 로그인 테스트
|
||||
curl -X POST http://localhost:8026/auth/login \
|
||||
-d '{"username":"ythong@zioinfo.co.kr","password":"1q2w3e!Q"}'
|
||||
|
||||
# 3. 메일 목록 조회
|
||||
curl http://localhost:8026/mail/messages \
|
||||
-H "Authorization: Bearer {token}"
|
||||
|
||||
# 4. nginx 응답
|
||||
curl -f http://localhost:8025/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 데이터 흐름
|
||||
|
||||
```
|
||||
_workspace/
|
||||
├── infra-check.json ← Phase 1 결과
|
||||
├── api-spec.md ← Backend → Frontend 전달
|
||||
└── deploy-result.json ← Phase 3 결과
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 에러 핸들링
|
||||
|
||||
| 에러 | 원인 | 처리 |
|
||||
|------|------|------|
|
||||
| IMAP 연결 실패 | Dovecot SSL 설정 | `/etc/ssl/guardia/server.crt` 확인 |
|
||||
| SMTP 인증 실패 | SASL 설정 | `postconf smtpd_sasl_*` 확인 |
|
||||
| npm build 실패 | node_modules 없음 | `npm ci` 재시도 |
|
||||
| 포트 충돌 | 8026 이미 사용 | 8027로 변경 |
|
||||
| HTML 메일 XSS | DOMPurify 미적용 | iframe sandbox 사용 |
|
||||
|
||||
---
|
||||
|
||||
## 테스트 시나리오
|
||||
|
||||
**정상 흐름:**
|
||||
1. `https://mail.zioinfo.co.kr:8025` 접속
|
||||
2. `ythong@zioinfo.co.kr` / `1q2w3e!Q` 로그인
|
||||
3. 받은메함 목록 표시
|
||||
4. 메일 클릭 → 본문 조회
|
||||
5. 작성 → To: `info@zioinfo.co.kr` → 발송
|
||||
6. 발신 계정 받은메함에서 수신 확인
|
||||
|
||||
**에러 흐름:**
|
||||
- 잘못된 비밀번호 → 401 응답 + "인증 실패" 메시지
|
||||
|
||||
---
|
||||
|
||||
## should-trigger
|
||||
|
||||
- "웹메일 만들어줘"
|
||||
- "zioinfo-mail 구축"
|
||||
- "메일 클라이언트 개발"
|
||||
- "mail.zioinfo.co.kr 배포"
|
||||
- "IMAP 연동 웹메일"
|
||||
- "다시 실행", "수정", "보완"
|
||||
|
||||
## should-NOT-trigger
|
||||
|
||||
- "GUARDiA에서 메일 알림 보내줘" → guardia-orchestrator (ITSM 알림)
|
||||
- "메일 서버 설정해줘" → Postfix/Dovecot 직접 설정 (인프라 작업)
|
||||
- "홈페이지 문의 메일 연동" → homepage-cms-orchestrator
|
||||
@ -0,0 +1,144 @@
|
||||
"""
|
||||
zioinfo-mail FastAPI 백엔드 템플릿
|
||||
mail-backend-dev 에이전트가 이 파일을 기반으로 확장 구현한다.
|
||||
"""
|
||||
from fastapi import FastAPI, Depends, HTTPException, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import asyncio, aioimaplib, aiosmtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.base import MIMEBase
|
||||
from email import encoders
|
||||
import email.header, chardet, ssl
|
||||
from jose import jwt
|
||||
from cryptography.fernet import Fernet
|
||||
import os, json, base64, hashlib
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
app = FastAPI(title="zioinfo-mail API", version="1.0.0")
|
||||
app.add_middleware(CORSMiddleware,
|
||||
allow_origins=["https://mail.zioinfo.co.kr", "http://localhost:5173"],
|
||||
allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
||||
|
||||
# ── 설정 ────────────────────────────────────────────────────
|
||||
IMAP_HOST = "localhost"; IMAP_PORT = 993
|
||||
SMTP_HOST = "localhost"; SMTP_PORT = 587
|
||||
JWT_SECRET = os.getenv("MAIL_JWT_SECRET", "change-me-in-production")
|
||||
JWT_EXPIRE_HOURS = 8
|
||||
# IMAP 자격증명 암호화 키 (32바이트 → Fernet)
|
||||
FERNET_KEY = os.getenv("MAIL_FERNET_KEY",
|
||||
base64.urlsafe_b64encode(hashlib.sha256(JWT_SECRET.encode()).digest()))
|
||||
fernet = Fernet(FERNET_KEY)
|
||||
|
||||
# ── 모델 ────────────────────────────────────────────────────
|
||||
class LoginRequest(BaseModel):
|
||||
username: str # user@zioinfo.co.kr
|
||||
password: str
|
||||
|
||||
class SendRequest(BaseModel):
|
||||
to: str; cc: Optional[str] = None; bcc: Optional[str] = None
|
||||
subject: str; body: str; html: bool = False
|
||||
reply_to_uid: Optional[str] = None
|
||||
|
||||
# ── 인증 ────────────────────────────────────────────────────
|
||||
async def verify_imap(username: str, password: str) -> bool:
|
||||
"""IMAP 로그인으로 자격증명 검증"""
|
||||
try:
|
||||
ssl_ctx = ssl.create_default_context()
|
||||
ssl_ctx.check_hostname = False
|
||||
ssl_ctx.verify_mode = ssl.CERT_NONE
|
||||
imap = aioimaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT, ssl_context=ssl_ctx)
|
||||
await imap.wait_hello_from_server()
|
||||
res, _ = await imap.login(username, password)
|
||||
await imap.logout()
|
||||
return res == "OK"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def create_token(username: str, password: str) -> str:
|
||||
enc_pw = fernet.encrypt(password.encode()).decode()
|
||||
payload = {
|
||||
"sub": username,
|
||||
"pw": enc_pw,
|
||||
"exp": datetime.utcnow() + timedelta(hours=JWT_EXPIRE_HOURS)
|
||||
}
|
||||
return jwt.encode(payload, JWT_SECRET, algorithm="HS256")
|
||||
|
||||
def get_credentials(token: str = Depends(lambda: None)) -> tuple[str, str]:
|
||||
from fastapi import Header
|
||||
# FastAPI security dependency - 실제 구현에서 Bearer 헤더에서 추출
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||
username = payload["sub"]
|
||||
password = fernet.decrypt(payload["pw"].encode()).decode()
|
||||
return username, password
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# ── 엔드포인트 ───────────────────────────────────────────────
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok", "service": "zioinfo-mail"}
|
||||
|
||||
@app.post("/auth/login")
|
||||
async def login(req: LoginRequest):
|
||||
if not await verify_imap(req.username, req.password):
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "인증 실패")
|
||||
token = create_token(req.username, req.password)
|
||||
return {"access_token": token, "token_type": "bearer", "username": req.username}
|
||||
|
||||
# ── 메일 파싱 유틸 ───────────────────────────────────────────
|
||||
def decode_header_str(raw: str) -> str:
|
||||
"""RFC2047 인코딩된 헤더 디코딩 (한글 포함)"""
|
||||
parts = email.header.decode_header(raw or "")
|
||||
result = []
|
||||
for part, charset in parts:
|
||||
if isinstance(part, bytes):
|
||||
charset = charset or chardet.detect(part).get('encoding', 'utf-8') or 'utf-8'
|
||||
result.append(part.decode(charset, errors='replace'))
|
||||
else:
|
||||
result.append(part)
|
||||
return "".join(result)
|
||||
|
||||
def parse_message(msg) -> dict:
|
||||
"""email.message.Message → dict"""
|
||||
body_text = body_html = ""
|
||||
attachments = []
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
ct = part.get_content_type()
|
||||
cd = str(part.get('Content-Disposition', ''))
|
||||
if ct == 'text/plain' and 'attachment' not in cd:
|
||||
body_text = _decode_payload(part)
|
||||
elif ct == 'text/html' and 'attachment' not in cd:
|
||||
body_html = _decode_payload(part)
|
||||
elif 'attachment' in cd or part.get_filename():
|
||||
attachments.append({
|
||||
"filename": decode_header_str(part.get_filename() or "unnamed"),
|
||||
"content_type": ct,
|
||||
"size": len(part.get_payload(decode=True) or b""),
|
||||
})
|
||||
else:
|
||||
ct = msg.get_content_type()
|
||||
if ct == 'text/html':
|
||||
body_html = _decode_payload(msg)
|
||||
else:
|
||||
body_text = _decode_payload(msg)
|
||||
return {
|
||||
"subject": decode_header_str(msg.get("Subject", "")),
|
||||
"from": decode_header_str(msg.get("From", "")),
|
||||
"to": decode_header_str(msg.get("To", "")),
|
||||
"cc": decode_header_str(msg.get("Cc", "")),
|
||||
"date": msg.get("Date", ""),
|
||||
"body_text": body_text,
|
||||
"body_html": body_html,
|
||||
"attachments": attachments,
|
||||
}
|
||||
|
||||
def _decode_payload(part) -> str:
|
||||
raw = part.get_payload(decode=True) or b""
|
||||
charset = part.get_content_charset() or chardet.detect(raw).get('encoding', 'utf-8') or 'utf-8'
|
||||
return raw.decode(charset, errors='replace')
|
||||
17
CLAUDE.md
17
CLAUDE.md
@ -279,6 +279,23 @@ GUARDiA ITSM (허브, :9001/:8443)
|
||||
|
||||
---
|
||||
|
||||
## 하네스: zioinfo-mail 웹메일 시스템
|
||||
|
||||
**목표:** 지오정보기술 Postfix/Dovecot SMTP 서버를 활용한 React+FastAPI 웹메일 클라이언트 구축. `mail.zioinfo.co.kr:8025`, IMAP/SMTP 연동, 3-패널 메일 UI.
|
||||
|
||||
**트리거:** 웹메일, zioinfo-mail, 메일 클라이언트, IMAP/SMTP 연동 요청 시 `zioinfo-mail-orchestrator` 스킬을 사용하라.
|
||||
|
||||
**에이전트:** mail-backend-dev (FastAPI IMAP/SMTP 프록시), mail-frontend-dev (React SPA), mail-infra-setup (nginx/systemd/배포)
|
||||
|
||||
**인프라:** Postfix(25/587) + Dovecot(143/993) 기운영 중 | 계정: ythong/info/admin @zioinfo.co.kr
|
||||
|
||||
**변경 이력:**
|
||||
| 날짜 | 변경 내용 | 대상 | 사유 |
|
||||
|------|----------|------|------|
|
||||
| 2026-06-01 | 초기 하네스 구성 | 전체 | zioinfo SMTP 기반 웹메일 구축 |
|
||||
|
||||
---
|
||||
|
||||
## 하네스: 5개 시스템 배포 동기화
|
||||
|
||||
**목표:** guardia-itsm·zioinfo-web·guardia-manager·guardia-messenger·guardia-docs 5개 시스템의 workspace↔repos↔Gitea↔서버 4-way 동기화 상태를 검증하고 이슈를 자동 수정한다.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user