generated from ttipo/project-base
docs: 4단계(계정 · 메일 설정) 계약과 디자인
- 계약 1절: PUT /api/auth/password — 지금 비밀번호 틀림은 400 AUTH_005(401 이면 클라이언트가 갱신 · 재시도한다), 로그인과 같은 실패 한도, 바뀌면 이 세션만 남기고 다른 로그인을 끊는다(결과를 모를 때도) - 계약 2절: 메일함 관리(만들기 · 이름 바꾸기 · 지우기 · 모두 읽음 · 비우기 — 500통씩 · 한 요청 25초, 같은 메일함은 하나씩), 부재중 자동 응답(JMAP VacationResponse), 스팸 표시(isJunk) · 원문(.eml) - 디자인 캔버스 4단계 보드(SettingsAccount · SettingsVacation · MailboxManage · MailActions4)와 README "4단계" · "4단계 구현하며 정한 보완" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+115
-6
@@ -8,7 +8,7 @@
|
||||
|
||||
- 성공 `{ "success": true, "data": … }` / 실패 `{ "success": false, "error": { "code", "message", "traceId" } }`
|
||||
- 날짜·시각은 ISO 8601 UTC 문자열, 목록은 `{ items, pageInfo }`
|
||||
- 이 단계에서 쓰는 에러 코드: `AUTH_001` `AUTH_002`(클라이언트 전용) `AUTH_003` `AUTH_004` `VAL_001` `RES_001` `REQ_001` `FILE_001` `FILE_002` `MAIL_001` `MAIL_002` `MAIL_003` `MAIL_004` `MAIL_005` `MAIL_006` `MAIL_007` `MAIL_008` `SYS_001`
|
||||
- 이 단계에서 쓰는 에러 코드: `AUTH_001` `AUTH_002`(클라이언트 전용) `AUTH_003` `AUTH_004` `AUTH_005` `VAL_001` `RES_001` `REQ_001` `FILE_001` `FILE_002` `MAIL_001` `MAIL_002` `MAIL_003` `MAIL_004` `MAIL_005` `MAIL_006` `MAIL_007` `MAIL_008` `MAIL_009` `SYS_001`
|
||||
- **적용 범위: 1단계는 웹(브라우저) 전용이다.** 인증은 HttpOnly 쿠키뿐이고 토큰을 본문으로 주지 않는다.
|
||||
모바일 앱(Flutter)의 인증 방식(토큰 발급·`flutter_secure_storage`)은 앱 착수 때 이 문서에 추가한다.
|
||||
- **배포 형태: 화면과 API 는 같은 주소(origin)를 쓴다.** 운영은 프론트 nginx 가 `/api` 를 백엔드로 넘기고,
|
||||
@@ -43,8 +43,8 @@
|
||||
| --- | --- |
|
||||
| 계정 없음 · 비밀번호 틀림 · 사내 도메인이 아닌 주소 · 메일 엔진 관리자 계정(웹메일 로그인 대상이 아니다) (구분하지 않는다) | 401 `AUTH_004` |
|
||||
| 형식 오류 | 400 `VAL_001` |
|
||||
| 시도 과다 — 같은 IP 에서 1분에 **실패** 20회, 같은 주소로 15분에 **실패** 10회 (성공한 로그인은 세지 않는다 — 사무실처럼 여러 직원이 한 IP 를 쓰는 환경 때문). 이와 별도로 같은 IP 의 로그인 **요청**은 1분에 60회까지. "같은 IP" 는 IPv6 를 **/64 단위**로 묶는다(IPv4-mapped 주소는 IPv4 로). 사내 도메인이 아닌 주소는 IP 한도에만 센다 | 429 `REQ_001` |
|
||||
| 같은 주소의 로그인이 아직 처리 중 (버튼을 두 번 누른 경우 등) | 429 `REQ_001` |
|
||||
| 시도 과다 — 같은 IP 에서 1분에 **실패** 20회, 같은 주소로 15분에 **실패** 10회 (성공한 로그인은 세지 않는다 — 사무실처럼 여러 직원이 한 IP 를 쓰는 환경 때문). 이와 별도로 같은 IP 의 로그인 **요청**은 1분에 60회까지. "같은 IP" 는 IPv6 를 **/64 단위**로 묶는다(IPv4-mapped 주소는 IPv4 로). 사내 도메인이 아닌 주소는 IP 한도에만 센다. 비밀번호 변경(`PUT /api/auth/password`)의 지금 비밀번호 틀림도 같은 한도로 함께 센다 | 429 `REQ_001` |
|
||||
| 같은 주소의 로그인 · 비밀번호 변경이 아직 처리 중 (버튼을 두 번 누른 경우 등) | 429 `REQ_001` |
|
||||
| 대량 공격으로 서버가 기억하는 시도 기록이 상한에 닿았고 밀어낼 수 있는 기록이 없음 — 새 IP·주소의 로그인 (잠금 기록을 지키려고 막는 쪽을 택한다) | 429 `REQ_001` |
|
||||
| 메일 엔진 장애 | 503 `MAIL_001` |
|
||||
|
||||
@@ -76,6 +76,34 @@
|
||||
|
||||
항상 200 (`data: null`). 두 쿠키를 지우고 **서버의 세션을 폐기**한다.
|
||||
|
||||
### `PUT /api/auth/password` — 4단계
|
||||
|
||||
```jsonc
|
||||
// 요청 (로그인 필요)
|
||||
{ "currentPassword": "…", "newPassword": "…" }
|
||||
// 200 (data: null)
|
||||
```
|
||||
|
||||
| 실패 | 코드 |
|
||||
| --- | --- |
|
||||
| 지금 비밀번호가 틀림 | 400 `AUTH_005` — 세션은 그대로다. 401 이 아닌 까닭: 클라이언트는 401 이면 세션을 갱신하고 같은 요청을 다시 보낸다 |
|
||||
| 새 비밀번호가 8자 미만 · 128자 초과 · 지금 비밀번호와 같음, 엔진 규칙에 어긋남 · 지금 비밀번호가 비었거나 256자 초과(로그인과 같은 한도) — 길이는 JS 문자열 길이 | 400 `VAL_001` |
|
||||
| 지금 비밀번호 틀림이 잦음 — 로그인 실패와 **같은 한도**(같은 IP 1분 20회 · 같은 주소 15분 10회)를 함께 센다. 같은 주소의 로그인 · 변경이 처리 중 | 429 `REQ_001` |
|
||||
| 웹메일로 다루지 않는 계정(메일 엔진 관리자 · 게이트웨이 · 막은 계정 — 엔진에 묻지 않는다) | 401 `AUTH_001` |
|
||||
| 메일 엔진 장애 — 바꾸는 요청의 결과를 알 수 없는 실패(시간 초과 · 연결 끊김 · 5xx)도 | 503 `MAIL_001` — 결과를 모를 때는 바뀌었을 수 있으므로 **다른 세션을 끊어 둔다** |
|
||||
|
||||
- 서버는 메일 엔진에 **그 직원 본인의 자격으로**(지금 비밀번호) 본인 비밀번호 변경(Stalwart `x:AccountPassword/set` 에 `currentSecret` · `secret`)을 보낸다 —
|
||||
게이트웨이 대리 접근이나 관리자 권한을 쓰지 않는다. 엔진은 지금 비밀번호 없이는 바꾸지 않고 8자 미만을 거절한다 (2026-10-28 개발 엔진에서 확인).
|
||||
세션은 같은 값으로 열렸는데 엔진이 바꾸기를 거절하면(`forbidden` · `currentSecret` 의 `invalidProperties` · 바꾸기 요청의 401 — 앱 비밀번호를 넣은 경우 · 역할의 권한 설정)
|
||||
`AUTH_005` 로 답하되 실패 횟수로 세지 않고 세션도 끊지 않는다(추측이 아니라서). 결과를 알 수 없는 실패도 실패 횟수로 세지 않는다.
|
||||
- 바뀌면 그 직원의 **다른 로그인 세션을 모두 끊는다** — 이 요청의 리프레시 쿠키(Path=`/api/auth` 라 이 요청에도 온다)가 가리키는 세션만 남긴다.
|
||||
쿠키가 없거나 다른 직원 것이면 이 세션도 끊긴다(다음 갱신에서 로그인 화면으로). 끊긴 기기의 액세스 토큰은 만료(최대 1시간)까지 쓰인다(위 세션 규칙).
|
||||
휴대폰 · PC 메일 프로그램(IMAP)은 새 비밀번호를 다시 넣어야 한다.
|
||||
- 비밀번호는 바뀌었는데 세션 끊기가 실패하면 200 으로 답하고 오류 로그를 남긴다 — 실패로 답하면 사용자가 옛 비밀번호로 다시 시도하기 때문이다.
|
||||
- 비밀번호는 로그 · 응답에 남기지 않는다. 바꾼 일은 info 로그에 주소(가린 것)만 남긴다.
|
||||
- **클라이언트**: 같은 규칙(8자 이상 · 지금과 다름 · 확인 칸 일치)을 먼저 보고, `AUTH_005` 는 지금 비밀번호 칸 아래에 사전 문구로 보인다. 성공하면 세 칸을 비운다.
|
||||
요청 한도는 30초다 — 서버가 엔진을 두 번 부르고 세션을 끊으므로, 기본 한도(10초)로는 서버가 바꾼 뒤 화면만 실패로 볼 수 있다.
|
||||
|
||||
### 클라이언트 처리 규칙
|
||||
|
||||
- 보호 API 가 401 `AUTH_001` 을 주면 **한 번만** `/auth/refresh` 를 시도하고, 성공하면 원래 요청을 다시 보낸다.
|
||||
@@ -175,17 +203,25 @@ HTML 은 정화하기 **전에** 길이(HTML 부분이 여럿이면 이어 붙
|
||||
| 메서드 · 경로 | 요청 | 성공 | 주요 실패 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET /api/mailboxes` | — | 200 `{ items: Mailbox[], pageInfo }` (hasNext=false) | `MAIL_001` |
|
||||
| `POST /api/mailboxes` | `{ name, parentId? }` (아래 "메일함 관리") | 201 `Mailbox` | 400 `VAL_001`, 404 `RES_001`(없는 parentId), 409 `MAIL_009`(같은 자리에 같은 이름), 503 `MAIL_001` |
|
||||
| `PATCH /api/mailboxes/{id}` | `{ name }` | 200 `Mailbox` | 400 `VAL_001`(기본 메일함), 404 `RES_001`, 409 `MAIL_009`, 503 `MAIL_001` |
|
||||
| `DELETE /api/mailboxes/{id}` | — | 200 `MailboxDeletion` — 안의 메일을 휴지통으로 옮긴 뒤 지운다 | 400 `VAL_001`(기본 메일함 · 하위 메일함 있음), 404 `RES_001`, 429 `REQ_001`(같은 메일함의 처리가 진행 중), 503 `MAIL_001` |
|
||||
| `PATCH /api/mailboxes/{id}/messages` | `{ isRead: true }` — 그 메일함 전체를 읽음으로 | 200 `MailboxBulkResult` | 400 `VAL_001`, 404 `RES_001`, 429 `REQ_001`(같은 메일함의 처리가 진행 중), 503 `MAIL_001` |
|
||||
| `DELETE /api/mailboxes/{id}/messages` | — 휴지통 · 스팸메일함 비우기(영구 삭제) | 200 `MailboxBulkResult` | 400 `VAL_001`(휴지통 · 스팸메일함이 아님), 404 `RES_001`, 429 `REQ_001`(같은 메일함의 처리가 진행 중), 503 `MAIL_001` |
|
||||
| `GET /api/mail-vacation` | — | 200 `MailVacation` (아래 "부재중 자동 응답") | 503 `MAIL_001` |
|
||||
| `PUT /api/mail-vacation` | `MailVacation` (전체 교체) | 200 `MailVacation` | 400 `VAL_001`, 503 `MAIL_001` |
|
||||
| `GET /api/mail-quota` | — | 200 `MailQuota` | `MAIL_001` |
|
||||
| `GET /api/mail-state` | — | 200 `MailState` (아래 "새 메일 확인") | 503 `MAIL_001` |
|
||||
| `GET /api/messages` | `mailboxId` · `cursor` · `limit`(기본 20, 최대 100) · `q` — `mailboxId` 와 `q` 중 하나는 있어야 한다(`q` 만 있으면 휴지통·스팸메일함을 뺀 모든 메일함에서 찾는다 — 아래 "검색") | 200 `{ items: MessageSummary[], pageInfo }` — 받은 시각 내림차순, `total` 포함 | 400 `VAL_001`, `MAIL_001` |
|
||||
| `GET /api/messages/{id}` | — | 200 `MessageDetail` — **조회만으로 읽음 처리하지 않는다** | 404 `RES_001` |
|
||||
| `PATCH /api/messages/{id}` | `{ isRead?, isFlagged?, mailboxId? }` (하나 이상) | 200 `MessageSummary` | 404 `RES_001`, 400 `VAL_001` |
|
||||
| `PATCH /api/messages` | `{ ids, isRead?, isFlagged?, mailboxId? }` — 여러 통 (아래 "여러 통 한꺼번에") | 200 `MessagesPatched` | 400 `VAL_001`, 404 `RES_001`(없는 `mailboxId` — 아무것도 바꾸지 않음), 503 `MAIL_001` |
|
||||
| `PATCH /api/messages/{id}` | `{ isRead?, isFlagged?, mailboxId?, isJunk? }` (하나 이상 — `isJunk` 는 4단계 "스팸 · 원문") | 200 `MessageSummary` | 404 `RES_001`, 400 `VAL_001` |
|
||||
| `PATCH /api/messages` | `{ ids, isRead?, isFlagged?, mailboxId?, isJunk? }` — 여러 통 (아래 "여러 통 한꺼번에") | 200 `MessagesPatched` | 400 `VAL_001`, 404 `RES_001`(없는 `mailboxId` — 아무것도 바꾸지 않음), 503 `MAIL_001` |
|
||||
| `DELETE /api/messages/{id}` | — | 204 — 휴지통이 아니면 휴지통으로 이동, 휴지통에만 있으면 영구 삭제(확인한 상태 그대로일 때만 — 아래). 휴지통과 다른 메일함에 함께 있으면 휴지통에서만 뺀다. **웹 클라이언트는 쓰지 않는다**(3단계 — 아래 "여러 통 한꺼번에") | 404 `RES_001`, 503 `MAIL_001`(확인하는 사이 계속 바뀜 — 지우지 않았다) |
|
||||
| `POST /api/message-deletions` | `{ ids }` — 휴지통에서 영구 삭제, 여러 통 (아래 "여러 통 한꺼번에") | 200 `MessageDeletionResult` | 400 `VAL_001`, 503 `MAIL_001`(확인하는 사이 계속 바뀜 · 휴지통 없음 — 지우지 않았다) |
|
||||
| `POST /api/messages` | 아래 발송 본문 + `Idempotency-Key` 헤더 | 201 `{ id }` (보낸편지함의 메일 id) | 400 `MAIL_002`·`MAIL_003`·`MAIL_004`·`MAIL_006`·`MAIL_007`·`VAL_001`, 403 `AUTH_003`(보낼 권한 없음), 409 `MAIL_005`(같은 키·다른 내용), 413 `FILE_002`(첨부 합계 초과 포함), 429 `REQ_001`(엔진 발송 한도·같은 키 처리 중·엔진 장애 직후 30초), 503 `MAIL_001` |
|
||||
| `POST /api/mail-attachments` | multipart `file` 1개 | 201 `Attachment` | 400 `FILE_001`, 413 `FILE_002` |
|
||||
| `GET /api/messages/{id}/attachments/{attachmentId}` | — | 파일 (표준 래퍼 없음, `Content-Disposition: attachment`) | 404 `RES_001` |
|
||||
| `GET /api/messages/{id}/source` | — | 받은 그대로의 메일 `message/rfc822` (표준 래퍼 없음, `Content-Disposition: attachment; filename="<제목>.eml"`) — 4단계 | 404 `RES_001`, 503 `MAIL_001` |
|
||||
| `GET /api/drafts/{id}` | — | 200 `Draft` (아래 "임시 저장") | 404 `RES_001`(없음·초안이 아님), 503 `MAIL_001` |
|
||||
| `POST /api/drafts` | 초안 본문 | 201 `DraftSaved` | 400 `VAL_001`·`MAIL_002`·`MAIL_003`·`MAIL_004`·`MAIL_006`·`MAIL_007`·`FILE_001`, 413 `FILE_002`, 503 `MAIL_001` |
|
||||
| `PUT /api/drafts/{id}` | 초안 본문 (전체 교체) | 200 `DraftSaved` — **새 id** | 위와 같음 + 404 `RES_001`(초안이 아닌 id — 지우지 않음) + 409 `MAIL_008`(다른 곳에서 이미 바뀜 — 저장 안 함) |
|
||||
@@ -535,8 +571,81 @@ interface MessageDeletionResult {
|
||||
- `failedIds` · `skippedIds` 가 있으면 알림에 "N통은 그사이 옮겨졌거나 지워져 그대로 두었습니다"를 덧붙인다.
|
||||
- 메일함을 바꾸거나 검색하면 선택을 푼다. 새 메일 확인으로 목록을 맞출 때는 남아 있는 메일의 선택을 그대로 둔다.
|
||||
|
||||
### 메일함 관리 (`/api/mailboxes`) — 4단계
|
||||
|
||||
```ts
|
||||
interface MailboxCreate { name: string; parentId?: string | null } // parentId 없음 = 맨 위
|
||||
interface MailboxRename { name: string }
|
||||
interface MailboxDeletion {
|
||||
deleted: boolean // true = 지웠다. false = 아직 옮길 메일이 남았다 — 같은 요청을 다시 보낸다
|
||||
processed: number // 이번 요청에서 이 메일함에서 빠진 메일 수 — 휴지통으로 옮김 + 다른 메일함에도 있어 이 메일함에서만 뺌 (진행 표시용)
|
||||
movedToTrash: number // 그중 휴지통으로 옮긴 수 (끝난 알림의 "안의 메일 N통은 휴지통에 있습니다")
|
||||
remaining: number // 아직 남은 메일 수
|
||||
}
|
||||
interface MailboxBulkResult {
|
||||
processed: number // 이번 요청에서 읽음으로 바꾼 · 지운 메일 수
|
||||
remaining: number // 남은 수 — 0 이 될 때까지 같은 요청을 다시 보낸다
|
||||
}
|
||||
```
|
||||
|
||||
- **이름**: 앞뒤 공백을 뺀 1~64자(JS 문자열 길이), `/` 는 쓰지 않는다(경로 구분자로 읽는 메일 프로그램이 있다). 제어 문자도 쓰지 않는다 —
|
||||
C0(U+0000–U+001F, 탭 · 줄바꿈 포함) · DEL(U+007F) · C1(U+0080–U+009F). 그 밖의 문자(NBSP · 이모지 등)는 받는다.
|
||||
같은 상위 안에 같은 이름은 409 `MAIL_009` (엔진이 `alreadyExists` 로 거절한다 — 2026-10-28 확인). 지금 이름으로 바꾸면 엔진에 묻지 않고 그대로 돌려준다.
|
||||
만든 메일함은 구독 상태(`isSubscribed`)라 IMAP 메일 프로그램에도 보인다.
|
||||
- **만들 수 있는 자리**: 맨 위, 받은편지함 · 보관함 · 내 메일함(역할 없음) 아래. 보낸편지함 · 임시보관함 · 스팸메일함 · 휴지통 아래는 400 `VAL_001`.
|
||||
웹 화면은 받은편지함 · 내 메일함 아래만 메뉴에 둔다(보관함 아래는 다른 메일 프로그램이 만든 것을 그대로 보인다).
|
||||
- **기본 메일함**(역할이 있는 메일함)은 이름을 바꾸거나 지울 수 없다 — 400 `VAL_001`. 화면은 메뉴에 그 항목을 두지 않는다.
|
||||
- **지우기**: 하위 메일함이 있으면 400 `VAL_001`(화면이 먼저 막는다). 안의 메일은 **휴지통으로 옮긴 뒤**(다른 메일함에도 있는 메일은 이 메일함에서만 뺀다)
|
||||
빈 메일함을 지운다 — 엔진은 메일이 든 메일함을 `mailboxHasEmail` 로 거절하므로(`onDestroyRemoveEmails: false` — 메일을 영구 삭제하는 옵션은 쓰지 않는다) 그사이 들어온 메일도 남지 않는다.
|
||||
메일이 많으면 한 요청에 다 옮기지 못한다 — `deleted: false` 면 클라이언트가 같은 요청을 다시 보낸다.
|
||||
- **전체 읽음**(`PATCH /api/mailboxes/{id}/messages {isRead:true}`): 안 읽은 메일을 500통씩 읽음으로 바꾼다. **비우기**(`DELETE /api/mailboxes/{id}/messages`): 휴지통 · 스팸메일함만,
|
||||
500통씩 확인한 상태 그대로일 때만(`ifInState`) 영구 삭제한다 — 그 메일함에만 있는 메일은 지우고, 다른 메일함에도 있는 메일은 그 메일함에서만 뺀다(`POST /api/message-deletions` 와 같은 규칙).
|
||||
- 오래 걸리는 처리(전체 읽음 · 비우기 · 메일함 지우기)는 한 요청을 25초 안에 끝내고(엔진 호출마다 남은 시간만큼만 기다린다) `remaining` 을 돌려준다.
|
||||
클라이언트는 0(또는 `deleted: true`)이 될 때까지 다시 보내며 진행을 알림으로 보인다("…하는 중 · 600 / 1,234통"). 도중에 실패하면 거기까지 처리한 수를 알리고, 다시 하면 남은 것만 처리한다.
|
||||
- 같은 직원 · 같은 메일함의 처리는 한 번에 하나다 — 진행 중이면 엔진에 묻지 않고 429 `REQ_001`(다른 탭에서 같은 메일함을 처리할 때). 다른 메일함끼리는 함께 된다.
|
||||
- 한 요청의 상한 25초는 메일함 목록을 읽기 전부터 잰다(엔진의 세션 조회 포함). 다음 500통은 남은 시간이 5초보다 많을 때만 시작한다.
|
||||
- 멈추는 경우 — 진행한 것이 있으면 거기까지 200(`remaining` > 0 · `deleted: false`), 하나도 처리하지 못했으면 503 `MAIL_001`(클라이언트가 끝없이 다시 보내지 않게):
|
||||
아무것도 바뀌지 않는 차례(확인하는 사이 계속 바뀜 · 바뀐 것도 거절도 없음 · 비운 메일함을 지우는 사이 메일이 들어옴)가 이어서 3번, 남은 시간 때문에 엔진 호출이 끊김.
|
||||
엔진이 한 차례를 모두 거절하면 진행한 것이 없을 때 그 거절의 코드(권한 없음 403 `AUTH_003` 등)다.
|
||||
- **비우기는 차례마다 그 시점의 메일함을 다시 읽는다** — 비우는 동안 들어온 메일(새 스팸, 다른 탭에서 휴지통으로 옮긴 메일)도 지운다(흔한 메일 프로그램과 같다).
|
||||
그래서 화면은 휴지통 비우기와 메일함 지우기(안의 메일이 휴지통으로 간다)를 함께 돌리지 않는다.
|
||||
- **클라이언트**: 메일함 패널의 각 메일함 ⋯ 메뉴 — 모두 읽음으로 표시(안 읽은 메일이 있을 때) · 하위 메일함 만들기 · 이름 바꾸기 · 지우기(내 메일함만) · 휴지통 / 스팸메일함 비우기.
|
||||
휴지통 · 스팸메일함 목록 머리에도 「비우기」. 비우기 · 지우기는 확인 대화상자(처음 초점 「취소」) 뒤에만 하고, 확인한 그 메일함만 처리한다.
|
||||
다른 탭에서 먼저 끝냈으면 실패로 보이지 않는다 — 모두 읽음 · 비우기의 0통은 "…에 안 읽은 메일이 없습니다" · "…이 이미 비어 있습니다", 지우기의 404 `RES_001` 은 지운 것으로 본다.
|
||||
메일함 지우기의 진행은 `processed` 로 센다(`movedToTrash` 는 휴지통으로 간 것만이라 다른 메일함에도 든 메일만 남으면 늘지 않는다).
|
||||
|
||||
### 부재중 자동 응답 (`/api/mail-vacation`) — 4단계
|
||||
|
||||
```ts
|
||||
interface MailVacation {
|
||||
isEnabled: boolean
|
||||
fromDate: string | null // ISO 8601 UTC — null 이면 켠 때부터
|
||||
toDate: string | null // null 이면 끌 때까지
|
||||
subject: string // 200자까지 — 켤 때(isEnabled true)는 공백이 아닌 글자 1자 이상. 제어 문자(메일함 이름과 같은 범위 — 탭 · 줄바꿈 포함)는 쓰지 않는다(한 줄짜리 머리글)
|
||||
textBody: string // 4,000자까지 — 켤 때는 공백이 아닌 글자 1자 이상. 글자만(서식 없음) — 제어 문자는 탭 · LF · CR 만. 서명은 붙지 않는다
|
||||
}
|
||||
```
|
||||
|
||||
- 한 번도 설정하지 않은 계정은 `{ isEnabled: false, fromDate: null, toDate: null, subject: "", textBody: "" }` 로 온다(엔진의 null 은 빈 문자열로). 끈 채로는 빈 제목 · 내용도 저장된다.
|
||||
제목 · 내용은 쓴 그대로 저장한다 — "공백이 아닌 글자" 검사만 앞뒤 공백을 뺀 값(JS `trim()` — NBSP · U+3000 포함)으로 한다. 제어 문자 검사는 꺼 둔 채로도 한다.
|
||||
|
||||
- 엔진의 JMAP `VacationResponse`(singleton)를 그대로 쓴다 — `htmlBody` 는 비운다(null). 저장은 전체 교체다. 꺼도 쓴 제목 · 내용은 남겨 둔다.
|
||||
- `fromDate` · `toDate` 는 `Z` 로 끝나는 UTC 시각이고, 두 키는 늘 보낸다(값은 null 가능). 엔진에는 초 단위로 저장한다(소수 초는 버린다).
|
||||
둘 다 있으면 버린 값으로 `fromDate < toDate`(아니면 400 `VAL_001`). 화면은 날짜만 고르고, 브라우저 시간대의 시작일 0시 · 끝나는 날 다음 날 0시를 UTC 로 보낸다.
|
||||
- 같은 사람에게 매번 답하지는 않는다 — JMAP 규격(RFC 8621 8절)이 "보낸 사람마다 적당한 기간에 한 번" 을 요구하고 엔진이 따른다.
|
||||
- **클라이언트**: 설정 › 메일 › 부재중 자동 응답. 켜져 있으면 메일 화면의 메일함 패널 아래(사용량 위)에 "부재중 자동 응답 켜짐 · 11월 3일까지"(누르면 설정)를 보인다 —
|
||||
메일 화면에 들어올 때 한 번 받고, 시간이 지나면 다시 계산한다(끝나는 시각이 지나면 숨기고, 시작 전이면 "10월 30일 ~ 11월 3일 · 설정").
|
||||
|
||||
### 스팸 · 원문 — 4단계
|
||||
|
||||
- **스팸 신고 · 스팸 아님**: `PATCH /api/messages/{id}` · `PATCH /api/messages` 의 `isJunk` — `true` 면 키워드 `$junk` 를 붙이고 `$notjunk` 를 뗀다, `false` 면 반대(RFC 8621 4.1.1).
|
||||
화면은 이동과 함께 보낸다 — 스팸 신고 `{ mailboxId: <스팸메일함>, isJunk: true }`, 스팸 아님 `{ mailboxId: <받은편지함>, isJunk: false }`. 엔진의 스팸 학습에 쓰이는지는 엔진 몫이다.
|
||||
「되돌리기」는 원래 메일함으로 옮기며 보낸 값의 반대를 보낸다 — 표시를 지우는 값은 두지 않아서, 원래 아무 표시도 없던 메일도 되돌린 뒤에는 `$notjunk`(스팸 아님)가 남는다.
|
||||
- **원문 내려받기**: `GET /api/messages/{id}/source` — 엔진의 원본(blob)을 그대로 흘려보낸다(`message/rfc822`, `Cache-Control: private, no-store`). 파일 이름은 제목(없으면 "메일")에
|
||||
`.eml` — 다운로드 헤더는 첨부와 같은 규칙(`shared/utils/content-disposition.ts`). 화면은 첨부 내려받기처럼 이동 전에 세션을 확인한다(`confirmSession()`).
|
||||
|
||||
### 이 단계에서 하지 않는 것
|
||||
|
||||
실시간 푸시(SSE · 데스크톱 알림 · 소리), 본문 안 이미지(인라인 cid — 붙여 넣은 이미지는 첨부로, 서명 그림만 예외), 외부 이미지 보기(이미지 중계),
|
||||
여러 개의 서명, 엔진 계정 목록 기반의 사내 주소록, 자체 검색 색인(붙여 쓴 말 · 앞부분 · 제목 말머리로 찾기), 조건 검색(보낸 사람 · 기간 · 첨부 있음), 검색어 강조,
|
||||
불러오지 않은 메일까지 한꺼번에(메일함 전체 읽음 표시 · 휴지통 비우기), 끌어다 놓아 옮기기.
|
||||
끌어다 놓아 옮기기, 메일함 옮기기(다른 상위로) · 하위 메일함까지 한꺼번에 지우기, 앱 비밀번호 · 2단계 인증(엔진은 지원한다), 부재중 응답의 서식 본문 · 사내/외부 구분, 인쇄.
|
||||
|
||||
Reference in New Issue
Block a user