Feat: Email Docs 추가

This commit is contained in:
Heejae 2025-07-09 17:23:42 +09:00
parent 4eaa7d3d1b
commit 915dc2a921
2 changed files with 277 additions and 0 deletions

View File

@ -31,6 +31,7 @@ Slack 기반 AI 어시스턴트 **로빙(Robeing)** 프로젝트의 모든 문
- [Slack 테스트 가이드](./docs/setups/slack-test-guide.md)
- [Socket Mode 테스트 가이드](./docs/setups/socket-mode-test.md)
- [ngrok 테스트 가이드](./docs/setups/ngrok-test-guide.md)
- [이메일 설정 가이드](./docs/setups/Email_readme.md)
### 외부 참조
- [Slack API 문서](https://api.slack.com/web)

276
docs/setups/Email_readme.md Normal file
View File

@ -0,0 +1,276 @@
# Gmail Push + Send with FastAPI (전체 가이드)
> **범위** FastAPI 백엔드 한 곳에서 다음 두 가지를 모두 처리하는 완전한 단계별 설명(생략 없음)
>
> 1. **새 메일 실시간 수신**: Gmail API `users.watch` → Pub/Sub Push → FastAPI `/push`
> 2. **메일 발송**: Gmail API `users.messages.send`
>
> 아래 문서는 우리가 실행한 **모든** 셸 명령어·권한 설정·오류 해결(Org Policy, IAM, ngrok3200 등)을 그대로 보존합니다. 로컬 개발부터 프로덕션 배포(Nginx + Lets Encrypt, Cloud Run)까지 전 과정을 다룹니다.
---
## 0. 준비물 요약
| 항목 | 내용 |
| -------------- | ---------------------------------------------------------------------------- |
| **GCP 프로젝트** | `email-465312` (예시) Gmail API, Pub/Sub API 활성화 |
| **OAuth 토큰** | 로컬 PC에서 `InstalledAppFlow` 로 발급한 \`\`(scope: `gmail.readonly`, `gmail.send`) |
| **서비스 계정(SA)** | `mail-bot-sa@<PROJECT_ID>.iam.gserviceaccount.com` Pub/Sub Push JWT 서명용 |
| **Pub/Sub 토픽** | `projects/email-465312/topics/gmail-push` |
| **Push 구독** | `gmail-push-sub``https://<HTTPS 도메인>/push` |
| **도메인 제한** | Org Policy `iam.allowedPolicyMemberDomains`  \`\` 만 허용 → 우회 절차 포함 |
---
## 1. 로컬 PC OAuth & 첫 토큰 발급
```bash
pip install google-auth-oauthlib google-api-python-client
python auth_flow.py # 아래 스크립트 참고
```
```python
auth_flow.py
------------
from google_auth_oauthlib.flow import InstalledAppFlow
SCOPES=["https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/gmail.send"]
flow=InstalledAppFlow.from_client_secrets_file("client_secret.json",SCOPES)
creds=flow.run_local_server(port=8080,access_type="offline",
include_granted_scopes="true")
open("token.json","w").write(creds.to_json())
```
`token.json` 을 안전한 경로에 보관(Secret Manager 권장).
---
## 2. IAM · Org Policy 셋업 (모든 명령은 \$PROJECT\_ID, \$PROJECT\_NUMBER, \$SA 변수 사용)
```bash
export PROJECT_ID=email-465312
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
export SA=mail-bot-sa
# 21) 서비스 계정 생성
gcloud iam service-accounts create $SA \
--description="Gmail push auth SA" \
--display-name="Mail Bot SA" \
--project=$PROJECT_ID
# 22) Pub/Sub 서비스 에이전트에 TokenCreator 권한
gcloud iam service-accounts add-iam-policy-binding \
$SA@$PROJECT_ID.iam.gserviceaccount.com \
--member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com" \
--role="roles/iam.serviceAccountTokenCreator" \
--project=$PROJECT_ID
# 23) 토픽 생성 및 Gmail 시스템 계정 퍼블리셔 권한
gcloud pubsub topics create gmail-push --project=$PROJECT_ID
gcloud pubsub topics add-iam-policy-binding gmail-push \
--member=serviceAccount:gmail-api-push@system.gserviceaccount.com \
--role=roles/pubsub.publisher \
--project=$PROJECT_ID
```
### ⚠️ Org Policy 오류(FAILED\_PRECONDITION) 해결
1. **문제**: 조직 정책 `iam.allowedPolicyMemberDomains` 가 외부 도메인 차단 → `gmail-api-push@system.gserviceaccount.com` 추가 실패.
2. **우회 절차**
```bash
# 정책 리셋 (임시로 차단 해제)
gcloud org-policies reset constraints/iam.allowedPolicyMemberDomains \
--project=$PROJECT_ID
# 퍼블리셔 권한 재시도 → 성공
# 정책 복원
cat >restore.yaml <<EOF
constraint: constraints/iam.allowedPolicyMemberDomains
listPolicy:
allowedValues:
- C01h2nc1g
EOF
gcloud resource-manager org-policies set-policy --project=$PROJECT_ID restore.yaml
```
---
## 3. Push 구독 생성 (ngrok 또는 고정 HTTPS 도메인)
```bash
export HTTPS_URL=https://abcd-1234.ngrok-free.app # 최초 또는 변경된 외부 HTTPS 주소
gcloud pubsub subscriptions create gmail-push-sub \
--topic=gmail-push \
--push-endpoint=$HTTPS_URL/push \
--push-auth-service-account=$SA@$PROJECT_ID.iam.gserviceaccount.com \
--project=$PROJECT_ID
```
> **이미 존재 오류** — `FAILED_PRECONDITION: Resource already exists` → 구독은 하나만 필요하므로 **새로 만들지 말고 URL만 수정**합니다.
### 3A 구독 URL만 바꾸기
```bash
# ngrok 재시작, 도메인 변경 등으로 https 주소가 바뀐 경우
export NEW_URL=https://fc29def256f2.ngrok-free.app
gcloud pubsub subscriptions modify-push-config gmail-push-sub \
--push-endpoint=$NEW_URL/push \
--push-auth-service-account=$SA@$PROJECT_ID.iam.gserviceaccount.com \
--project=$PROJECT_ID
```
- `--push-auth-service-account` 를 생략하면 기존 값이 유지됩니다.
- 변경은 실시간 적용 몇 초 뒤부터 새 URL로 POST 도착.
### 3B (참고) 구독 삭제 후 재생성
```bash
# 메시지 중복·유실 리스크가 있으므로 특별한 이유가 없으면 modifypushconfig 권장
gcloud pubsub subscriptions delete gmail-push-sub --project=$PROJECT_ID
gcloud pubsub subscriptions create gmail-push-sub \
--topic=gmail-push \
--push-endpoint=$NEW_URL/push \
--push-auth-service-account=$SA@$PROJECT_ID.iam.gserviceaccount.com \
--project=$PROJECT_ID
```
---
## 4. FastAPI (main.py) 최소 구현
FastAPI (main.py) 최소 구현
```python
import os, json, base64, datetime
from fastapi import FastAPI, Request
from pydantic import BaseModel, EmailStr
from email.message import EmailMessage
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
SCOPES=[
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/gmail.send",
]
CREDS=Credentials.from_authorized_user_file("token.json",SCOPES)
gmail=build("gmail","v1",credentials=CREDS,cache_discovery=False)
TOPIC=os.getenv("TOPIC","projects/email-465312/topics/gmail-push")
LAST=".last_history"
app=FastAPI()
# --- watch 등록
@app.on_event("startup")
def register_watch():
resp=gmail.users().watch(userId="me",body={"topicName":TOPIC,"labelIds":["INBOX"]}).execute()
open(LAST,"w").write(resp["historyId"])
print("watch expires",datetime.datetime.fromtimestamp(int(resp["expiration"])/1000))
# --- Push 엔드포인트
@app.post("/push")
async def push(req:Request):
msg=await req.json()
data=json.loads(base64.urlsafe_b64decode(msg["message"]["data"]))
new_id=data["historyId"]
start=open(LAST).read()
hist=gmail.users().history().list(userId="me",startHistoryId=start,historyTypes=["messageAdded"]).execute()
for h in hist.get("history",[]):
for m in h.get("messages",[]):
full=gmail.users().messages().get(userId="me",id=m["id"],format="full").execute()
print("📨",full["snippet"])
open(LAST,"w").write(new_id)
return {"ok":True}
# --- Send 메일
class SendReq(BaseModel):
to: EmailStr
subject:str
body:str
@app.post("/send")
def send(req:SendReq):
m=EmailMessage(); m["To"],m["Subject"]=req.to,req.subject
m.set_content(req.body)
raw=base64.urlsafe_b64encode(m.as_bytes()).decode()
sent=gmail.users().messages().send(userId="me",body={"raw":raw}).execute()
return {"id":sent["id"],"status":"sent"}
```
### 로컬 실행 & ngrok
```bash
uvicorn main:app --reload --port 8000
ngrok http 8000
```
### cURL 발송 테스트
```bash
curl -X POST https://abcd-1234.ngrok-free.app/send \
-H "Content-Type: application/json" \
-d '{"to":"0914eagle@gmail.com","subject":"데모","body":"테스트 메일"}'
```
---
## 5. 운영 배포 시나리오
### 5A. VM + Nginx + Let's Encrypt
(1) DNS `mailbot.ro-being.com` A → 서버 IP. (2) `certbot --nginx -d mailbot.ro-being.com`. (3) `/etc/nginx/sites-available/mailbot` → `proxy_pass http://127.0.0.1:8000;`. (4) Pub/Sub `modify-push-config` 로 새 HTTPS URL 반영.
### 5B. Cloud Run
```bash
gcloud builds submit --tag gcr.io/$PROJECT_ID/mailbot:latest
gcloud run deploy mailbot --image gcr.io/$PROJECT_ID/mailbot:latest --allow-unauthenticated
```
Run URL (`https://mailbot-xyz.a.run.app`) 를 Push 엔드포인트로 사용.
---
## 6. 자동화 & 모니터링
| 항목 | 구현 |
| -------------- | ----------------------------------------------------------------------- |
| watch 세션 7일 만료 | Cloud Scheduler → 하루 1회 GET `https://<DOMAIN>/register_watch` |
| `/push` JWT 검증 | `google.oauth2.id_token.verify_oauth2_token()` → `info["email"] == $SA` |
| 비밀 관리 | GCP Secret Manager에 `token.json`, `.env` |
| 로깅 & 알림 | Cloud Logging → Error Alert Policy |
---
## 7. 트러블슈팅 로그 모음
### Org Policy 오류
```
FAILED_PRECONDITION: User gmail-api-push@system.gserviceaccount.com is not in permitted organization
```
- 해결: 정책 reset → 바인딩 → 정책 복원 (위 2단계 참조)
### ngrok ERR\_NGROK\_3200
- 원인: 터널 꺼짐 → `ngrok http 8000` 재실행, Push 구독 URL 갱신
### curl `(6) Could not resolve host: https`
- URL 타이포 `https://https://` → 중복 `https://` 하나 삭제
---
## 8. 체크리스트 (최종)
-
모든 체크가 끝나면 실시간 Gmail 자동화가 완전히 작동합니다!