diff --git a/docs/mail/api-contract.md b/docs/mail/api-contract.md index fad4e35..2880542 100644 --- a/docs/mail/api-contract.md +++ b/docs/mail/api-contract.md @@ -397,7 +397,7 @@ interface RejectedRecipient { (조건을 붙이면 엔진이 그 한도를 연결이 아닌 MAIL FROM 단계로 옮겨 바깥 접속을 더는 세지 않는다 — Stalwart 0.16.27 소스로 확인). - **클라이언트** - 일부 거절 — 보낸 것으로 처리한다(쓰기 화면을 닫고 떠난다). 보냈다는 알림 대신 실패 알림(「보낸편지함 보기」 링크 — 보냈다는 알림과 같은 글, 단추 있는 알림과 같은 10초)으로 - "보냈지만 N명은 받지 못했습니다 — nobody@arasystem.it.kr (없는 주소)" — 두 명까지 주소와 까닭을 적고 더 있으면 " 외 N명". + "보냈지만 N명은 받지 못했습니다. nobody@arasystem.it.kr (없는 주소)" — 두 명까지 주소와 까닭을 적고 더 있으면 " 외 N명". 까닭 문구는 `not-found` "없는 주소" · `refused` "받는 쪽이 거절" · `temporary` "지금은 받을 수 없음". - `MAIL_010` — 받는 사람 칸의 안내 띠(`MAIL_002` 와 같은 자리 · 같은 초점)로만 알린다(토스트 없음). 쓰던 메일 · 키는 그대로. - `MAIL_011` — 인터셉터 알림(사전 문구). 쓰던 메일 · 키는 그대로 — 잠시 뒤 다시 누르면 같은 키로 간다. @@ -764,7 +764,7 @@ interface MailVacationState extends MailVacation { // GET · PUT 의 응답 (7 필터와 한 스크립트라서 부재중을 저장해도 스크립트를 다시 켠다(필터가 꺼져 있었으면 함께 다시 동작한다). 필터 규칙은 건드리지 않는다. - 6단계부터 바뀐 동작: 엔진이 스팸으로 분류한 메일에는 답하지 않는다(필터와 같은 스팸 막기 — "메일 필터"의 "만드는 Sieve"). - 우리 스크립트가 엔진에서 꺼져 있으면(`isActive: false`) 부재중도 동작하지 않는다 — `isEnabled` 는 저장한 값이라 그때도 `true` 다. - 7단계부터 응답의 `isActive` 로 알린다 — 화면은 켜 두었을 때 머리 아래 줄 "켜 둔 자동 응답이 지금은 동작하지 않습니다. 메일 서버의 규칙이 다른 곳에서 바뀌었습니다 — + 7단계부터 응답의 `isActive` 로 알린다 — 화면은 켜 두었을 때 머리 아래 줄 "다른 곳에서 메일 설정이 바뀌어 켜 둔 자동 응답이 지금은 나가지 않습니다. 아래 [저장]을 누르면 다시 켜집니다."(단추 없음 — 저장 단추는 늘 누를 수 있다. 6단계의 알려진 한계를 고쳤다). - 저장해 둔 원본이 깨진 계정은 꺼짐(`isEnabled: false`, 빈 제목 · 내용)으로 온다. 원본이 이 서버보다 새 판이면 저장은 503 `MAIL_001` 이다("메일 필터"의 "저장 자리"). - `fromDate` · `toDate` 는 `Z` 로 끝나는 UTC 시각이고, 두 키는 늘 보낸다(값은 null 가능). 엔진에는 초 단위로 저장한다(소수 초는 버린다). @@ -941,7 +941,7 @@ interface MailFilterActions { - `MAIL_012` — 목록 위 줄 "다른 곳에서 필터가 바뀌어 저장하지 않았습니다. 다시 불러온 뒤 고쳐 주세요." + [다시 불러오기]. 고치던 창은 닫지 않는다 — 창 안에도 같은 안내와 [다시 불러오기]를 둔다(모달이라 뒤의 줄을 누를 수 없다). 창에서 다시 불러오면 쓴 내용은 그대로이고, 다음 [저장]은 다시 받은 목록에 그 필터를 넣는다(같은 id 자리 — 그사이 다른 곳에서 지워졌으면 맨 아래). - - `isActive: false` — 목록 위 줄 "지금은 필터와 부재중 자동 응답이 동작하지 않습니다…" + [다시 켜기](지금 목록 그대로 저장). + - `isActive: false` — 목록 위 줄 "다른 곳에서 메일 설정이 바뀌어 지금은 필터와 부재중 자동 응답이 되지 않습니다." + [다시 켜기](지금 목록 그대로 저장). - 읽기 화면 ⋯ 「이런 메일로 필터 만들기」 → `/settings/mail/filters?from=<보낸 사람 주소>` — 새 필터 창이 "보낸 사람 · 포함 · 그 주소"로 채워져 열린다(초점은 할 일의 첫 칸). 50개를 다 만들었는지 알려고 ⋯ 메뉴를 열 때 `GET` 을 한다(받아 둔 목록이 없거나 50개일 때만 — 알림 없이). - 검사는 서버와 같은 규칙(이름 50 · 값 200 · 조건 5 · 규칙 50 · `${` · 제어 문자 · 할 일 하나 이상 · 전달은 사내 주소이고 자기 주소 아님) — 칸 아래에만 알린다. @@ -1017,11 +1017,12 @@ interface MailFilterRun { (응답 목록의 같은 id — 앞뒤 공백을 지운 값)으로 이 요청을 `cursor` 가 null 이 될 때까지 보낸다. - 칸을 끈다(칸 아래 설명이 까닭을 말한다): 이 탭에서 다른 적용이 진행 중, 그리고 **이미 받은 메일에 할 일이 없는 필터** — 읽음 · 중요 표시가 없고 옮기기도 없거나 받은편지함으로 · 지워진 메일함으로 옮기는 것뿐(까닭 문구는 지워진 메일함 → 받은편지함 → 전달만 차례로 고른다). 서버는 그런 규칙도 받는다(맞은 수만 센다). - - 진행 알림 "‘{이름}’ 필터를 받은편지함에 적용하는 중 · {확인} / {전체}통 확인"(확인 = scanned 합, 전체 = 확인 + remaining) → 끝나면 그 자리에서 - "‘{이름}’ 필터를 받은편지함의 메일 N통에 적용했습니다."(N = matched 합) + 옮기는 필터이고 1통 이상이면 [{메일함} 보기], 0통이면 "받은편지함에 ‘{이름}’ 필터에 맞는 메일이 없습니다." - - 멈춤: "‘{이름}’ 필터 — {전체}통 가운데 {확인}통까지 확인하고 멈췄습니다(N통에 적용). 다시 시도해 주세요." + [다시 시도](마지막 cursor 로 이어서). - 처음 요청부터 실패하면 "필터는 저장했지만 받은편지함에 적용하지 못했습니다. 다시 시도해 주세요." + [다시 시도](처음부터). - 429 는 "다른 창에서 필터를 받은편지함에 적용하는 중입니다. 끝난 뒤 다시 시도해 주세요." — 도중의 429 면 [다시 시도](이어서)도 붙인다. + - 진행 알림 "‘{이름}’ 필터로 받은편지함을 정리하는 중 · {확인} / {전체}통 확인"(확인 = scanned 합, 전체 = 확인 + remaining) → 끝나면 그 자리에서 + "‘{이름}’ 필터로 받은편지함의 메일 N통을 정리했습니다."(N = matched 합) + 옮기는 필터이고 1통 이상이면 [{메일함} 보기], 0통이면 "받은편지함에 ‘{이름}’ 필터에 맞는 메일이 없습니다." + - 멈춤: "‘{이름}’ 필터로 {전체}통 가운데 {확인}통까지 확인하고 멈췄습니다. 그동안 N통을 정리했습니다. 다시 시도해 주세요." + [다시 시도](마지막 cursor 로 이어서). + 처음 요청부터 실패하면 "필터는 저장했지만 받은편지함은 정리하지 못했습니다. 다시 시도해 주세요." + [다시 시도](처음부터). + 429 는 "다른 창에서 필터로 받은편지함을 정리하는 중입니다. 끝난 뒤 다시 시도해 주세요." — 도중의 429 면 [다시 시도](이어서)도 붙인다. + (문구는 2026-11-05 「화면 문구 — 사용자에게 말하듯」으로 고쳤다 — 화면 상수 `MAIL_FILTER_RUN_TEXT` 가 원본.) 확인 0 인데 `cursor` 가 있는 응답이 세 번 이어지면 멈춘다(같은 요청을 끝없이 보내지 않게 — 멈춤 문구와 같다). - 그동안 다른 화면으로 가도 이어 한다. 탭을 닫으면 멈춘다(이어 하지 않는다). 세션이 바뀌면(로그아웃 · 만료 · 다른 계정) 다음 요청을 보내지 않고 알림을 닫는다. - **고치기 창의 나가기 확인**(6단계 한계를 고친다): 창을 연 뒤 바꾼 것이 있으면 [취소] · Esc · 다른 화면으로 가기에 확인 — "저장하지 않은 필터가 있습니다" / @@ -1636,7 +1637,7 @@ interface FileUploadCreate { - 검사 상태 다시 묻기: 쓰기 화면 · 읽기 화면은 15 · 30 · 60초 간격 최대 30번, 공개 화면은 20번(공개 경로의 IP 한도 안). - 진행은 받은 바이트로 보인다("420MB / 1.2GB · 약 3분"), 멈춤 · 빼기. 빼면 `DELETE`(알림 없이 — 404 는 지운 것으로 본다). 쓰기 화면을 버리면(휴지통) 지우지 않는다 — 되돌리면 다시 쓴다. - **새로고침 · 창 닫힘 뒤 이어 올리기(처음 판에 넣는다)**: 올리는 중인 파일도 임시 저장본 머리글에 실린다(아래 "임시 저장") — 다시 열면 `uploading` 으로 오고 - "올리다 멈춘 파일 — 같은 파일을 다시 고르면 이어서 올립니다"(브라우저는 고른 파일을 스스로 다시 읽지 못한다). + "올리다 멈춤 · {N} 올림 · 같은 파일을 다시 고르면 이어서 올립니다"(브라우저는 고른 파일을 스스로 다시 읽지 못한다). 다시 고른 파일은 **이름 · 크기 · `lastModified` 가 같고 그 파일의 맨 앞 조각 SHA-256 이 `firstChunkSha256` 과 같을 때** 같은 파일로 보고 빠진 조각부터 잇는다. 다르면 새로 만든다(앞 올리기는 `DELETE`). 맨 앞 조각을 받기 전(`firstChunkSha256: null`)이면 이을 것이 없다 — 새로 만든다. 올리는 중은 마지막 조각 뒤 24시간 남는다 — 그 뒤에 열면 `MissingLargeFile`(`reason: 'expired'`)이라 다시 올린다.