# Phase 16 — 실제 이메일 계정 연동 (Gmail + Outlook)

> mock 시드 메일을 넘어 **내 Gmail·Outlook 계정을 OAuth로 붙여** 받은편지함을 수집하고,
> 회신/새 메일을 **결재 승인 시 실제 발송**한다. mock 데모는 1바이트도 바뀌지 않는다
> (`CONNECTOR_MAIL=mock` 기본, real은 연결된 계정에만 적용).

이 문서는 **OAuth 앱 자격증명 발급(Google Cloud · Azure) 가이드**와 동작 확인 절차다.
코드는 이미 구현되어 있고, 자격증명만 `backend/.env`에 넣으면 실연동이 켜진다.

---

## 1. 무엇이 동작하나

- **Gmail**: Google OAuth(Authorization Code + PKCE) → Gmail REST API로 받은편지함 증분 수집 + 발송.
- **Outlook / Microsoft 365**: Microsoft Graph OAuth → `/me/messages` 수집 + `/me/sendMail` 발송.
- **Outlook / M365 캘린더**: 같은 Microsoft 앱 등록에 `Calendars.Read` 권한만 더하면
  `/me/calendarView`로 일정 수집(메일과 **별개 계정** "Outlook 캘린더 추가"로 붙음, 설정 → 일정 탭).
- **회사 M365**: 개인 Outlook.com과 **같은 앱 하나**로 처리(테넌트 `common`). 회사 계정은
  "Outlook 추가"를 한 번 더 눌러 로그인하면 별도 계정으로 붙음. 단 회사가 외부 앱 동의를
  잠가뒀으면 "관리자 승인 필요"가 떠서 IT에 이 앱(클라이언트 ID) 승인을 요청해야 함.
- **멀티계정**: 같은 제공자라도 이메일 주소가 다르면 별도 계정으로 추가(덮어쓰기 없음).
- **읽기 + 보내기**: 메일 페이지의 회신/새 메일은 high-risk **결재함**으로 가고, 승인 순간
  연결된 real 계정이면 진짜 발송, 미연결이면 보낸편지함(mock) 기록.

> **왜 OAuth인가 (SMTP/IMAP 비밀번호 불가):** Gmail은 2022년 basic-auth(앱 비밀번호 제외)를,
> Outlook.com(개인)은 2024-09-16부로 IMAP/POP/SMTP basic-auth를, M365(직장/학교)는 그 이전에
> 차단했다. Outlook은 OAuth2가 **유일한** 경로라 둘 다 OAuth로 통일했다.

---

## 2. Google Cloud — Gmail OAuth 클라이언트 발급

1. <https://console.cloud.google.com> → 프로젝트 생성(예: `ari-mail`).
2. **API 및 서비스 → 라이브러리** → **Gmail API** 사용 설정.
3. **OAuth 동의 화면**:
   - User Type = **외부(External)**, 앱 이름/지원 이메일 입력 후 저장.
   - **게시 상태 = 테스트(Testing)** 로 두고, **테스트 사용자**에 본인 Gmail 주소 추가.
     (테스트 모드면 Google 검수 없이 본인 계정으로 바로 사용 가능 — 개인용은 이걸로 충분.)
   - 스코프는 굳이 추가 안 해도 됨(요청 시 동적으로 동의 화면에 표시됨).
4. **사용자 인증 정보 → 사용자 인증 정보 만들기 → OAuth 클라이언트 ID**:
   - 애플리케이션 유형 = **웹 애플리케이션**.
   - **승인된 리디렉션 URI** =
     `http://localhost:31800/api/connectors/oauth/callback`
   - 만들면 **클라이언트 ID / 클라이언트 보안 비밀** 발급 → 복사.

요청 스코프(코드가 자동 요청): `gmail.readonly`, `gmail.send`
(연결 계정 이메일은 `users/me/profile`로 조회 — 추가 스코프 불필요).

---

## 3. Azure Portal — Outlook(Microsoft Graph) 앱 등록

1. <https://portal.azure.com> → **Microsoft Entra ID → 앱 등록 → 새 등록**.
2. **지원되는 계정 유형** = "모든 조직 디렉터리 + 개인 Microsoft 계정"(개인 Outlook.com 포함).
   - 이 경우 테넌트는 기본 `common`(아래 `.env` `MICROSOFT_TENANT` 기본값).
3. **리디렉션 URI** = 플랫폼 **웹**,
   `http://localhost:31800/api/connectors/oauth/callback`
4. **인증서 및 비밀 → 새 클라이언트 비밀** 생성 → **값(Value)** 복사(이때만 보임).
5. **API 권한 → 권한 추가 → Microsoft Graph → 위임된 권한**:
   - 메일: `Mail.Read`, `Mail.Send`, `User.Read`, `offline_access` 추가.
   - 일정도 쓰려면: **`Calendars.Read`** 추가(이걸 넣으면 설정 → 일정 탭에 "Outlook 캘린더 추가"가 활성).
   - 개인 계정은 관리자 동의 불필요(동의 화면에서 본인이 동의). **회사 M365**는 테넌트 정책에
     따라 "관리자 승인 필요"가 뜰 수 있음 → 별도 앱이 아니라 IT에 이 앱 승인을 요청.
6. **개요**에서 **애플리케이션(클라이언트) ID** 복사.

---

## 4. backend/.env 주입

```dotenv
# 토큰 암호화 키(실연동 필수 — 32바이트 이상 임의 문자열)
ARI_SECRET_KEY=<openssl rand -hex 32 등으로 생성한 강한 키>

# real 메일 연동 ON (phase-16+: 메일은 mock 제거됨 — 연결 전까지 빈 상태)
CONNECTOR_MAIL=real

# Google (Gmail)
GOOGLE_CLIENT_ID=<2단계에서 발급>
GOOGLE_CLIENT_SECRET=<2단계에서 발급>

# Microsoft (Outlook)
MICROSOFT_CLIENT_ID=<3단계에서 발급>
MICROSOFT_CLIENT_SECRET=<3단계의 비밀 '값'>
MICROSOFT_TENANT=common

# google·microsoft 공용 콜백(양쪽 콘솔에 등록한 것과 정확히 일치해야 함)
OAUTH_REDIRECT_URI=http://localhost:31800/api/connectors/oauth/callback
```

> `ARI_SECRET_KEY`를 바꾸면 기존에 암호화 저장된 토큰은 복호화 불가(재연결 필요).
> 프로덕션에선 HTTPS 콜백 URL로 바꾸고 각 콘솔에도 그 URL을 등록한다.

---

## 5. 동작 확인

1. 백엔드 기동: `cd backend && uv run uvicorn app.main:app --port 31800`
   프론트 기동: `cd frontend && pnpm dev` (`:31300`).
2. `/settings?tab=mail` → **계정 추가** 버튼 → **Gmail**(또는 Outlook) 선택.
   - 자격증명이 설정됐으면 항목이 활성화됨(미설정이면 "설정 필요"로 비활성).
3. 제공자 동의 화면 → 허용 → 콜백이 `/settings?tab=mail?connect=ok`로 복귀하며 토스트.
4. 연결 직후 1회 초기 sync로 받은편지함 일부가 수집됨 → **메일 페이지**에 그 계정 탭으로 노출.
5. 다른 이메일로 한 번 더 추가하면 **별도 계정**으로 붙음(멀티계정).
6. 메일 회신/새 메일 작성 → **결재함**에 pending → **승인** → 실제 수신함으로 발송 확인.
   (연결 안 된 계정에서 보내면 보낸편지함 mock 기록만 남고 외부 발송은 안 됨 = 데모 안전.)

---

## 6. 구현 노트 (파일 맵)

| 영역 | 파일 | 변경 |
| --- | --- | --- |
| OAuth | `backend/app/connectors/oauth.py` | `microsoft` provider, `_family(outlook→microsoft)`, gmail.send 스코프, `_fetch_identity`(프로필 이메일), 멀티계정 `_ensure_account`+`MailAccount` upsert |
| 설정 | `backend/app/config.py` | `microsoft_client_id/secret`, `microsoft_tenant`, `oauth_redirect_uri` |
| 커넥터 | `backend/app/connectors/mail/real_outlook.py` (신규), `real_gmail.py`(`send_mail`), `normalize.py`(outlook 분기 + real 계정 매핑), `registry.py`(provider별 Gmail/Outlook 분기) | |
| 발송 | `backend/app/models.py`(`OutboundMail`), 마이그레이션 `p17e5f6a7b8c9`, `routers/mail.py`(`send()`), `connectors/mail/outbound.py`(`send_outbound`), `worker/main.py`(approval.executed 구독) | |
| 라우터 | `backend/app/routers/connectors.py` | `/connectors/providers`, oauth_start outlook 검증, 콜백 `redirect_after` 반영 |
| 프론트 | `frontend/components/connectors/AddAccountMenu.tsx`(신규), `settings/ConnectorList.tsx`·`MailTab.tsx`·`SettingsClient.tsx`(connect 토스트), `lib/connectors/api.ts`(`providers`/`oauthStart`), `lib/types.ts` | |

**설계 원칙**
- `effective_mode`: 시드 mock 계정은 env=real이어도 mock 유지(데모 결정성). real은 **연결된 계정**에만.
- 토큰은 `app/crypto.py` Fernet로 암호화 저장(평문 금지), 만료 시 `valid_access_token`이 refresh.
- 수집은 `ExternalLink` 기준 멱등 upsert(중복 없음), 발송 실패는 `OutboundMail.status=failed`로 격리.
- 발송 훅은 동기 event_bus 구독이라 `WORKER_ENABLED=false`(기본) 데모에서도 승인 즉시 처리.
  테스트는 lifespan 미가동이라 자동 발송 안 됨 → `send_outbound` 헬퍼를 직접 단위 테스트.

## 7. 범위 밖(후속)
첨부 다운로드/발송, 라벨 양방향 동기화, push/webhook(현재 polling), 프로덕션 OAuth 검수, 멀티유저.
