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:
DESKTOP-TKLFCPR\ython 2026-06-01 21:19:51 +09:00
parent 19dd2c0c09
commit 60be2f9375
6 changed files with 692 additions and 0 deletions

View 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에게 엔드포인트 목록 전달

View 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로 질의

View 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 업데이트 완료 보고

View 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

View File

@ -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')

View File

@ -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 동기화 상태를 검증하고 이슈를 자동 수정한다.