DOCS/_archive/docs/setups/Email_readme.md
2025-08-13 14:04:25 +09:00

277 lines
9.5 KiB
Markdown
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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