# 오피셜메일

## Microsoft Store 다운로드 전환

오피셜메일 Windows 스토어 ID는 `9NCD8ZQFP0NJ`입니다. **개발자 계정 승인만으로 전환하지 않습니다.** 앱 심사 승인 후 [공개 스토어 페이지](https://apps.microsoft.com/detail/9NCD8ZQFP0NJ)에서 설치가 가능한 것을 확인하고 웹 서버 환경에 `OFFICIAL_MAIL_WINDOWS_STORE_APPROVED=true`를 적용하여 재배포합니다.

그전까지는 기본값 `false`로 기존 EXE 다운로드를 유지합니다. 전환되는 것은 홈페이지·메일함의 최신 Windows 다운로드 진입점뿐입니다. 기존 앱이 사용하는 버전별 설치파일 URL과 업데이트 매니페스트는 유지하며, 무서명 MSIX 파일을 직접 다운로드용으로 게시하지 않습니다.

스토어용 MSIX와 Android AAB는 `flutter_app` 브랜치에서 빌드합니다. 패치 시 웹 배포, Windows 스토어 제출, Android 빌드·제출 상태를 각각 확인하며, 심사 중인 버전을 출시 완료로 보고하지 않습니다.

혼자 시작하는 사업자를 위한 무료 기업메일 서비스 MVP 레포입니다.

## 포함한 범위

- Next.js 랜딩페이지
- 로그인 페이지
- 대시보드
- 헤더 우측 상단 프로필 아이콘
- 프로필 아이콘으로 진입하는 메일 사이트
- 설정 미완료 사용자를 위한 로딩 후 페이드 오버레이
- 메일 설정 페이지
- Ubuntu 서버 배포 스크립트
- DNS / Mailcow 문서

## 라우트

- `/` 랜딩페이지
- `/login` 로그인
- `/dashboard` 사용자 대시보드
- `/mail` 메일 사이트 진입 페이지
- `/mail-setup` 메일 설정 페이지

## 로컬 실행

### 유입 분석 검증

- `node --test scripts/test-marketing-attribution.mjs`는 실제 페이지 목록, 다국어/게시글, 인앱 출처, IP, 개인정보 제외를 검사합니다. 새 페이지를 추가할 때 수집 정책도 함께 갱신합니다.
- 공개 페이지와 로그인/가입/계정 복구는 `landing_view`, 메일함/설정/결제 화면은 `app_view`로 구분합니다. 후자는 최고관리자 유입 분석의 `메일함 및 설정 페이지 이용 보기`에서 확인하며 신규 유입 수에 합산하지 않습니다. 관리자, API, 메일 본문 iframe, 로컬 개발 접속은 제외합니다.
- 방문 실패를 성공 처리하거나 가입 완료로 가짜 `/signup` 방문을 만들지 않습니다. 과거 누락된 유입, 게시글 URL, IP는 추측해서 복구하지 않습니다.
- UTM과 외부 리퍼러를 30분 동안 서브도메인 이동에도 유지합니다. 게시글 경로가 전달되면 보관하되 쿼리/해시는 제거합니다. 리퍼러가 없으면 명시적인 앱 식별 정보로 `스레드 (인앱 추정)` 등으로 분류합니다. 일반 Safari/Chrome이나 `fbclid`만으로 앱을 단정하지 않습니다.
- 출처 전달이 없는 앱에서 확실하게 구분하려면 공유 링크에 `?utm_source=threads&utm_medium=social&utm_campaign=post_slug`처럼 UTM을 붙입니다. 원본 글 URL이 전달되지 않으면 UTM으로도 원본 주소를 복원할 수는 없습니다.
- IP는 외부 프록시가 실제 소켓 주소로 **덮어쓴** `X-Real-IP`를 사용합니다. 외부 Nginx는 해당 사이트 프록시 location에 `proxy_set_header X-Real-IP $remote_addr;`를 설정하고 내부 Apache/Next 포트는 외부에 직접 노출하지 않습니다. Docker 내부 IP는 수집/해시/네트워크 집계에서 제외합니다.
- XFF 전용 환경만 `OFFICIAL_MAIL_TRUSTED_PROXY_IPS`에 실제 프록시 주소를 명시합니다. 무조건 첫 번째 XFF나 임의의 `CF-Connecting-IP`를 신뢰하지 않습니다. CDN을 도입할 때는 CDN 공식 IP 대역에 한해서 외부 프록시의 real-IP 처리를 먼저 설정합니다.
- 최고관리자 로그인 후 `/api/kavenix/analytics/request-context`에서 현재 요청의 IP 전달 상태를 검사할 수 있습니다. 쿠키/인증 토큰은 반환하지 않고 일반 사용자와 비로그인은 차단합니다.

```bash
npm install
npm run dev
```

`.env.production`:

```bash
NEXT_PUBLIC_APP_URL=https://your-domain.example
NEXT_PUBLIC_MAIL_APP_URL=https://mail.your-domain.example
DB_HOST=your-db-host
DB_PORT=3306
DB_USER=your-db-user
DB_PASSWORD=your-db-password
DB_NAME=official_mail
MAILCOW_BASE_URL=https://mx1.your-domain.example
MAILCOW_API_KEY=your-mailcow-rw-api-key
MAILCOW_ALLOW_SELF_SIGNED=false
MAILCOW_REQUEST_TIMEOUT_MS=15000
MAILCOW_DOMAIN_ALIAS_LIMIT=400
MAILCOW_DOMAIN_MAILBOX_LIMIT=0
MAILCOW_DOMAIN_QUOTA_MB=0
MAILCOW_MAILBOX_QUOTA_MB=2048
MAILCOW_FREE_MAILBOX_QUOTA_MB=2048
MAILCOW_GROWTH_MAILBOX_QUOTA_MB=10240
MAILCOW_BUSINESS_MAILBOX_QUOTA_MB=102400
MAILCOW_DOMAIN_MAX_MAILBOX_QUOTA_MB=102400
MAILCOW_QUOTA_DOWNGRADE_GRACE_DAYS=7
MAILCOW_DKIM_KEY_SIZE=2048
MX_HOSTNAME=mx1.your-domain.example
DKIM_SELECTOR=dkim
DMARC_RUA=mailto:dmarc@your-domain.example
```

`MAILCOW_API_KEY` 는 Mailcow 관리자 화면의 `Configuration -> Access -> API` 에서
읽기/쓰기 키로 생성하고, 호출 서버 IP 를 허용 목록에 추가해야 합니다.

`MAILCOW_BASE_URL` 은 Swagger 주소인 `/api` 가 아니라 Mailcow 호스트 루트
예: `https://mx1.your-domain.example` 로 넣어야 합니다. 앱이 내부에서 `/api/v1/...`
경로를 직접 붙여 호출합니다.

공개 도메인 + 정상 인증서 구조라면 `MAILCOW_ALLOW_SELF_SIGNED=false` 가 맞습니다.
호스트 로컬 `127.0.0.1:8443` 또는 self-signed 인증서를 쓰는 구조에서만
`MAILCOW_ALLOW_SELF_SIGNED=true` 를 사용하세요.

현재 오피셜메일 앱은 Mailcow API 를 브라우저에서 직접 호출하지 않고 Next.js 서버가
서버-투-서버로 호출합니다. 따라서 기본 구성에서는 Mailcow CORS 설정이 필요하지 않습니다.
Mailcow 쪽에는 API 키와 허용 IP 만 맞추면 됩니다.

메일함 용량은 무료 2GB, 성장 10GB, 비즈니스 100GB 기준으로 동기화됩니다.
`MAILCOW_DOMAIN_MAX_MAILBOX_QUOTA_MB` 는 가장 큰 플랜 용량보다 작게 설정할 수 없습니다.
구독이 끝나 용량보다 사용량이 많아져도 기존 메일은 삭제하지 않으며, 사용량을 줄일 때까지
새 메일 수신과 추가 저장이 제한됩니다. 일반 해지는 이용 종료 후 7일간 정리 기간을 둔 뒤
무료 플랜 용량으로 낮추며, 청약철회처럼 즉시 무료 플랜으로 복귀하는 경우에는 정리 기간을 적용하지 않습니다.

만약 별도의 프론트엔드에서 브라우저가 Mailcow API 를 직접 호출하도록 만들 경우에만
CORS 를 설정하세요.

- `Access-Control-Allow-Origin`: 호출하는 웹앱의 정확한 Origin
  예: `https://mail.officialsite.kr`
- `Access-Control-Allow-Methods`: `GET, POST, PUT, DELETE, OPTIONS`

`mx1.officialsite.kr` 는 API 서버 주소이지 브라우저 Origin 값이 아니므로,
CORS Origin 칸에는 보통 넣으면 안 됩니다.

도메인 등록과 Mailcow 프로비저닝은 같은 Next.js repo 안에서 처리하므로 별도 repo 는 필요하지 않습니다.

## DB 스키마 생성

```bash
python scripts/setup_official_mail_db.py
```

이 스크립트는 MySQL/MariaDB 서버에 `official_mail` 데이터베이스와 관련 테이블을 생성합니다.

## 서버 세팅 문서

- [deploy/ubuntu/MAIL_SERVICE_RUNBOOK.md](deploy/ubuntu/MAIL_SERVICE_RUNBOOK.md)
- [deploy/ubuntu/04-host-nginx-mail.conf.example](deploy/ubuntu/04-host-nginx-mail.conf.example)
- [.github/workflows/deploy-officialsite.yml](.github/workflows/deploy-officialsite.yml)

## 주의

실서비스용 비밀값은 절대로 레포나 GitHub Actions 워크플로에 넣지 말고,
서버의 `.env.production` 또는 별도 비밀 저장소에서만 관리해야 합니다.

## 메일 동기화 로그 확인

메일함 동기화가 멈춘 것 같으면 아래 순서대로 확인하면 됩니다.

앱 로그 확인

```bash
pm2 logs officialsite-kr --lines 200
```

AI 릴레이 답장 worker 실행

```bash
pm2 start npm --name official-mail-ai-reply-relay -- run worker:ai-reply-relay
pm2 save
pm2 logs official-mail-ai-reply-relay --lines 100
```

`OFFICIAL_MAIL_CRON_SECRET` 은 Next 앱과 worker 프로세스에서 같은 값을 사용해야 합니다.

앱 로그를 파일 tail 로 계속 보기

```bash
tail -f ~/.pm2/logs/officialsite-kr-out.log ~/.pm2/logs/officialsite-kr-error.log
```

앱 로그에서 메일 관련 에러만 빠르게 보기

```bash
grep -Ei "mailbox|imap|smtp|sync|mailcow|error" ~/.pm2/logs/officialsite-kr-*.log | tail -100
```

Mailcow 전체 메일 관련 로그 실시간 보기

```bash
cd /opt/mailcow-dockerized
docker compose logs -f --tail=100 dovecot-mailcow postfix-mailcow nginx-mailcow php-fpm-mailcow
```

IMAP 수신 동기화 로그만 보기

```bash
cd /opt/mailcow-dockerized
docker compose logs -f --tail=100 dovecot-mailcow
```

SMTP 수신/발송 로그만 보기

```bash
cd /opt/mailcow-dockerized
docker compose logs -f --tail=100 postfix-mailcow
```

DB 조회 전에 앱 환경변수 로드

```bash
cd /var/www/front/official-mail
set -a
source .env.production
set +a
```

현재 동기화 상태 DB 확인

```bash
mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" -e "
SELECT
  id,
  email,
  last_sync_at,
  LEFT(COALESCE(last_sync_error, ''), 300) AS last_sync_error
FROM mailboxes
ORDER BY id DESC;
"
```

특정 메일함의 폴더별 동기화 상태 확인

```bash
MAILBOX_EMAIL="대표메일주소@example.com"
mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" -e "
SELECT
  f.system_name,
  f.name,
  f.remote_total,
  f.remote_unseen,
  f.last_synced_at
FROM mailbox_folders f
JOIN mailboxes m ON m.id = f.mailbox_id
WHERE m.email = '$MAILBOX_EMAIL'
ORDER BY f.sort_order ASC, f.id ASC;
"
```

최근 수신 메일이 DB 에 들어왔는지 확인

```bash
MAILBOX_EMAIL="대표메일주소@example.com"
mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" -e "
SELECT
  mm.id,
  f.system_name,
  mm.from_address,
  mm.subject,
  mm.is_read,
  mm.received_at
FROM mailbox_messages mm
JOIN mailbox_folders f ON f.id = mm.folder_id
JOIN mailboxes m ON m.id = mm.mailbox_id
WHERE m.email = '$MAILBOX_EMAIL'
ORDER BY mm.received_at DESC, mm.id DESC
LIMIT 30;
"
```

주로 보는 포인트

- `mailboxes.last_sync_error` 에 `mailbox-auth-missing`, `mailbox-auth-invalid`, `mailbox-imap-failed:*` 가 남는지 확인
- `mailbox_folders.last_synced_at` 이 오래 멈춰 있으면 IMAP 동기화가 안 돈 상태인지 확인
- `dovecot-mailcow` 로그에 로그인 실패나 폴더 open 실패가 있는지 확인
- `postfix-mailcow` 로그에 실제 수신은 됐는데 앱 쪽만 반영이 안 되는지 확인
