diff --git a/README.md b/README.md index 39e3907..11c7418 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/setups/Email_readme.md b/docs/setups/Email_readme.md new file mode 100644 index 0000000..9fdcf80 --- /dev/null +++ b/docs/setups/Email_readme.md @@ -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, ngrok 3200 등)을 그대로 보존합니다. 로컬 개발부터 프로덕션 배포(Nginx + Let’s Encrypt, Cloud Run)까지 전 과정을 다룹니다. + +--- + +## 0. 준비물 요약 + +| 항목 | 내용 | +| -------------- | ---------------------------------------------------------------------------- | +| **GCP 프로젝트** | `email-465312` (예시) – Gmail API, Pub/Sub API 활성화 | +| **OAuth 토큰** | 로컬 PC에서 `InstalledAppFlow` 로 발급한 \`\`(scope: `gmail.readonly`, `gmail.send`) | +| **서비스 계정(SA)** | `mail-bot-sa@.iam.gserviceaccount.com` – Pub/Sub Push JWT 서명용 | +| **Pub/Sub 토픽** | `projects/email-465312/topics/gmail-push` | +| **Push 구독** | `gmail-push-sub` → `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 + +# 2‑1) 서비스 계정 생성 +gcloud iam service-accounts create $SA \ + --description="Gmail push auth SA" \ + --display-name="Mail Bot SA" \ + --project=$PROJECT_ID + +# 2‑2) 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 + +# 2‑3) 토픽 생성 및 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 < **이미 존재 오류** — `FAILED_PRECONDITION: Resource already exists` → 구독은 하나만 필요하므로 **새로 만들지 말고 URL만 수정**합니다. + +### 3‑A 구독 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 도착. + +### 3‑B (참고) 구독 삭제 후 재생성 + +```bash +# 메시지 중복·유실 리스크가 있으므로 특별한 이유가 없으면 modify‑push‑config 권장 + +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. 운영 배포 시나리오 + +### 5‑A. 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 반영. + +### 5‑B. 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:///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 자동화가 완전히 작동합니다! +