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개 시스템 배포 동기화
|
## 하네스: 5개 시스템 배포 동기화
|
||||||
|
|
||||||
**목표:** guardia-itsm·zioinfo-web·guardia-manager·guardia-messenger·guardia-docs 5개 시스템의 workspace↔repos↔Gitea↔서버 4-way 동기화 상태를 검증하고 이슈를 자동 수정한다.
|
**목표:** guardia-itsm·zioinfo-web·guardia-manager·guardia-messenger·guardia-docs 5개 시스템의 workspace↔repos↔Gitea↔서버 4-way 동기화 상태를 검증하고 이슈를 자동 수정한다.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user