# 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 자동화가 완전히 작동합니다!