- 모든 .md, .html 파일 권한을 644로 정상화 - .gitignore 파일 권한도 644로 수정 - 문서 파일에 실행 권한은 불필요하고 보안상 바람직하지 않음 - deprecated 아이디어 폴더 생성 및 레벨별 UI 변경 아이디어 이동
9.5 KiB
9.5 KiB
Gmail Push + Send with FastAPI (전체 가이드)
범위 – FastAPI 백엔드 한 곳에서 다음 두 가지를 모두 처리하는 완전한 단계별 설명(생략 없음)
- 새 메일 실시간 수신: Gmail API
users.watch→ Pub/Sub Push → FastAPI/push- 메일 발송: 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@<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 & 첫 토큰 발급
pip install google-auth-oauthlib google-api-python-client
python auth_flow.py # 아래 스크립트 참고
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 변수 사용)
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) 해결
- 문제: 조직 정책
iam.allowedPolicyMemberDomains가 외부 도메인 차단 →gmail-api-push@system.gserviceaccount.com추가 실패. - 우회 절차
# 정책 리셋 (임시로 차단 해제) 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 도메인)
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만 수정합니다.
3‑A 구독 URL만 바꾸기
# 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 (참고) 구독 삭제 후 재생성
# 메시지 중복·유실 리스크가 있으므로 특별한 이유가 없으면 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) 최소 구현
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
uvicorn main:app --reload --port 8000
ngrok http 8000
cURL 발송 테스트
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
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 자동화가 완전히 작동합니다!