From 5ddc849c11de3768504a936dcb540e6cb0e0cb87 Mon Sep 17 00:00:00 2001
From: ttipo
Date: Thu, 29 Oct 2026 08:03:42 +0900
Subject: [PATCH] =?UTF-8?q?docs:=207=EB=8B=A8=EA=B3=84(=EC=9D=B4=EB=AF=B8?=
=?UTF-8?q?=20=EB=B0=9B=EC=9D=80=20=EB=A9=94=EC=9D=BC=EC=97=90=20=ED=95=84?=
=?UTF-8?q?=ED=84=B0=20=EC=A0=81=EC=9A=A9)=20=EA=B3=84=EC=95=BD=EA=B3=BC?=
=?UTF-8?q?=20=EB=94=94=EC=9E=90=EC=9D=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
필터를 저장할 때 받은편지함에 이미 있는 메일에도 그 필터 하나를 적용한다. 엔진의 Sieve 는 받을 때만
돌고 엔진 검색은 낱말 단위라, 서버가 받은편지함을 읽어 Sieve 와 같은 규칙으로 판정한다.
- 계약 2절 "이미 받은 메일에 적용"(POST /api/mail-filter-runs): 판정 규칙은 실엔진 배달로 맞췄다 —
HTML 은 닫는 p · br 만 줄바꿈, style · script · head · template 은 글째 빼고, 속성이 붙은 태그는
엔진처럼 알아보지 않는다. 머리글은 접힌 줄을 띄어쓰기 하나로, 인코딩된 글은 홀로 선 것만 푼다
- 시작한 때 받은편지함에 있던 메일만 보고(위 경계), 마지막으로 남긴 메일이 빠지면 그 초부터 다시 읽는다.
본문 값은 한 통 1MiB 까지 — 바깥 메일 한 통으로 서버가 메모리를 크게 쓰지 않게
- 부재중 응답에 isActive(엔진에서 우리 규칙이 꺼졌는지), 엔진의 임시 업로드 한도, 바깥 주소 자동 전달은
허용하지 않는다는 정책(2026-10-29 결정)
- 디자인 캔버스 확정본 MailFilterApply · MailFilterApplyStates 와 README "7단계"(구현하며 정한 보완 포함)
Co-Authored-By: Claude Opus 5.5
---
docs/design/webmail/MailFilterApply.dc.html | 561 ++++++++++++++++++
.../webmail/MailFilterApplyStates.dc.html | 178 ++++++
docs/design/webmail/README.md | 57 +-
docs/design/webmail/canvas.json | 9 +-
docs/mail/api-contract.md | 102 +++-
5 files changed, 896 insertions(+), 11 deletions(-)
create mode 100644 docs/design/webmail/MailFilterApply.dc.html
create mode 100644 docs/design/webmail/MailFilterApplyStates.dc.html
diff --git a/docs/design/webmail/MailFilterApply.dc.html b/docs/design/webmail/MailFilterApply.dc.html
new file mode 100644
index 0000000..4b34442
--- /dev/null
+++ b/docs/design/webmail/MailFilterApply.dc.html
@@ -0,0 +1,561 @@
+
+
+
+
+설정 · 필터 · 이미 받은 메일에 적용
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 설정
+
+
+
+
+
+
+
+
+
+
+ 계정
+
+ 메일
+
+
+
+
+
+
+
+
+
필터
+
받은 메일을 조건에 따라 메일함으로 옮기거나, 읽음 · 중요로 표시하거나, 동료에게 전달합니다. 새로 오는 메일에 적용하고, 고를 때는 받은편지함에 이미 있는 메일에도 적용합니다.
+
+
+
+ 새 필터
+
+
+
+
+
+
+
+ {{ r.no }}
+
+
+
+
+ {{ r.title }}
+ {{ r.sub }}
+
+
+
+
+
+
필터 {{ count }}개 · 50개까지 만들 수 있습니다. 행을 누르면 고칩니다.
+
+
+
+
· 위에서부터 차례로 적용합니다. 메일함으로 옮기는 필터에 맞으면 그 아래 필터는 적용하지 않습니다 — 순서는 ▲ ▼ 로 바꿉니다.
+
· 이미 받은 메일에도 적용하려면 필터를 저장할 때 「받은편지함에 이미 있는 메일에도 적용」 을 고릅니다. 받은편지함의 메일만 보고, 그 필터 하나만 적용합니다.
+
· 스팸으로 판정되어 들어오는 메일에는 필터를 적용하지 않습니다 — 그대로 스팸메일함으로 갑니다.
+
· 전달은 사내 주소로만, 새로 오는 메일만 합니다. 메일 한 통은 한 곳으로만 전달됩니다(위의 필터가 먼저). 전달해도 내 메일함에 그대로 남습니다.
+
· 부재중 자동 응답은 필터와 함께 동작합니다.
+
+
+
+
+
+
+
+
+
+
+
{{ dialogTitle }}
+
+
+ 이름 (선택)
+
+
+
+
+ 어떤 메일에
+
+ 다음 조건이 모두 맞는 메일
+ 다음 조건 가운데 하나라도 맞는 메일
+ 모든 메일
+
+
+
+
+
+
+ 조건 더하기
+
+ 5개까지 · 글자는 대소문자를 가리지 않습니다
+
+
+
+ 비어 있는 조건이 있습니다. 찾을 글을 적거나 그 조건을 지워 주세요.
+
+
+
+
+ 할 일
+
+
+
+ 메일함으로 옮기기
+
+
+ 받은편지함
+ 거래처
+ 전체 공지
+ 프로젝트
+ 스팸메일함
+ 휴지통
+
+
+ 옮기면 이 아래 필터는 적용하지 않습니다.
+
+
+ 읽음으로 표시
+
+
+
+ 중요 표시
+
+
+
+
+ 동료에게 전달
+
+
+
+ {{ fwdHelp }}
+
+ 할 일을 하나 이상 골라 주세요.
+
+
+
+
+
+
+ 받은편지함에 이미 있는 메일에도 적용
+
+
{{ applyHelp }}
+
+
스팸으로 판정되어 들어오는 메일에는 적용하지 않습니다.
+
+
+
+
+
+ 이 필터 지우기
+
+
+
+ 저장
+ 취소
+
+
+
+
+
+
+
+
+
+
저장하지 않은 필터가 있습니다
+
창을 닫으면 고친 내용이 사라집니다.
+
+ 계속 고치기
+ 닫기
+
+
+
+
+
+
+
+
+
{{ runText }} · {{ runCount }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/design/webmail/MailFilterApplyStates.dc.html b/docs/design/webmail/MailFilterApplyStates.dc.html
new file mode 100644
index 0000000..8416d80
--- /dev/null
+++ b/docs/design/webmail/MailFilterApplyStates.dc.html
@@ -0,0 +1,178 @@
+
+
+
+
+이미 받은 메일에 필터 적용 · 상태
+
+
+
+
+
+
+
+
+
+
+
이미 받은 메일에 필터 적용
+
7단계에서 새로 생기는 것입니다. 「설정 · 필터 · 이미 받은 메일에 적용」 보드에서 필터를 열고 「받은편지함에 이미 있는 메일에도 적용」을 골라 저장해 볼 수 있습니다(오른쪽 위 Tweaks 에서 결과를 바꿉니다). 이 보드는 그 다른 상태와 함께 고치는 두 가지 — 고치기 창의 나가기 확인, 부재중 화면의 안내 — 를 모았습니다.
+
+
+
+
+
+
+
고치기 창 — 「받은편지함에 이미 있는 메일에도 적용」
+
할 일 아래 · 창을 열 때마다 꺼져 있다 · 저장할 때 한 번 쓰고 기억하지 않는다(필터의 설정이 아니다)
+
+
+
+
기본
+
+
+
+ 받은편지함에 이미 있는 메일에도 적용
+
+
저장한 뒤 받은편지함에서 조건에 맞는 메일을 찾아 바로 적용합니다.
+
+
스팸으로 판정되어 들어오는 메일에는 적용하지 않습니다.
+
+
+
+
고름 — 전달도 하는 필터
+
+
+
+ 받은편지함에 이미 있는 메일에도 적용
+
+
저장한 뒤 받은편지함에서 조건에 맞는 메일을 찾아 바로 적용합니다. 전달은 새로 오는 메일에만 합니다.
+
+
+
+
+
고를 수 없음 — 전달만 하는 필터
+
+
+
+ 받은편지함에 이미 있는 메일에도 적용
+
+
전달만 하는 필터는 이미 받은 메일에 할 일이 없습니다 — 전달은 새로 오는 메일에만 합니다.
+
+
+
+
+
고를 수 없음 — 다른 필터를 적용하는 중
+
+
+
+ 받은편지함에 이미 있는 메일에도 적용
+
+
다른 필터를 받은편지함에 적용하는 중입니다. 끝난 뒤에 고를 수 있습니다.
+
+
+
+
+
+
+
저장한 뒤 — 진행 · 결과 알림
+
오른쪽 아래 · 끝날 때까지 남는다 · 그동안 다른 화면으로 가서 메일을 읽고 써도 된다(탭을 닫으면 멈춘다)
+
+
+
+
‘거래처 메일’ 필터를 받은편지함에 적용하는 중 · 4,200 / 8,412통 확인
+
+
+
+
‘거래처 메일’ 필터를 받은편지함의 메일 37통에 적용했습니다.
+
거래처 보기
+
+
+
+
‘결재 알림’ 필터를 받은편지함의 메일 12통에 적용했습니다.
+
+
+
+
받은편지함에 ‘거래처 메일’ 필터에 맞는 메일이 없습니다.
+
+ 숫자는 확인한 메일 / 받은편지함의 메일 수다. 받은편지함의 최근 메일부터 본다. 옮기는 필터의 결과에는 그 메일함으로 가는 단추를 둔다 — 읽음 · 중요만 하는 필터는 단추가 없다.
+
+
+
+
+
+
+
+
+
멈췄을 때
+
서버 · 연결 문제 — [다시 시도]는 남은 메일부터 이어서 본다
+
+
+
+ ‘거래처 메일’ 필터 — 8,412통 가운데 4,200통까지 확인하고 멈췄습니다(12통에 적용). 다시 시도해 주세요.
+ 다시 시도
+
+
+
+ 필터는 저장했지만 받은편지함에 적용하지 못했습니다. 다시 시도해 주세요.
+ 다시 시도
+
+
+
+ 다른 창에서 필터를 받은편지함에 적용하는 중입니다. 끝난 뒤 다시 시도해 주세요.
+
+ 처음 요청부터 실패해도 필터는 저장되어 있다 — 새로 오는 메일에는 적용된다. 로그아웃 · 세션 만료 · 다른 계정 로그인이면 멈추고 알림을 닫는다.
+
+
+
+
+
저장하지 않고 닫을 때
+
고치기 창의 [취소] · Esc · 다른 화면으로 가기 — 창을 연 뒤 바꾼 것이 있을 때만 묻는다
+
+
+
+
저장하지 않은 필터가 있습니다
+
창을 닫으면 고친 내용이 사라집니다.
+
+ 계속 고치기
+ 닫기
+
+
+
+ 처음 초점은 [계속 고치기] · Esc 도 같다(서명 · 부재중 설정과 같은 모양). 다른 화면으로 가려다 물으면 "이 화면을 나가면…" · [나가기]. 탭을 닫거나 새로 고치면 브라우저가 묻는다. 읽기 화면에서 채워 연 새 필터는 채운 그대로면 묻지 않는다.
+
+
+
+
+
부재중 자동 응답 — 메일 서버에서 규칙이 꺼졌을 때
+
자동 응답을 켜 두었는데 메일 서버의 규칙이 이 화면 밖에서 바뀐 드문 경우 — 머리 아래 줄 (필터 화면의 줄과 같은 뜻)
+
+
+
+ 켜 둔 자동 응답이 지금은 동작하지 않습니다. 메일 서버의 규칙이 다른 곳에서 바뀌었습니다 — 아래 [저장]을 누르면 다시 켜집니다.
+
+ 자동 응답을 꺼 두었으면 보이지 않는다. 저장하면 필터도 함께 다시 켜진다(한 규칙에 들어 있다). 지금까지는 이 화면이 이 상태를 몰라 "켜짐"으로만 보였다.
+
+
+
+
+
+
+
+
diff --git a/docs/design/webmail/README.md b/docs/design/webmail/README.md
index 27c00f8..0028889 100644
--- a/docs/design/webmail/README.md
+++ b/docs/design/webmail/README.md
@@ -372,6 +372,43 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
- 읽기 화면 ⋯ 메뉴에 「이런 메일로 필터 만들기」(원문 내려받기 아래) — 설정 › 필터의 새 필터 창이 보낸 사람의 **주소**로 채워져 열린다.
- 켜기 · 끄기 · 순서 바꾸기는 알리지 않는다(목록이 바로 바뀐다). 저장 "필터를 저장했습니다.", 지우기 "‘거래처 메일’ 필터를 지웠습니다. [되돌리기]".
+## 7단계 — 이미 받은 메일에 필터 적용 (2026-10-29 확정 — `MailFilterApply` · `MailFilterApplyStates`)
+
+필터를 저장할 때 받은편지함에 이미 있는 메일에도 그 필터 하나를 적용한다. 6단계의 두 한계(고치기 창의 나가기 확인 없음 · 부재중 화면이 꺼진 규칙을 모름)도 함께 고친다.
+규칙은 계약 2절 "이미 받은 메일에 적용".
+
+### 고치기 창 — 「받은편지함에 이미 있는 메일에도 적용」
+
+- 할 일 아래, 6단계의 안내 띠 자리에 회색 상자 하나: 체크 칸(이름 굵게) + 칸 아래 설명(12.5). 상자 아래 한 줄 "스팸으로 판정되어 들어오는 메일에는 적용하지 않습니다."
+- 창을 열 때마다 꺼져 있다 — 필터의 설정이 아니라 이번 저장에 한 번 쓴다(읽기 화면에서 만드는 새 필터도 같다). 목록의 행에는 단추를 더하지 않는다 — 있는 필터는 창을 열어 고르고 저장한다.
+- 설명은 "저장한 뒤 받은편지함에서 조건에 맞는 메일을 찾아 바로 적용합니다." — 전달이 있는 필터면 " 전달은 새로 오는 메일에만 합니다."를 더한다.
+- 고를 수 없는 두 경우(칸을 끄고 이름을 흐리게, 설명이 까닭):
+ 전달만 하는 필터 "전달만 하는 필터는 이미 받은 메일에 할 일이 없습니다 — 전달은 새로 오는 메일에만 합니다." ·
+ 이 탭에서 다른 필터를 적용하는 중 "다른 필터를 받은편지함에 적용하는 중입니다. 끝난 뒤에 고를 수 있습니다."
+- 목록 머리의 설명과 안내 상자의 "새로 오는 메일에만"을 바꾼다(`MailFilterApply` 보드의 글 — 이미 받은 메일 줄을 더하고, 전달 줄에 "새로 오는 메일만").
+
+### 저장한 뒤 — 진행 · 결과 알림
+
+- "필터를 저장했습니다." 대신 4단계의 진행 알림: "‘거래처 메일’ 필터를 받은편지함에 적용하는 중 · 4,200 / 8,412통 확인"(확인한 수 / 받은편지함 메일 수 — 최근 메일부터).
+ 끝나면 그 자리에서 "‘거래처 메일’ 필터를 받은편지함의 메일 37통에 적용했습니다." + 옮기는 필터면 [거래처 보기](그 메일함으로) —
+ 읽음 · 중요만 하는 필터는 단추 없음, 맞는 메일이 없으면 "받은편지함에 ‘거래처 메일’ 필터에 맞는 메일이 없습니다."
+- 멈추면 "‘거래처 메일’ 필터 — 8,412통 가운데 4,200통까지 확인하고 멈췄습니다(12통에 적용). 다시 시도해 주세요." + [다시 시도](남은 메일부터 이어서).
+ 처음 요청부터 실패하면 "필터는 저장했지만 받은편지함에 적용하지 못했습니다. 다시 시도해 주세요." + [다시 시도]. 다른 창 · PC 에서 적용 중이면 "다른 창에서 필터를 받은편지함에 적용하는 중입니다. 끝난 뒤 다시 시도해 주세요."
+- 그동안 다른 화면으로 가서 메일을 읽고 써도 이어 한다. 탭을 닫으면 멈춘다(이어 하지 않는다). 세션이 바뀌면 멈추고 알림을 닫는다.
+
+### 저장하지 않고 닫을 때 (6단계 한계를 고친다)
+
+- 창을 연 뒤 바꾼 것이 있으면 [취소] · Esc · 다른 화면으로 가기에 확인 — 서명 · 부재중 설정의 나가기 확인과 같은 모양:
+ "저장하지 않은 필터가 있습니다" / "창을 닫으면 고친 내용이 사라집니다." [계속 고치기](채운 단추 · 처음 초점 · Esc) [닫기].
+ 다른 화면으로 가려다 물으면 "이 화면을 나가면 고친 내용이 사라집니다." [나가기]. 탭 닫기 · 새로고침은 브라우저 확인창.
+- 읽기 화면에서 채워 연 새 필터는 채운 그대로면 묻지 않는다(바꾼 것은 창을 연 때와 견준다).
+
+### 부재중 자동 응답 화면 — 엔진에서 규칙이 꺼졌을 때 (6단계 한계를 고친다)
+
+- 자동 응답을 켜 두었는데(저장한 값) 엔진에서 우리 규칙이 꺼져 있으면(`isActive: false`) 머리 아래 노란 줄(필터 화면의 줄과 같은 모양):
+ "켜 둔 자동 응답이 지금은 동작하지 않습니다. 메일 서버의 규칙이 다른 곳에서 바뀌었습니다 — 아래 [저장]을 누르면 다시 켜집니다."
+- 단추는 없다 — 저장 단추는 늘 누를 수 있다. 꺼 두었으면 보이지 않는다. 저장하면 필터도 함께 다시 켜진다.
+
## 구현하며 정한 보완 (2026-10-26 — 캔버스에는 아직 없다)
- 찾지 못한 첨부(보관 시간이 지난 업로드·지워진 원본)는 실패로 표시하고, 전달 원본에는 "빼기"만 둔다(다시 올릴 수 없다).
@@ -531,4 +568,22 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
- **그 밖**: [다시 켜기]가 성공해도 알림은 없다(줄이 사라진다). 목록 아래 줄은 보드 글 그대로 "… 행을 누르면 고칩니다."까지.
전달 칸은 `type=email` · 254자이고, 보기 글(받는 사람 칸 · 전달 칸)은 로그인한 직원 주소의 도메인으로 만든다.
저장하는 사이 화면을 떠나면 칸 안에 보일 거절을 알림으로 띄운다(부재중 화면과 같다). 목록에 `aria-busy` 를 걸지 않는다(칸마다 `aria-disabled`).
-- **하지 않은 것**: 고치기 창에 쓰다 만 채로 떠날 때 묻기(나가기 확인) — 보드에 없다. 창을 연 채 브라우저 뒤로 가기를 하면 쓴 것을 잃는다(알려진 한계).
+- **하지 않은 것**: 고치기 창에 쓰다 만 채로 떠날 때 묻기(나가기 확인) — 보드에 없다. 창을 연 채 브라우저 뒤로 가기를 하면 쓴 것을 잃는다(알려진 한계 — 7단계에서 고쳤다, 위 "7단계").
+
+### 7단계 구현하며 정한 보완 (2026-10-29 — 캔버스에는 아직 없다)
+
+- **「…에도 적용」 줄 높이**: 할 일 칸과 같은 44(보드는 40) — 고치기 창의 밀도 예외는 조건 칸 · 단추 40 뿐이라 새 예외를 만들지 않는다.
+- **고를 수 없는 경우를 넓혔다**(보드는 전달만 · 적용 중 둘): 옮길 메일함이 지워짐 · 받은편지함으로 옮기기만 하는 필터도 이미 받은 메일에 할 일이 없다(서버도 옮기지 않는다).
+ 까닭은 이 차례로 하나만 — 이 탭에서 적용 중 → 지워진 메일함 → 받은편지함으로 옮기기만 → 전달만. 할 일을 아직 하나도 고르지 않았으면 막지 않는다(저장할 때 칸 아래 "할 일을 하나 이상 골라 주세요.").
+ 고를 수 없게 되면 골라 둔 것을 쓰지 않고, 다시 고를 수 있게 되면 돌아온다. 같은 판정으로 저장한 뒤에도 시작하지 않는다 — 그때는 "필터를 저장했습니다."
+- **꺼 둔 필터도 적용한다** — 사용자가 이번 저장에 고른 일이다. 적용 칸만 고르고 다른 칸을 고치지 않아도 바꾼 것으로 본다(나가기 확인 · [저장]).
+- **전체 수**: 첫 응답 전에는 받은편지함의 메일 수(메일함을 아직 받지 않았으면 수를 뺀 문구), 그 뒤로는 "확인 + 남은 수"(최근 메일부터라 그사이 온 메일 · 옮겨진 메일로 조금 바뀔 수 있다).
+- **실패 알림**: 요청은 인터셉터 알림을 끄고 보내고, 응답이 온 때 세션이 그대로일 때만 사전 문구로 알린다(세션 만료 대화상자는 그대로) — 그래서 처음 요청부터 실패하면
+ 사전 문구 알림과 진행 알림의 결과 "필터는 저장했지만 …"가 함께 보인다(4단계 오래 걸리는 처리와 같다). 다른 창 · PC 에서 적용 중(429)은 결과 문구 하나만.
+ 도중의 429 는 이어 할 자리가 있으니 [다시 시도]를 붙인다(처음 요청의 429 는 보드처럼 단추 없음).
+- **이 탭에서 적용 중에 [다시 시도]**: 시작하지 않고 "다른 필터를 받은편지함에 적용하는 중입니다. 끝난 뒤 다시 시도해 주세요." + 같은 자리 · 같은 수의 [다시 시도].
+- **나아가지 못한 응답**(확인 0 인데 이어 할 자리가 있다)이 세 번 이어지면 멈춘다(`MAIL_FILTER_RUN_MAX_STALLS`) — 같은 요청을 끝없이 보내지 않게.
+- **맞추기**: 끝났거나 이번에 나아간 뒤 멈췄으면 개수 · 사용량을 다시 받고, 보이는 목록이 받은편지함이나 옮긴 메일함이면 그 목록도 맞춘다(실시간 연결이 없는 탭에서 1분까지 옛 개수가 보이지 않게).
+- **닫기 확인과 고치기 창**: 브라우저가 Esc 를 거듭 누르면 고치기 창을 막지 못하고 스스로 닫는다 — [계속 고치기]면 창을 다시 띄우고 초점은 닫히기 전에 있던 칸으로 돌려준다.
+ 닫기 확인이 떠 있는 동안 다른 화면으로 가려 하면(뒤로 가기) 같은 확인 창이 "이 화면을 나가면 …" [나가기]로 바뀐다. 저장 · 지우기를 보내는 중에는 묻지 않는다(결과는 알림으로).
+- **부재중**: 메일함 패널 아래의 부재중 안내는 그대로 둔다(저장한 값으로 보인다 — 동작하지 않음은 설정 화면에서만 알린다).
diff --git a/docs/design/webmail/canvas.json b/docs/design/webmail/canvas.json
index 5273b6e..635ddcc 100644
--- a/docs/design/webmail/canvas.json
+++ b/docs/design/webmail/canvas.json
@@ -24,16 +24,19 @@
"SettingsNotify.dc.html": { "x": 0, "y": 8560, "w": 1440, "h": 900, "title": "설정 · 알림", "expand": "fill", "is_interactive": true },
"MailRealtime.dc.html": { "x": 1520, "y": 8560, "w": 1440, "h": 1520, "title": "실시간 새 메일 · 바탕화면 알림 상태" },
"SettingsFilter.dc.html": { "x": 0, "y": 10680, "w": 1440, "h": 900, "title": "설정 · 필터", "expand": "fill", "is_interactive": true },
- "MailFilterStates.dc.html": { "x": 1520, "y": 10680, "w": 1440, "h": 1420, "title": "메일 필터 상태 · 메일에서 바로 만들기" }
+ "MailFilterStates.dc.html": { "x": 1520, "y": 10680, "w": 1440, "h": 1420, "title": "메일 필터 상태 · 메일에서 바로 만들기" },
+ "MailFilterApply.dc.html": { "x": 0, "y": 12700, "w": 1440, "h": 900, "title": "설정 · 필터 · 이미 받은 메일에 적용", "expand": "fill", "is_interactive": true },
+ "MailFilterApplyStates.dc.html": { "x": 1520, "y": 12700, "w": 1440, "h": 1280, "title": "필터 적용 진행 · 결과 · 나가기 확인 · 부재중 안내" }
},
- "order": ["Main.dc.html", "Login.dc.html", "Compose.dc.html", "States.dc.html", "MainList.dc.html", "MainListRead.dc.html", "ComposeEditor.dc.html", "Settings.dc.html", "ComposeAssist.dc.html", "Search.dc.html", "BulkSelect.dc.html", "ListStates.dc.html", "SettingsAccount.dc.html", "SettingsVacation.dc.html", "MailboxManage.dc.html", "MailActions4.dc.html", "SettingsNotify.dc.html", "MailRealtime.dc.html", "SettingsFilter.dc.html", "MailFilterStates.dc.html"],
+ "order": ["Main.dc.html", "Login.dc.html", "Compose.dc.html", "States.dc.html", "MainList.dc.html", "MainListRead.dc.html", "ComposeEditor.dc.html", "Settings.dc.html", "ComposeAssist.dc.html", "Search.dc.html", "BulkSelect.dc.html", "ListStates.dc.html", "SettingsAccount.dc.html", "SettingsVacation.dc.html", "MailboxManage.dc.html", "MailActions4.dc.html", "SettingsNotify.dc.html", "MailRealtime.dc.html", "SettingsFilter.dc.html", "MailFilterStates.dc.html", "MailFilterApply.dc.html", "MailFilterApplyStates.dc.html"],
"notes": {
"flow": { "x": 0, "y": -300, "text": "arasystem 웹메일 — 1단계 화면 (로그인 → 메일함 → 쓰기)", "kind": "title1", "maxW": 4480 },
"phase2": { "x": 0, "y": 2180, "text": "2단계 화면 — 임시 저장 · 받는 사람 자동 완성 · 서명 (2026-10-27 확정)", "kind": "title1", "maxW": 2960 },
"phase3": { "x": 0, "y": 4180, "text": "3단계 화면 — 새 메일 · 검색 · 여러 통 처리 (2026-10-27 확정)", "kind": "title1", "maxW": 4480 },
"phase4": { "x": 0, "y": 6340, "text": "4단계 화면 — 계정 · 메일 설정 (비밀번호 · 부재중 응답 · 메일함 관리 · 스팸 · 원문) — 2026-10-28 확정", "kind": "title1", "maxW": 6000 },
"phase5": { "x": 0, "y": 8260, "text": "5단계 화면 — 실시간 새 메일 · 바탕화면 알림 — 2026-10-28 확정", "kind": "title1", "maxW": 2960 },
- "phase6": { "x": 0, "y": 10380, "text": "6단계 화면 — 메일 필터 (설정 › 메일 › 필터 · 읽기 화면 ⋯ 「이런 메일로 필터 만들기」) — 2026-10-28 확정", "kind": "title1", "maxW": 2960 }
+ "phase6": { "x": 0, "y": 10380, "text": "6단계 화면 — 메일 필터 (설정 › 메일 › 필터 · 읽기 화면 ⋯ 「이런 메일로 필터 만들기」) — 2026-10-28 확정", "kind": "title1", "maxW": 2960 },
+ "phase7": { "x": 0, "y": 12400, "text": "7단계 화면 — 이미 받은 메일에 필터 적용 · 필터 창 나가기 확인 · 부재중 안내 — 2026-10-29 확정", "kind": "title1", "maxW": 2960 }
},
"designSystems": []
}
diff --git a/docs/mail/api-contract.md b/docs/mail/api-contract.md
index b0a1565..0fd1170 100644
--- a/docs/mail/api-contract.md
+++ b/docs/mail/api-contract.md
@@ -208,10 +208,11 @@ HTML 은 정화하기 **전에** 길이(HTML 부분이 여럿이면 이어 붙
| `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` · `MAIL_013`(필터와 함께 만든 엔진 규칙이 한도를 넘음 — 6단계), 503 `MAIL_001` |
+| `GET /api/mail-vacation` | — | 200 `MailVacationState` (아래 "부재중 자동 응답" — `MailVacation` + 7단계의 `isActive`) | 503 `MAIL_001` |
+| `PUT /api/mail-vacation` | `MailVacation` (전체 교체 — `isActive` 는 보내지 않는다) | 200 `MailVacationState`(저장한 값) | 400 `VAL_001` · `MAIL_013`(필터와 함께 만든 엔진 규칙이 한도를 넘음 — 6단계), 503 `MAIL_001` |
| `GET /api/mail-filters` | — | 200 `MailFilterSet` (아래 "메일 필터", 6단계) | 503 `MAIL_001` |
| `PUT /api/mail-filters` | `{ rules, version }` (전체 교체) | 200 `MailFilterSet` | 400 `VAL_001` · `MAIL_013`(만든 규칙이 엔진 한도를 넘음), 409 `MAIL_012`(그사이 다른 곳에서 필터가 바뀜 — 저장 안 함), 503 `MAIL_001` |
+| `POST /api/mail-filter-runs` | `MailFilterRunRequest` — 받은편지함에 이미 있는 메일에 규칙 하나를 적용한다(아래 "이미 받은 메일에 적용", 7단계) | 200 `MailFilterRun` | 400 `VAL_001`(규칙 검사 · 할 일 없음 · 깨진 cursor), 429 `REQ_001`(같은 직원의 적용이 진행 중), 503 `MAIL_001` |
| `GET /api/mail-quota` | — | 200 `MailQuota` | `MAIL_001` |
| `GET /api/mail-state` | — | 200 `MailState` (아래 "새 메일 확인") | 503 `MAIL_001` |
| `GET /api/mail-events` | — | `text/event-stream` (표준 래퍼 없음 — 아래 "실시간 알림", 5단계) | 401 `AUTH_001`, 429 `REQ_001`(한 직원의 연결 5개 초과), 503 `MAIL_001` |
@@ -408,6 +409,10 @@ interface RejectedRecipient {
- 메일 한 통의 첨부 합계(전달하는 원본 첨부 포함, 같은 첨부를 두 번 붙이면 두 번 센다) `attachmentTotalMaxBytes`
(`MAIL_ATTACHMENT_TOTAL_MAX_SIZE_MB`, 기본 25MB) — 넘으면 발송이 413 `FILE_002`.
클라이언트는 첨부를 추가할 때 이 값으로 **미리 막고** 안내한다 (다 쓰고 보낼 때 알게 하지 않는다).
+- **엔진의 임시 업로드 한도**(7단계에 찾음): 계정마다 1시간 동안 올린 크기 합계 · 개수(Stalwart `uploadQuota` · `maxUploadCount` · `uploadTtl`). 엔진 기본값은 50MB · 1,000개 —
+ 넘으면 올린 것이 만료될 때까지(1시간) 그 직원의 첨부 올리기와 필터 · 부재중 저장(엔진에 규칙 글을 올린다) · 서명 그림이 든 발송이 429 `REQ_001` 로 막힌다
+ (2026-10-29 개발 엔진: 1,000개를 넘기자 필터 저장이 막혔고, 20MiB 두 개 뒤 세 번째 업로드가 429 — 크기 · 개수 어느 쪽이든 같은 `REQ_001`, 백엔드 로그 `"call":"upload"`).
+ 그래서 mail setup 이 크기 합계를 첨부 합계 한도의 20배(기본 25MB × 20 = 524,288,000바이트)로 둔다 — 개수(1,000)는 그대로(mail/README.md "엔진 한도").
- 허용 형식(확장자 + MIME 둘 다 검사): pdf · doc/docx · xls/xlsx · ppt/pptx · hwp/hwpx · txt · csv · png · jpg/jpeg · gif · webp · zip
- 다운로드는 `` 이동으로 받는다 — 같은 출처라 쿠키가 함께 간다.
이동은 axios 를 거치지 않아 401 재시도 규칙이 적용되지 않으므로, **이동하기 전에 세션을 확인**한다
@@ -681,6 +686,10 @@ interface MailVacation {
subject: string // 켤 때 150자까지(꺼 둔 채로는 200자까지 — 아래 "길이 한도의 근거") — 켤 때(isEnabled true)는 공백이 아닌 글자 1자 이상. 제어 문자(메일함 이름과 같은 범위 — 탭 · 줄바꿈 포함)는 쓰지 않는다(한 줄짜리 머리글)
textBody: string // 켤 때 600자까지(꺼 둔 채로는 4,000자까지) — 켤 때는 공백이 아닌 글자 1자 이상. 글자만(서식 없음) — 제어 문자는 탭 · LF · CR 만. 서명은 붙지 않는다
}
+interface MailVacationState extends MailVacation { // GET · PUT 의 응답 (7단계)
+ isActive: boolean // false = 엔진에서 우리 규칙(필터와 한 스크립트)이 꺼져 있어 켜 둔 자동 응답이 지금은 동작하지 않는다 — 저장하면 다시 켜진다.
+ // 메일 필터의 isActive 와 같은 값(한 번도 저장하지 않은 계정은 true). PUT 본문에는 넣지 않는다(넣으면 400 VAL_001 — 모르는 필드)
+}
```
- **길이 한도의 근거 (6단계 앞 수정)**: 엔진이 UTF-8 **바이트**로 막는다 — 제목 511바이트(한글 170자 · 영문 511자), 본문 2,047바이트(한글 682자 · 영문 2,047자)
@@ -696,8 +705,9 @@ interface MailVacation {
서식 본문은 없다(글자만). 저장은 전체 교체이고 응답은 저장한 값이다. 꺼도 쓴 제목 · 내용은 남겨 둔다.
필터와 한 스크립트라서 부재중을 저장해도 스크립트를 다시 켠다(필터가 꺼져 있었으면 함께 다시 동작한다). 필터 규칙은 건드리지 않는다.
- 6단계부터 바뀐 동작: 엔진이 스팸으로 분류한 메일에는 답하지 않는다(필터와 같은 스팸 막기 — "메일 필터"의 "만드는 Sieve").
- - 우리 스크립트가 엔진에서 꺼져 있으면(필터의 `isActive: false`) 부재중도 동작하지 않는다. 이 API 의 `isEnabled` 는 저장한 값이라 그때도 `true` 로 온다
- (알려진 한계 — 드묾. 화면은 필터 화면의 줄로만 알린다).
+ - 우리 스크립트가 엔진에서 꺼져 있으면(`isActive: false`) 부재중도 동작하지 않는다 — `isEnabled` 는 저장한 값이라 그때도 `true` 다.
+ 7단계부터 응답의 `isActive` 로 알린다 — 화면은 켜 두었을 때 머리 아래 줄 "켜 둔 자동 응답이 지금은 동작하지 않습니다. 메일 서버의 규칙이 다른 곳에서 바뀌었습니다 —
+ 아래 [저장]을 누르면 다시 켜집니다."(단추 없음 — 저장 단추는 늘 누를 수 있다. 6단계의 알려진 한계를 고쳤다).
- 저장해 둔 원본이 깨진 계정은 꺼짐(`isEnabled: false`, 빈 제목 · 내용)으로 온다. 원본이 이 서버보다 새 판이면 저장은 503 `MAIL_001` 이다("메일 필터"의 "저장 자리").
- `fromDate` · `toDate` 는 `Z` 로 끝나는 UTC 시각이고, 두 키는 늘 보낸다(값은 null 가능). 엔진에는 초 단위로 저장한다(소수 초는 버린다).
둘 다 있으면 버린 값으로 `fromDate < toDate`(아니면 400 `VAL_001`). 화면은 날짜만 고르고, 브라우저 시간대의 시작일 0시 · 끝나는 날 다음 날 0시를 UTC 로 보낸다.
@@ -832,7 +842,7 @@ interface MailFilterActions {
전달은 한 통에 한 곳 — 엔진 한도(사용자 스크립트의 리다이렉트 1)라 두 번째 전달은 엔진이 조용히 버린다(2026-10-28 확인). 사본은 늘 남긴다(`redirect :copy`).
**스팸으로 판정된 메일에는 아무 규칙도 적용하지 않는다**(부재중 응답도 하지 않는다) — 엔진은 규칙의 옮기기가 스팸메일함 분류를 이기므로(같은 날 확인) 막는다.
판정은 엔진의 분류 그대로다(지금 엔진 설정은 5점부터 스팸 — 아래 "만드는 Sieve"의 스팸 막기).
- 꺼 둔 규칙은 목록에만 있고 적용하지 않는다. 이미 받은 메일에는 적용하지 않는다.
+ 꺼 둔 규칙은 목록에만 있고 적용하지 않는다. 이미 받은 메일에는 저절로 적용하지 않는다 — 저장할 때 고르면 아래 "이미 받은 메일에 적용"(7단계).
- **저장 자리** — 엔진의 Sieve 스크립트 하나(`arasystem`)에 필터와 **부재중 자동 응답을 함께** 둔다. Stalwart 0.16.27 은 계정마다 켜진 스크립트가 하나뿐이고
엔진의 VacationResponse 도 스크립트(`vacation`)라서, 부재중을 켜면 필터가 꺼지고 필터를 켜면 부재중이 꺼진다(같은 날 확인).
- 4단계 `/api/mail-vacation` 은 계약 그대로 두고 저장 자리만 이 스크립트로 옮긴다 — 엔진의 VacationResponse 는 더 쓰지 않는다.
@@ -878,8 +888,86 @@ interface MailFilterActions {
- 검사는 서버와 같은 규칙(이름 50 · 값 200 · 조건 5 · 규칙 50 · `${` · 제어 문자 · 할 일 하나 이상 · 전달은 사내 주소이고 자기 주소 아님) — 칸 아래에만 알린다.
전달 주소의 앞부분은 서버(`IsEmail`)보다 좁게 본다(RFC 5322 dot-atom · 64자) — 화면이 통과시킨 주소를 서버가 거절하지 않게.
규칙이 50개면 [새 필터] · 「이런 메일로 필터 만들기」를 끄고 까닭을 보인다. 지운 메일함을 가리키는 규칙은 목록에 빨간 안내.
-- 이 단계에서 하지 않는 것: 이미 받은 메일에 적용, **바깥 주소로 전달**(엔진에 SRS 가 없어 Gmail 등이 DMARC 로 거절할 수 있고, 회사 메일이 개인 메일로 빠지는 통로다 —
- 필요해지면 정책과 함께), 크기 · 첨부 · 날짜 조건, 정규식, 맞는 메일 미리 보기, mailcow(SOGo)의 필터 옮겨 오기(운영 전환 때 — imapsync 로는 옮겨지지 않는다).
+- 이 단계에서 하지 않는 것: **바깥 주소로 전달** — 전달한 메일은 보낸 사람(From)이 원래의 바깥 도메인인데 우리 서버가 건넨다. 엔진은 봉투 보낸 사람을
+ 필터 주인으로 바꿔 SPF 는 통과하지만(SRS 는 없다 — 거절되면 반송은 원래 보낸 사람이 아니라 필터 주인에게 온다), 받는 쪽의 DMARC 는 From 기준이라
+ 원래 보낸 쪽의 DKIM 서명에 기댄다. 서명이 없거나 깨진 메일은 그 도메인이 엄격하면(p=reject) Gmail 등이 거절하거나 스팸으로 보낸다 — 전달 메일의 일반적인 한계라
+ SRS 가 있어도 같다. 그리고 회사 메일이 개인 메일로 자동으로 빠지는 통로다 — **회사 정책으로 허용하지 않는다**(2026-10-29 결정 — 전달은 사내 주소로만).
+ 보통 발송(직원이 바깥 주소로 보내는 메일)과는 상관없다.
+ 크기 · 첨부 · 날짜 조건, 정규식, 맞는 메일 미리 보기, mailcow(SOGo)의 필터 옮겨 오기(운영 전환 때 — imapsync 로는 옮겨지지 않는다).
+
+### 이미 받은 메일에 적용 (`/api/mail-filter-runs`) — 7단계
+
+필터를 저장할 때 고르면(고치기 창 「받은편지함에 이미 있는 메일에도 적용」) 받은편지함에 이미 있는 메일에도 그 규칙 하나를 적용한다.
+엔진의 Sieve 는 메일을 받을 때만 돌기 때문에 서버가 받은편지함을 읽어 같은 규칙으로 판정하고 표시 · 옮기기를 한다.
+
+```ts
+interface MailFilterRunRequest {
+ match: 'all' | 'any' | 'always' // 필터 규칙과 같은 검사(MailFilterRule 의 match · conditions)
+ conditions: MailFilterCondition[] // all · any 는 1~5개, always 는 빈 배열
+ actions: { // 하나 이상 — 전달은 없다(이미 받은 메일은 전달하지 않는다)
+ moveToMailboxId: string | null // 필터 규칙과 같은 메일함만(보낸편지함 · 임시보관함은 400). 받은편지함이거나 없는 메일함이면 옮기지 않는다
+ markAsRead: boolean
+ flag: boolean
+ }
+ cursor: string | null // 처음은 null, 이어 할 때는 앞 응답의 cursor 그대로(해석하지 않는다)
+}
+interface MailFilterRun {
+ scanned: number // 이번 요청에서 확인한 받은편지함 메일 수
+ matched: number // 그중 조건에 맞아 할 일을 한 메일 수(이미 읽음 · 중요였던 메일도 센다)
+ remaining: number // 아직 확인하지 않은 메일 수 — 어림(그사이 들어오고 나간 메일이 있다)
+ cursor: string | null // null = 끝. 아니면 같은 본문에 이 cursor 를 넣어 다시 보낸다
+}
+```
+
+- 받은편지함만, 받은 시각이 최근인 메일부터 본다. 저장해 둔 필터를 읽지 않고 본문의 규칙을 쓴다(클라이언트는 방금 저장한 그 규칙을 보낸다) —
+ 그래서 적용하는 동안 다른 필터를 켜고 끄거나 저장해도 멈추지 않는다.
+- 한 요청은 25초 안에 끝내고 `cursor` 를 돌려준다 — 4단계 "오래 걸리는 처리"와 같은 상한 · 같은 규칙(다음 차례는 남은 시간이 5초보다 많을 때만, 엔진 호출마다 남은 시간만큼,
+ 진행한 것이 있으면 거기까지 200, 하나도 확인하지 못했으면 503 `MAIL_001`). 같은 직원의 적용은 한 번에 하나 — 진행 중이면 엔진에 묻지 않고 429 `REQ_001`(다른 탭 · PC).
+- **판정은 서버가 한다** — 새로 오는 메일의 Sieve(위 "메일 필터"의 "만드는 Sieve")와 같은 규칙을 따라 한다. 엔진 검색(`Email/query` 의 글자 조건)은 쓰지 않는다 —
+ 낱말이 통째로 같을 때만 맞고('회의'로 '회의를' · '아라'로 '김아라'를 못 찾는다) `header` 조건은 아무것도 찾지 못한다(Stalwart 0.16.27, 2026-10-28 확인).
+ - 보낸 사람: From 머리글의 원래 글 — 접힌 줄(줄바꿈 + 공백 · 탭)은 띄어쓰기 하나로 펴고(접지 않은 탭 · 이어진 공백은 그대로), 인코딩된 글(RFC 2047 — 문자셋 그대로)은
+ **홀로 선 것만** 풀고(앞이 처음 · 공백 · `"` · `,`, 뒤가 끝 · 공백 · `"` — `abc=?…?=` · `=?…?= ` · `(=?…?=)` 는 그대로. 공백만 사이에 둔 인코딩된 글끼리는 붙인다),
+ 앞뒤 공백을 뺀 것 — 따옴표 · 꺾쇠까지 견준다(`"Kim, Ara" ` 에 `"Kim` 이 맞는다 — 엔진과 같다). 받는 사람: To · Cc 머리글 각각(하나라도 맞으면). 제목: 풀어 쓴 제목.
+ - 본문: 텍스트 · HTML 부분과 **텍스트 첨부**(text/* — 파일로 붙인 것도. 엔진이 그렇게 본다). 한 부분은 앞 256KiB 까지, 한 통은 모두 합쳐 1MiB 까지 본다
+ (글자 부분이 많아 넘으면 부분마다 같은 몫만큼 앞쪽만 — 바깥에서 온 메일 한 통으로 서버가 메모리를 크게 쓰지 않게). HTML 은 글자 부분만 — 엔진과 같게:
+ - 태그는 `<` 부터 다음 `>` 까지다 — `<` 뒤가 무엇이든 태그이고(`a < b` · `<광고>` 도), 따옴표 안의 `>` 에서도 끝난다. 닫히지 않은 `<` 는 끝까지 삼킨다.
+ 태그는 속성까지 **빈자리 없이** 뺀다(`회의 록` → "회의록", `div` · `li` · `h1` · 표 칸 · 표 줄 · `hr` · 여는 `p` 사이도 이어 붙는다). 주석(``)은 안의 글까지 뺀다
+ - `
`(닫는 것만) · ` `(` ` · ` ` · `` · 대문자도)은 줄바꿈이다 — 찾을 글(줄바꿈이 없다)은 이 자리를 건너 맞지 않는다(`앞
뒤
` 에 "앞뒤" · "앞 뒤" 모두 안 맞음)
+ - `