From 41b5fdbe223443a3fbe9ebee8338e40654aae3dc Mon Sep 17 00:00:00 2001 From: ttipo Date: Wed, 28 Oct 2026 20:58:51 +0900 Subject: [PATCH] =?UTF-8?q?docs:=206=EB=8B=A8=EA=B3=84(=EB=A9=94=EC=9D=BC?= =?UTF-8?q?=20=ED=95=84=ED=84=B0)=20=EA=B3=84=EC=95=BD=EA=B3=BC=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=C2=B7=20JSON=20=EB=B3=B8?= =?UTF-8?q?=EB=AC=B8=20=EB=AA=A8=EC=96=91=20=ED=95=9C=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 메일 필터(GET·PUT /api/mail-filters)와, 부재중 자동 응답의 저장 자리를 엔진의 Sieve 스크립트 하나로 옮기는 계약을 적는다. 확정한 캔버스 보드(SettingsFilter · MailFilterStates)와 구현하며 정한 보완도 함께 둔다. - 엔진(Stalwart 0.16.27)은 계정마다 켜진 스크립트가 하나뿐이다. 엔진의 부재중도 스크립트라 따로 두면 하나를 켤 때 다른 하나가 꺼진다. 그래서 한 스크립트에 함께 둔다 - 스팸 막기는 엔진의 분류 표시(X-Spam-Score)로 한다. spamtest 는 점수를 정수로 내린 값이라 10 으로 막으면 5~10점 스팸을 놓친다(실엔진에서 확인) - 모든 JSON 본문에 모양 한도(배열 1,000 · 값 10,000 · 깊이 32)를 걸고 UTF-8 만 받는다. DTO 의 개수 한도로는 본문 검증 DoS 를 막지 못했다(리뷰 측정) Co-Authored-By: Claude Opus 5.5 --- docs/design/webmail/MailFilterStates.dc.html | 219 +++++++++ docs/design/webmail/README.md | 63 ++- docs/design/webmail/SettingsFilter.dc.html | 447 +++++++++++++++++++ docs/design/webmail/canvas.json | 9 +- docs/mail/api-contract.md | 109 ++++- 5 files changed, 839 insertions(+), 8 deletions(-) create mode 100644 docs/design/webmail/MailFilterStates.dc.html create mode 100644 docs/design/webmail/SettingsFilter.dc.html diff --git a/docs/design/webmail/MailFilterStates.dc.html b/docs/design/webmail/MailFilterStates.dc.html new file mode 100644 index 0000000..d72aff1 --- /dev/null +++ b/docs/design/webmail/MailFilterStates.dc.html @@ -0,0 +1,219 @@ + + + + +메일 필터 상태 모음 + + + + + + + +
+ +
+

메일 필터

+

6단계에서 새로 생기는 것입니다. 필터는 설정 › 메일 › 필터에서 만들고, 「설정 · 필터」 보드에서 눌러 볼 수 있습니다. 이 보드는 그 화면의 다른 상태와 메일 읽기 화면에서 바로 만드는 길을 모았습니다.

+
+ +
+ +
+
+

필터가 아직 없을 때

+

목록 자리에 안내 · [새 필터]는 머리의 단추와 같은 일을 한다

+
+
+ +

아직 필터가 없습니다

+

보낸 사람 · 받는 사람 · 제목 · 본문의 글로 메일을 골라 메일함으로 옮기거나, 읽음 · 중요로 표시하거나, 동료에게 전달합니다.

+ +
+
+ +
+
+

저장하지 못하는 필터 — 칸 아래에 알린다

+

「저장」을 누를 때 검사한다 · 알림(토스트)은 띄우지 않는다 · 첫 잘못된 칸에 초점

+
+
+
+ 보낸 사람 + 포함 + 예: @hanbit-partners.co.kr 또는 이름 +
+

비어 있는 조건이 있습니다. 찾을 글을 적거나 그 조건을 지워 주세요.

+
+ 제목 + 포함 + 가격 ${price} +
+

‘${’ 는 쓸 수 없습니다 — 메일 서버가 다른 뜻으로 읽습니다.

+
+ 동료에게 전달 + me@gmail.com +
+

사내 주소(@arasystem.it.kr)로만 전달할 수 있습니다.

+

할 일을 하나 이상 골라 주세요. — 할 일 칸이 모두 비었을 때

+
+
+ +
+ +
+ +
+
+

다른 곳에서 먼저 바꿨을 때

+

다른 탭 · PC 에서 필터를 먼저 저장한 뒤 이 화면에서 저장하면 — 목록 위 줄 (부재중 응답을 저장한 것은 걸리지 않는다)

+
+
+ + 다른 곳에서 필터가 바뀌어 저장하지 않았습니다. 다시 불러온 뒤 고쳐 주세요. + +
+

고치던 창은 닫지 않고 그대로 둔다(쓴 것을 잃지 않게). 다시 불러오면 목록을 새로 받고 창을 닫는다.

+
+ +
+
+

필터가 동작하지 않을 때

+

메일 서버의 규칙이 이 화면 밖에서 바뀐 드문 경우(관리자 작업 등) — 목록 위 줄

+
+
+ + 지금은 필터와 부재중 자동 응답이 동작하지 않습니다. 메일 서버의 규칙이 다른 곳에서 바뀌었습니다. + +
+

[다시 켜기]는 지금 보이는 필터 · 부재중 응답 그대로 다시 저장한다.

+
+ +
+
+

50개를 다 만들었을 때

+

[새 필터]를 끄고 까닭을 옆에 · 메일 읽기 화면의 「이런 메일로 필터 만들기」도 같은 안내

+
+
+ + 필터는 50개까지 만들 수 있습니다. 쓰지 않는 필터를 지워 주세요. +
+

조건이 길어 메일 서버의 한도(100KB)를 넘으면 저장할 때 알린다 — "필터가 너무 길어 저장할 수 없습니다. 조건을 줄이거나 쓰지 않는 필터를 지워 주세요."

+
+ +
+ +
+ +
+
+

메일에서 바로 만들기 — 읽기 화면 ⋯

+

⋯ 메뉴에 「이런 메일로 필터 만들기」 · 누르면 설정 › 필터의 새 필터 창이 보낸 사람으로 채워져 열린다

+
+
+
+
+ 읽기 화면 도구 줄 + +
+
+ + +
+
+
+ 새 필터 + 어떤 메일에 + 다음 조건이 모두 맞는 메일 +
+ 보낸 사람 + 포함 + seojun.park@arasystem.it.kr +
+

보낸 사람의 주소로 채운다(이름이 아니라 주소 — 같은 이름의 다른 사람을 고르지 않게). 할 일은 비어 있고, 초점은 할 일의 첫 칸.

+
+
+
+ +
+
+

알림 문구

+

켜기 · 끄기 · 순서 바꾸기는 목록이 바로 바뀌므로 알리지 않는다 — 실패하면 되돌리고 오류 알림

+
+
+
+ + 필터를 저장했습니다. +
+
+ + ‘거래처 메일’ 필터를 지웠습니다. + +
+
+ + 메일 서버와 연결할 수 없습니다. 잠시 후 다시 시도해 주세요. — 켜기 · 순서 바꾸기가 실패하면 목록을 되돌리고 +
+
+

[되돌리기]는 지우기 직전의 목록 그대로 다시 저장한다(10초 · 마우스를 올린 동안 멈춤 — 지금의 되돌리기 알림과 같다).

+
+ +
+ +
+

어떻게 적용하나

+
+

새로 온 메일을 위의 필터부터 차례로 견준다. 맞는 필터의 할 일을 모두 한다 — 읽음 · 중요 표시는 겹쳐 붙는다.

+

메일함으로 옮기는 필터에 맞으면 거기서 멈춘다 — 아래 필터는 보지 않는다. 받은편지함으로 옮기기를 위에 두면 "이 사람 메일은 늘 받은편지함에"가 된다.

+

전달은 한 통에 한 곳 — 먼저 맞은 필터의 주소로만 간다. 사본은 늘 내 메일함에 남는다.

+

스팸으로 판정된 메일은 필터를 건너뛰고 스팸메일함으로 간다. 부재중 자동 응답도 스팸에는 답하지 않는다.

+

옮길 메일함이 지워졌으면 받은편지함으로 들어온다 — 필터 목록에 빨간 안내. 메일함 이름을 바꾼 것은 그대로 이어진다.

+

꺼 둔 필터는 목록에만 남고 적용하지 않는다. 이미 받은 메일에는 적용하지 않는다.

+
+
+ +
+
+ + + diff --git a/docs/design/webmail/README.md b/docs/design/webmail/README.md index a5c6169..27c00f8 100644 --- a/docs/design/webmail/README.md +++ b/docs/design/webmail/README.md @@ -1,8 +1,8 @@ -# 웹메일 디자인 원본 (1~5단계) +# 웹메일 디자인 원본 (1~6단계) 확정된 디자인 캔버스의 원본이다. 화면 구현의 기준으로 쓴다. -- 캔버스: https://claude.ai/artifact/WJvVsKvXepxn3p75CGJAr2 (확정본: 2026-10-28, 버전 27) +- 캔버스: https://claude.ai/artifact/WJvVsKvXepxn3p75CGJAr2 (확정본: 2026-10-28, 버전 31) - 화면: `Login`(로그인) · `Main`(분할 보기: 메일함·목록·본문) · `MainList`(목록 보기: 게시판형 표) · `MainListRead`(목록 보기에서 연 메일) · `Compose`(메일 쓰기 — 서식 편집기) · `ComposeEditor`(메일 쓰기 · 편집기 상태: 포커스·툴바·메뉴·팝오버·인용·이미지 첨부) · `States`(오류·빈 상태·피드백 모음) @@ -10,6 +10,7 @@ · 3단계: `Search`(메일 검색 · 분할 보기) · `BulkSelect`(여러 통 선택 · 목록 보기) · `ListStates`(새 메일 · 검색 · 여러 통 처리 상태) — 아래 "3단계" · 4단계: `SettingsAccount`(설정 · 비밀번호) · `SettingsVacation`(설정 · 부재중 자동 응답) · `MailboxManage`(메일함 관리 · 휴지통 비우기) · `MailActions4`(스팸 · 원문 · 오래 걸리는 처리) — 아래 "4단계" · 5단계: `SettingsNotify`(설정 · 알림) · `MailRealtime`(실시간 새 메일 · 바탕화면 알림 상태) — 아래 "5단계" + · 6단계: `SettingsFilter`(설정 · 필터 — 고치기 창) · `MailFilterStates`(메일 필터 상태 · 메일에서 바로 만들기) — 아래 "6단계" - 파일 형식은 캔버스 전용(`` 마크업 + `DCLogic` 클래스)이다. 그대로 쓰지 말고 **구조·간격·색·문구의 기준**으로만 본다. `Compose` 의 툴바·메뉴는 눌러 볼 수 있는 시안이다 — 버튼 상태만 바뀌고 본문 글자에 서식이 입혀지지는 않는다. @@ -346,6 +347,31 @@ API 와 서버 규칙은 계약 2절 "실시간 알림". - 다른 탭을 보는 동안에도 탭 제목의 "(N)" 이 바로 바뀐다. - 실시간 연결이 끊기면 화면에는 알리지 않고 1분 확인으로 돌아간다(설정 › 알림의 연결 상태로 볼 수 있다). +## 6단계 — 메일 필터 (2026-10-28 확정 — `SettingsFilter` · `MailFilterStates`) + +받은 메일을 조건에 따라 메일함으로 옮기고, 읽음 · 중요로 표시하고, 동료에게 전달한다. 규칙과 엔진 쪽 사실은 계약 2절 "메일 필터". + +### 설정 › 메일 › 필터 + +- 메뉴는 메일 묶음의 서명 · 부재중 자동 응답 · **필터** · 알림 (깔때기 아이콘). 머리는 "필터" + 설명 한 줄 + 오른쪽 [새 필터](브랜드 색). +- 목록은 테두리 상자 하나에 행 60 — 순서 번호 · 켜기 스위치(36×20) · 이름(굵게)과 요약 한 줄 · ▲ ▼(32×40, 밀도 예외 — 메일함 ⋯ 와 같다). 행을 누르면 고친다. + 요약은 "보낸 사람에 ‘@hanbit-partners.co.kr’ 포함 → 옮기기: 거래처" — 조건은 " 그리고 " / " 또는 ", 할 일은 " · ". 이름이 없으면 조건이 이름 자리에 온다. + 꺼 둔 규칙은 글자를 흐리게. 옮길 메일함이 지워진 규칙은 빨간 안내 한 줄 "옮길 메일함이 지워져 받은편지함으로 들어옵니다 — 눌러서 고치세요." +- 목록 아래 "필터 N개 · 50개까지 만들 수 있습니다." 와 안내 상자(차례로 적용 · 옮기면 멈춤 / 스팸 제외 / 사내 전달만 · 한 곳 · 사본 남김 / 부재중과 함께 동작). + +### 고치기 창 (모달 620) + +- 이름(선택) → "어떤 메일에"(조건이 모두 · 하나라도 · 모든 메일) → 조건 줄(어디에서 116 · 포함/포함하지 않음 132 · 글 · ✕ 36, 5개까지 [조건 더하기]) + → "할 일"(메일함으로 옮기기 + 메일함 고르기 · 읽음으로 표시 · 중요 표시 · 동료에게 전달 + 사내 주소) → 안내 띠 → [이 필터 지우기](고칠 때만, 왼쪽) · [저장] [취소]. +- 메일함 고르기에는 받은편지함 · 내 메일함 · 스팸메일함 · 휴지통(보낸편지함 · 임시보관함 없음 — 보관함은 구현하며 더했다, 아래 "6단계 구현하며 정한 보완"). 지워진 메일함이면 "(지워진 메일함) — 다른 메일함을 고르세요"가 골라진 채로. +- 잘못은 [저장]을 누를 때 칸 아래 빨간 글로만 — 빈 조건 · `${` · 바깥 주소 전달 · 할 일 없음 (`MailFilterStates` 의 모음). + +### 상태 · 들어오는 길 (`MailFilterStates`) + +- 필터가 없을 때 · 다른 곳에서 먼저 바꿨을 때(목록 위 노란 줄 + [다시 불러오기]) · 동작하지 않을 때(+ [다시 켜기]) · 50개를 다 만들었을 때 · 알림 문구. +- 읽기 화면 ⋯ 메뉴에 「이런 메일로 필터 만들기」(원문 내려받기 아래) — 설정 › 필터의 새 필터 창이 보낸 사람의 **주소**로 채워져 열린다. +- 켜기 · 끄기 · 순서 바꾸기는 알리지 않는다(목록이 바로 바뀐다). 저장 "필터를 저장했습니다.", 지우기 "‘거래처 메일’ 필터를 지웠습니다. [되돌리기]". + ## 구현하며 정한 보완 (2026-10-26 — 캔버스에는 아직 없다) - 찾지 못한 첨부(보관 시간이 지난 업로드·지워진 원본)는 실패로 표시하고, 전달 원본에는 "빼기"만 둔다(다시 올릴 수 없다). @@ -473,3 +499,36 @@ API 와 서버 규칙은 계약 2절 "실시간 알림". "보냈지만 N명은 받지 못했습니다 — 주소 (까닭)"와 [보낸편지함 보기](보냈다는 알림과 같은 링크) — 두 명까지 주소 · 까닭을 적고 나머지는 " 외 N명". 까닭 문구 "없는 주소" · "받는 쪽이 거절" · "지금은 받을 수 없음". - **모두 받지 못했을 때**: 보내지 않은 것이다 — 없는 주소 · 거절(`MAIL_010`)은 받는 사람 오류(`MAIL_002`)와 같은 안내 띠 · 같은 초점으로, 지금은 받을 수 없음(`MAIL_011`)은 오류 알림으로. 쓰던 메일은 그대로 둔다. - **부재중 자동 응답 길이**: 제목 150자 · 내용 600자(입력칸 한도(maxlength)도 이 값, 켤 때 검사 — 꺼 둔 채로는 4단계 한도까지 저장된다) — 엔진이 바이트로 막아 한글로는 4단계의 200자 · 4,000자가 저장되지 않았다(계약 2절 "부재중 자동 응답"). + +### 6단계 구현하며 정한 보완 (2026-10-28 — 캔버스에는 아직 없다) + +- **되돌리기**: 지우기 전 목록을 통째로 되돌리지 않고, 지운 필터만 지운 자리에 다시 넣어 저장한다 — 10초 안에 다른 필터를 켜거나 옮긴 것이 사라지지 않게(보드는 "지우기 직전의 목록 그대로"). + 자리는 앞 · 뒤 필터로 기억한다(앞 필터 뒤 → 없으면 뒤 필터 앞 → 없으면 지운 때의 차례) — 여러 개를 어떤 차례로 되돌려도 원래 차례가 된다. + 여러 알림의 [되돌리기]는 누른 차례대로, 돌고 있는 저장이 끝난 뒤에 보낸다. 다시 넣으면 50개를 넘을 때는 보내지 않고 50개 안내를 오류 알림으로. + 필터 화면을 떠난 뒤에 실패하면(다른 곳에서 바뀜 포함) 알림으로 알린다. +- **다른 곳에서 바뀜**: 고치기 창 안(단추 줄 위)에도 목록 위 줄과 같은 문구와 [다시 불러오기](36 — 밀도 예외)를 둔다 — 창이 모달이라 목록 위 줄을 누를 수 없다. + 창에서 다시 불러오면 창과 쓴 내용은 그대로이고 초점은 [저장]으로 간다. 다음 [저장]은 다시 받은 목록에 넣는다 — + 같은 필터의 자리, 그사이 다른 곳에서 지워졌으면 맨 아래(그때 50개를 넘으면 창 안에 50개 안내). 안내는 다시 받기 전까지 남는다(칸을 고쳐도). + 다시 받은 목록에 이미 없는 필터에서 [이 필터 지우기]를 누르면 보내지 않고 창을 닫는다. 목록 위 줄의 [다시 불러오기]는 보드대로 창이 닫혀 있을 때 쓴다. +- **켜기 스위치**: 이름은 "‘…’ 필터 사용" 하나 — 켜짐 · 꺼짐은 `aria-checked` 가 알린다(보드는 이름을 끄기 · 켜기로 바꿨다). 그림은 36×20, 누르는 곳은 40×44(밀도 예외). +- **누를 수 없는 단추는 `aria-disabled`**(누른 단추 · 창이 돌아올 단추가 초점을 잃지 않게): 맨 위 ▲ · 맨 아래 ▼, 저장 중의 스위치 · ▲▼, 창의 [저장] · [이 필터 지우기], + 50개일 때의 [새 필터](까닭 글을 `aria-describedby`). 마우스를 올려도 모양이 바뀌지 않는다. + 메뉴의 꺼진 항목도 모두 그렇다 — 편집기 메뉴 · 이동 · 메일함 ⋯ · 읽기 화면 ⋯ 에서 화살표로 가서 초점은 받되 고를 수 없다(WAI-ARIA 메뉴). 이름 아래 까닭 글은 `aria-describedby`. +- **저장은 한 번에 하나**: 저장이 도는 동안 누른 다른 행의 스위치 · ▲▼ 는 보내지 않는다(모양은 그대로, 커서만 바뀐다). + 창으로 저장할 때 켜짐은 그 순간 목록의 값이다 — 창은 켜기를 고치지 않으므로, 창을 연 뒤 실패해 되돌려진 켜기가 창의 저장으로 살아나지 않게. +- **앞뒤 공백**: 이름 · 찾을 글 · 전달 주소는 앞뒤 공백을 빼고 보낸다(서버도 뺀 값으로 저장한다). +- **문구**(보드에 없던 것): "내 주소로는 전달할 수 없습니다." · "전달할 주소를 입력해 주세요." · "전달할 주소 형식이 올바르지 않습니다." + (@ 앞은 RFC 5322 dot-atom · 64자 — 서버의 검사보다 좁게 봐서, 화면이 통과시킨 주소를 서버가 거절하지 않게). 길이 · 제어 문자 · 이름 오류는 사전의 `VAL_001` 문구. + 이름 없는 필터의 이름(조건으로 만든 것)은 알림 · 단추 이름에서 50자로 자르고 "…"를 붙인다. 작은따옴표가 든 이름은 큰따옴표로 묶는다. +- **읽기 화면 ⋯**: 50개인지 알려고 메뉴를 열 때 필터를 알림 없이 받는다(받아 둔 것이 없거나 50개일 때). 결과가 오기 전에는 항목을 켜 둔다. + 50개면 항목을 끄고 이름 아래에 까닭 한 줄. 보낸 사람 주소가 없는 메일에는 항목이 없다. `?from=` 으로 왔는데 50개면 창을 열지 않고 주소의 쿼리만 지운다. +- **화면 폭**: 공용 설정 틀 그대로 최대 800 · 묶음 사이 22(보드는 880 · 18 — 다른 설정 화면과 맞춘다). 머리의 [새 필터]는 틀의 `actions` 자리. 안내 상자 700, 창 620. +- **메일함**: 받아 둔 메일함 목록을 보이며 뒤에서 다시 받는다(다른 탭에서 지웠을 수 있다). 요약에 메일함 이름이 들어가 필터와 메일함을 둘 다 받은 뒤 목록을 그린다. + 옮길 곳 고르기는 받은편지함 → 내 메일함 → **보관함** → 스팸메일함 → 휴지통(보관함과 그 아래 — 보드에 없었다. 서버가 받고, mailcow 에서 옮겨 온 계정에 있다). + 지워진 메일함은 "(지워진 메일함)…"이 꺼진 채 골라져 보이고, 그대로 저장해도 막지 않는다(서버가 받는다 — 받은편지함에 들어온다). +- **초점**: 조건 더하기 → 새 줄의 "어디에서", 조건 빼기 → 이웃 줄의 찾을 글, 필터 지우기 → 다음 행(마지막이면 앞 행), + 목록 위 [다시 불러오기] · [다시 켜기] · `?from=` 으로 연 창의 [취소] · 이미 없는 필터 지우기 → 머리의 [새 필터], 창 안 [다시 불러오기] → [저장]. +- **그 밖**: [다시 켜기]가 성공해도 알림은 없다(줄이 사라진다). 목록 아래 줄은 보드 글 그대로 "… 행을 누르면 고칩니다."까지. + 전달 칸은 `type=email` · 254자이고, 보기 글(받는 사람 칸 · 전달 칸)은 로그인한 직원 주소의 도메인으로 만든다. + 저장하는 사이 화면을 떠나면 칸 안에 보일 거절을 알림으로 띄운다(부재중 화면과 같다). 목록에 `aria-busy` 를 걸지 않는다(칸마다 `aria-disabled`). +- **하지 않은 것**: 고치기 창에 쓰다 만 채로 떠날 때 묻기(나가기 확인) — 보드에 없다. 창을 연 채 브라우저 뒤로 가기를 하면 쓴 것을 잃는다(알려진 한계). diff --git a/docs/design/webmail/SettingsFilter.dc.html b/docs/design/webmail/SettingsFilter.dc.html new file mode 100644 index 0000000..3ed4216 --- /dev/null +++ b/docs/design/webmail/SettingsFilter.dc.html @@ -0,0 +1,447 @@ + + + + +설정 · 필터 + + + + + + + +
+ + + +
+ +
+ 설정 +
+ + +
+
+ +
+ 김아라 + ara.kim@arasystem.it.kr +
+ + + 로그아웃 + +
+
+ +
+ + + +
+
+ +
+
+

필터

+

받은 메일을 조건에 따라 메일함으로 옮기거나, 읽음 · 중요로 표시하거나, 동료에게 전달합니다. 새로 오는 메일에만 적용됩니다.

+
+ +
+ +
+
    + +
  1. + {{ r.no }} + + +
    + + +
    +
  2. +
    +
+

필터 {{ count }}개 · 50개까지 만들 수 있습니다. 행을 누르면 고칩니다.

+
+ +
+

위에서부터 차례로 적용합니다. 메일함으로 옮기는 필터에 맞으면 그 아래 필터는 적용하지 않습니다 — 순서는 ▲ ▼ 로 바꿉니다.

+

스팸으로 판정된 메일에는 필터를 적용하지 않습니다 — 그대로 스팸메일함으로 갑니다.

+

전달은 사내 주소로만 할 수 있고, 메일 한 통은 한 곳으로만 전달됩니다(위의 필터가 먼저). 전달해도 내 메일함에 그대로 남습니다.

+

부재중 자동 응답은 필터와 함께 동작합니다.

+
+ +
+
+
+
+ + +
+
+

{{ dialogTitle }}

+ +
+ + +
+ +
+ 어떤 메일에 + + +
    + +
  • + + + + +
  • +
    +
+
+ + 5개까지 · 글자는 대소문자를 가리지 않습니다 +
+
+ +

비어 있는 조건이 있습니다. 찾을 글을 적거나 그 조건을 지워 주세요.

+
+
+ +
+ 할 일 +
+ + +
+

옮기면 이 아래 필터는 적용하지 않습니다.

+ + +
+ + +
+

{{ fwdHelp }}

+ +

할 일을 하나 이상 골라 주세요.

+
+
+ +

새로 오는 메일에만 적용됩니다 — 이미 받은 메일은 그대로입니다. 스팸으로 판정된 메일에는 적용하지 않습니다.

+ +
+ + + +
+ + +
+
+
+
+
+ + +
+ + {{ toastText }} + + + +
+
+
+
+ + + diff --git a/docs/design/webmail/canvas.json b/docs/design/webmail/canvas.json index b657ac8..5273b6e 100644 --- a/docs/design/webmail/canvas.json +++ b/docs/design/webmail/canvas.json @@ -22,15 +22,18 @@ "MailboxManage.dc.html": { "x": 3040, "y": 6640, "w": 1440, "h": 900, "title": "메일함 관리 · 휴지통 비우기", "expand": "fill", "is_interactive": true }, "MailActions4.dc.html": { "x": 4560, "y": 6640, "w": 1440, "h": 1320, "title": "스팸 · 원문 · 오래 걸리는 처리 상태" }, "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": "실시간 새 메일 · 바탕화면 알림 상태" } + "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": "메일 필터 상태 · 메일에서 바로 만들기" } }, - "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"], + "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"], "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 } + "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 } }, "designSystems": [] } diff --git a/docs/mail/api-contract.md b/docs/mail/api-contract.md index 0c44de7..b0a1565 100644 --- a/docs/mail/api-contract.md +++ b/docs/mail/api-contract.md @@ -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` `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` `MAIL_010` `MAIL_011` `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` `MAIL_010` `MAIL_011` `MAIL_012` `MAIL_013` `SYS_001` - **적용 범위: 1단계는 웹(브라우저) 전용이다.** 인증은 HttpOnly 쿠키뿐이고 토큰을 본문으로 주지 않는다. 모바일 앱(Flutter)의 인증 방식(토큰 발급·`flutter_secure_storage`)은 앱 착수 때 이 문서에 추가한다. - **배포 형태: 화면과 API 는 같은 주소(origin)를 쓴다.** 운영은 프론트 nginx 가 `/api` 를 백엔드로 넘기고, @@ -209,7 +209,9 @@ HTML 은 정화하기 **전에** 길이(HTML 부분이 여럿이면 이어 붙 | `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` | +| `PUT /api/mail-vacation` | `MailVacation` (전체 교체) | 200 `MailVacation`(저장한 값) | 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` | | `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` | @@ -244,6 +246,13 @@ HTML 은 정화하기 **전에** 길이(HTML 부분이 여럿이면 이어 붙 | 첨부 20개 초과 | 400 `MAIL_006` — 클라이언트는 첨부를 추가할 때 미리 막는다 | | 본문이 너무 김 — 텍스트 100,000자·서식 본문 500,000자 초과(JS 문자열 길이, UTF-16 단위), 서식 본문이 정화 전 비용 한도·정화 뒤 2,000,000바이트 초과 | 400 `MAIL_007` — 메일은 나가지 않는다. 클라이언트는 보내기 전에 같은 한도로 막고 본문 아래에 알린다 (아래 "서식 본문"의 한도) | | JSON 본문 1MB 초과 (발송 `POST /api/messages` 와 임시 저장 `POST /api/drafts`·`PUT /api/drafts/{id}` 만 4MB — 서식 본문과 긴 인용 때문) | 413 `FILE_002` | +| JSON 본문의 모양이 한도를 넘음 — 배열 하나 1,000개 · 값(객체 · 배열 · 원시 값) 10,000개 · 깊이 32단 | 400 `VAL_001` | +| JSON 본문이 UTF-8 이 아님 — `charset` 이 `utf-16` · `utf-32` · `utf-7` 등 (`charset` 을 붙이지 않거나 `utf-8` 이면 된다) | 400 `VAL_001` | + +**JSON 본문 모양 한도 (2026-10-28 — 6단계 리뷰에서, 전부터 있던 경로도 같은 원인)**: 본문 크기 한도 안에서도 작은 객체 수만~수십만 개면 DTO 변환 · 검사가 몇 초씩 서버를 멈추고 수백 MB 를 썼다 +(리뷰 측정 — 1MB 에 필터 규칙 8만 개 7.8초, 조건 15만 개는 500). 그래서 모든 JSON 본문을 DTO 에 닿기 전에 본다 — 읽기 전에 글자를 한 번 훑고, 읽은 뒤 한 번 더. +한도는 지금 계약의 어떤 본문보다 넉넉하다(가장 긴 배열 100 · 가장 깊은 본문 5단 · 값이 가장 많은 필터 50개 저장이 약 1,600개). +이 검사와 UTF-8 검사는 본문 크기 한도처럼 인증보다 앞이라, 로그인하지 않은 요청도 401 이 아니라 400 이다. JSON 은 UTF-8 만 받는다(RFC 8259 8.1 — 웹 · 앱 모두 UTF-8 로 보낸다). 메일함 정렬(확정 디자인 기준): 받은편지함 → 보낸편지함 → 임시보관함 → 스팸메일함 → 휴지통 → 보관함 → 그 외(`sortOrder`, 이름 순). @@ -683,10 +692,17 @@ interface MailVacation { - 한 번도 설정하지 않은 계정은 `{ isEnabled: false, fromDate: null, toDate: null, subject: "", textBody: "" }` 로 온다(엔진의 null 은 빈 문자열로). 끈 채로는 빈 제목 · 내용도 저장된다. 제목 · 내용은 쓴 그대로 저장한다 — "공백이 아닌 글자" 검사만 앞뒤 공백을 뺀 값(JS `trim()` — NBSP · U+3000 포함)으로 한다. 제어 문자 검사는 꺼 둔 채로도 한다. -- 엔진의 JMAP `VacationResponse`(singleton)를 그대로 쓴다 — `htmlBody` 는 비운다(null). 저장은 전체 교체다. 꺼도 쓴 제목 · 내용은 남겨 둔다. +- 저장 자리 — 6단계부터 메일 필터와 같은 엔진 규칙(Sieve 스크립트 `arasystem`)에 둔다(아래 "메일 필터"의 "저장 자리"). 4단계에는 엔진의 JMAP `VacationResponse`(singleton)였다. + 서식 본문은 없다(글자만). 저장은 전체 교체이고 응답은 저장한 값이다. 꺼도 쓴 제목 · 내용은 남겨 둔다. + 필터와 한 스크립트라서 부재중을 저장해도 스크립트를 다시 켠다(필터가 꺼져 있었으면 함께 다시 동작한다). 필터 규칙은 건드리지 않는다. + - 6단계부터 바뀐 동작: 엔진이 스팸으로 분류한 메일에는 답하지 않는다(필터와 같은 스팸 막기 — "메일 필터"의 "만드는 Sieve"). + - 우리 스크립트가 엔진에서 꺼져 있으면(필터의 `isActive: false`) 부재중도 동작하지 않는다. 이 API 의 `isEnabled` 는 저장한 값이라 그때도 `true` 로 온다 + (알려진 한계 — 드묾. 화면은 필터 화면의 줄로만 알린다). + - 저장해 둔 원본이 깨진 계정은 꺼짐(`isEnabled: false`, 빈 제목 · 내용)으로 온다. 원본이 이 서버보다 새 판이면 저장은 503 `MAIL_001` 이다("메일 필터"의 "저장 자리"). - `fromDate` · `toDate` 는 `Z` 로 끝나는 UTC 시각이고, 두 키는 늘 보낸다(값은 null 가능). 엔진에는 초 단위로 저장한다(소수 초는 버린다). 둘 다 있으면 버린 값으로 `fromDate < toDate`(아니면 400 `VAL_001`). 화면은 날짜만 고르고, 브라우저 시간대의 시작일 0시 · 끝나는 날 다음 날 0시를 UTC 로 보낸다. - 같은 사람에게 매번 답하지는 않는다 — JMAP 규격(RFC 8621 8절)이 "보낸 사람마다 적당한 기간에 한 번" 을 요구하고 엔진이 따른다. + 6단계부터는 Sieve `vacation`(RFC 5230)이 같은 일을 한다 — 간격은 엔진 기본값이다. - **클라이언트**: 설정 › 메일 › 부재중 자동 응답. 켜져 있으면 메일 화면의 메일함 패널 아래(사용량 위)에 "부재중 자동 응답 켜짐 · 11월 3일까지"(누르면 설정)를 보인다 — 메일 화면에 들어올 때 한 번 받고, 시간이 지나면 다시 계산한다(끝나는 시각이 지나면 숨기고, 시작 전이면 "10월 30일 ~ 11월 3일 · 설정"). @@ -778,6 +794,93 @@ data: {"reason":"token-expiring"} 같은 tag 로 앞 알림을 바꿔 쓴다(쌓이지 않는다). 누르면 그 탭으로 와서 메일을 연다(여러 통이면 받은편지함). 사용자가 웹메일로 돌아오면(어느 탭이든 보이고 초점이 생기면) 띄운 알림을 닫는다. 허락은 사용자가 누른 뒤에만 묻는다(목록 위 한 번 권하기 · 설정의 켜기 칸). `new Notification()` 을 막는 브라우저(휴대폰 Chrome)는 처음 실패할 때 "지원하지 않음"으로 바꾼다. +### 메일 필터 (`/api/mail-filters`) — 6단계 + +```ts +interface MailFilterSet { + rules: MailFilterRule[] // 위에서부터 차례로 적용한다 — 50개까지 + version: string // 지금 규칙의 지문 — 해석하지 말고 PUT 에 그대로 돌려보낸다(그사이 다른 곳에서 규칙이 바뀌었으면 409 MAIL_012). 영문 · 숫자 · - · _ 128자까지. 규칙만 본다(부재중이 바뀌어도 그대로) + isActive: boolean // false = 우리 규칙이 엔진에서 꺼져 있거나(다른 규칙이 켜짐) 저장해 둔 원본을 읽을 수 없어 지금은 필터 · 부재중 응답이 동작하지 않는다(드묾) — 저장하면 다시 켜진다 + // (아래 "저장 자리"의 새 판 원본은 예외). 한 번도 저장하지 않은 계정은 true(막힌 것이 없다) +} +interface MailFilterRule { + id: string // 클라이언트가 만든다(UUID — 영문 · 숫자 · - · _ 64자까지) — 목록 안에서 겹치지 않게 + name: string // 0~50자(앞뒤 공백은 지워 저장한다), 제어 문자는 안 된다 — 비면 화면이 조건으로 이름을 보인다 + isEnabled: boolean + match: 'all' | 'any' | 'always' // 조건이 모두 맞음 · 하나라도 맞음 · 모든 메일(조건 없음) + conditions: MailFilterCondition[] // all · any 는 1~5개, always 는 빈 배열 + actions: MailFilterActions // 하나 이상 +} +interface MailFilterCondition { + field: 'from' | 'to' | 'subject' | 'body' // 보낸 사람(이름 · 주소) · 받는 사람(받는 사람 · 참조의 이름 · 주소) · 제목 · 본문(글자 부분) + operator: 'contains' | 'notContains' + value: string // 1~200자(앞뒤 공백을 지운 값 — 그 값으로 저장한다), 제어 문자 · `${` 는 안 된다 +} +interface MailFilterActions { + moveToMailboxId: string | null // 받은편지함 · 내 메일함 · 보관함 · 스팸메일함 · 휴지통 (보낸편지함 · 임시보관함은 400 VAL_001). 없는 id 도 받는다 — 지운 메일함을 가리키는 규칙이 남아 있을 수 있다 + markAsRead: boolean + flag: boolean + forwardTo: string | null // 사내 주소(도메인이 `MAIL_DOMAIN` 과 똑같은 주소 — 하위 도메인은 안 된다)만, 자기 주소는 안 된다(대소문자 무시). + // 앞뒤 공백은 지워 저장한다. `${` 는 안 된다 +} +``` + +- PUT 본문은 `{ rules, version }` 뿐이다 — `isActive` 같은 다른 키를 보내면 400 `VAL_001`(모르는 필드). 응답은 저장한 규칙(앞뒤 공백을 지운 값)과 새 `version` 이다. + +- **적용 순서** — 새로 온 메일을 위의 규칙부터 차례로 견주고, 맞는 규칙의 할 일을 모두 한다(읽음 · 중요는 겹쳐 붙는다). + **메일함으로 옮기는 규칙에 맞으면 거기서 멈춘다** — 아래 규칙은 보지 않는다. 받은편지함으로 옮기는 규칙을 위에 두면 "늘 받은편지함에"가 된다. + 전달은 한 통에 한 곳 — 엔진 한도(사용자 스크립트의 리다이렉트 1)라 두 번째 전달은 엔진이 조용히 버린다(2026-10-28 확인). 사본은 늘 남긴다(`redirect :copy`). + **스팸으로 판정된 메일에는 아무 규칙도 적용하지 않는다**(부재중 응답도 하지 않는다) — 엔진은 규칙의 옮기기가 스팸메일함 분류를 이기므로(같은 날 확인) 막는다. + 판정은 엔진의 분류 그대로다(지금 엔진 설정은 5점부터 스팸 — 아래 "만드는 Sieve"의 스팸 막기). + 꺼 둔 규칙은 목록에만 있고 적용하지 않는다. 이미 받은 메일에는 적용하지 않는다. +- **저장 자리** — 엔진의 Sieve 스크립트 하나(`arasystem`)에 필터와 **부재중 자동 응답을 함께** 둔다. Stalwart 0.16.27 은 계정마다 켜진 스크립트가 하나뿐이고 + 엔진의 VacationResponse 도 스크립트(`vacation`)라서, 부재중을 켜면 필터가 꺼지고 필터를 켜면 부재중이 꺼진다(같은 날 확인). + - 4단계 `/api/mail-vacation` 은 계약 그대로 두고 저장 자리만 이 스크립트로 옮긴다 — 엔진의 VacationResponse 는 더 쓰지 않는다. + 스크립트가 아직 없는 계정은 처음 읽을 때 VacationResponse 의 값을 가져온다(개발 데이터 이어받기). 엔진의 `vacation` 스크립트는 고칠 수 없다(엔진이 `forbidden`). + - 스크립트 머리의 주석에 규칙 · 부재중 원본(JSON — 판 번호 `v` 를 함께 둔다)을 두고, 읽을 때는 그것만 믿는다(엔진의 Sieve 를 다시 해석하지 않는다). + - 스크립트가 아직 없으면 빈 규칙 + `isActive: true`, 부재중은 엔진의 VacationResponse 값(위 이어받기). + - 스크립트는 있는데 원본이 없거나 깨졌으면 빈 규칙 + `isActive: false`, 부재중은 꺼짐 — 저장하면(필터든 부재중이든) 덮어쓴다. + - 원본의 판이 이 서버가 아는 것보다 새로우면(새 서버를 배포했다 되돌린 경우) 읽기는 위와 같지만 **쓰기는 거절한다**(503 `MAIL_001`) — 옛 서버가 새 판의 규칙을 지우지 않게. + - 쓸 때는 읽은 상태 그대로일 때만(`ifInState`) 쓰고 켠다(`onSuccessActivateScript`). 그사이 바뀌면 다시 읽어 확인한다 + (쓰기 3번까지 — 시도마다 읽는 사이 글이 사라지면 그 읽기도 3번까지 다시 한다. 그래도 어긋나면 503 `MAIL_001`). + 필터 저장은 다시 읽은 규칙의 지문이 `version` 과 다르면 409 `MAIL_012`(부재중 응답만 바뀐 것은 걸리지 않는다). 부재중 저장은 규칙을 그대로 두고 부재중 부분만 바꾼다. + 다만 저장해 둔 규칙이 보내는 규칙과 이미 같으면 지문이 달라도 200 으로 저장한다 — 결과를 모르는 실패 뒤에 같은 요청을 다시 보낸 경우가 409 로 끝나지 않게 + (꺼져 있었으면 이때 다시 켜진다). +- **만드는 Sieve** (엔진 확인 사실 — 2026-10-28): + - 조건: `from` → `header :contains "from"`, `to` → `header :contains ["to", "cc"]`, `subject` → `header :contains "subject"`, `body` → `body :text :contains`. + 한국어 제목(인코딩된 머리글) · 본문 · 표시 이름 모두 맞는다. 대소문자는 ASCII 만 무시한다(엔진 기본 비교 — `i;unicode-casemap` 은 없다). + - 한 규칙은 `if` 하나: 표시(`addflag` — 옮기기보다 먼저, 엔진은 옮기는 순간의 표시만 붙인다) → 전달 → 옮기기(`fileinto :mailboxid "" "INBOX"` + `stop`). + 옮길 메일함이 없으면 대체 이름 "INBOX" 로 받은편지함에 들어온다. + - 스팸 막기는 엔진의 분류 표시로 한다 — `X-Spam-Score` 머리글(`spam, score=9.70` · `ham, score=2.20`)이 `spam` 으로 시작하면 아무 규칙도 · 부재중도 하지 않는다 + (`header :matches "X-Spam-Score" "spam*"`). 엔진이 우리 스크립트를 돌리기 **전에** 붙인다. `X-Spam-Status: Yes|No` 는 스크립트가 끝난 뒤 실제로 들어간 곳에 따라 붙어 쓸 수 없다 + (우리 규칙이 옮긴 스팸은 "No" 였다). 엔진 설정의 기준 점수(`scoreSpam` — 지금 5)를 바꿔도 이 표시가 따라간다. + - `spamtest` 는 쓰지 않는다 — 엔진의 값은 "스팸이면 10"이 아니라 점수를 정수로 내린 값(10에서 멈춤)이라, 10 으로 막으면 5~10점 스팸을 놓친다 + (2026-10-28 실엔진: 2.20점 → 2, 9.70점 → 10 미만, 11.2점 → 10. 처음 구현은 10 으로 막아 9.70점 스팸을 규칙대로 옮기고 부재중 응답까지 보냈다). + - 바깥에서 같은 이름의 머리글을 넣어 와도 엔진의 머리글이 함께 남는다(하나라도 맞으면 참). 그래서 위조로 스팸을 숨길 수는 없고, 자기 메일에 규칙이 적용되지 않게만 할 수 있다 + (그 메일은 엔진 판정대로 들어간다). 엔진의 스팸 필터는 바깥에서 오는(인증 없는) 메일에만 돌아 사내 메일에는 이 머리글이 없다 — 규칙이 그대로 적용된다. + - 부재중: 엔진이 하던 대로 `vacation :mime`(본문 base64) — 제목은 RFC 2047 로 인코딩해 넣는다. Stalwart 는 require 와 상관없이 문자열의 `${…}` 를 늘 변수로 펼쳐서 + 그대로 넣으면 지워진다(같은 날 확인). 기간은 `currentdate :zone "+0000"`(엔진 시간대에 기대지 않게 — 같은 날 확인). 같은 사람에게 다시 답하는 간격은 엔진 기본값(지금과 같다). + - 규칙 값에 `${` 는 받지 않는다(400 `VAL_001`) — 펼쳐져 빈 값이 되면 `:contains` 가 모든 메일에 맞는다. 문자열은 `"` · `\` 를 이스케이프한다. + `%{…}`(Stalwart 의 식 모양)는 펼치지 않아 막지 않는다 — `%{"a"}` · `%{}` · `%{1+1}` · `%{subject}` 모두 글자 그대로 견주었다(같은 날 확인). + - 크기: 엔진 한도는 스크립트 102,400바이트 · 문자열 4,096바이트다. 만든 스크립트가 90,000바이트를 넘으면 저장하지 않고 400 `MAIL_013`. +- 로그 — 규칙의 글(조건 값 · 전달 주소)은 남기지 않는다(검색어처럼 직원의 내용이다). 규칙 수 · 크기 · 결과만. + 엔진이 스크립트를 거절하면(`invalidScript` → 500 `SYS_001`) 엔진 설명의 줄 · 칸만 남긴다 — 설명 문장에는 만난 문자열(규칙의 글)이 그대로 들어갈 수 있다. +- **클라이언트**: 설정 › 메일 › 필터(`/settings/mail/filters`, 탭 제목 "메일 필터") — 메뉴는 서명 · 부재중 자동 응답 · 필터 · 알림. + - 켜기 · 끄기 · 순서 바꾸기 · 지우기 · 고치기 모두 `PUT` 전체 교체(받은 `version` 으로). 켜기 · 순서는 화면을 먼저 바꾸고 실패하면 되돌린다(오류 알림). + 지우기는 확인 없이 하고 알림의 [되돌리기](10초)로 지운 필터를 **지운 자리에 다시 넣어** 저장한다 — 그사이 바꾼 다른 필터(켜기 · 순서 등)는 그대로 둔다. + 저장은 한 번에 하나다 — 앞 저장이 도는 동안 누른 [되돌리기]는 차례로 보낸다. 고치기 창의 [저장]이 끝나면 "필터를 저장했습니다." + - `MAIL_012` — 목록 위 줄 "다른 곳에서 필터가 바뀌어 저장하지 않았습니다. 다시 불러온 뒤 고쳐 주세요." + [다시 불러오기]. 고치던 창은 닫지 않는다 — + 창 안에도 같은 안내와 [다시 불러오기]를 둔다(모달이라 뒤의 줄을 누를 수 없다). 창에서 다시 불러오면 쓴 내용은 그대로이고, 다음 [저장]은 다시 받은 목록에 + 그 필터를 넣는다(같은 id 자리 — 그사이 다른 곳에서 지워졌으면 맨 아래). + - `isActive: false` — 목록 위 줄 "지금은 필터와 부재중 자동 응답이 동작하지 않습니다…" + [다시 켜기](지금 목록 그대로 저장). + - 읽기 화면 ⋯ 「이런 메일로 필터 만들기」 → `/settings/mail/filters?from=<보낸 사람 주소>` — 새 필터 창이 "보낸 사람 · 포함 · 그 주소"로 채워져 열린다(초점은 할 일의 첫 칸). + 50개를 다 만들었는지 알려고 ⋯ 메뉴를 열 때 `GET` 을 한다(받아 둔 목록이 없거나 50개일 때만 — 알림 없이). + - 검사는 서버와 같은 규칙(이름 50 · 값 200 · 조건 5 · 규칙 50 · `${` · 제어 문자 · 할 일 하나 이상 · 전달은 사내 주소이고 자기 주소 아님) — 칸 아래에만 알린다. + 전달 주소의 앞부분은 서버(`IsEmail`)보다 좁게 본다(RFC 5322 dot-atom · 64자) — 화면이 통과시킨 주소를 서버가 거절하지 않게. + 규칙이 50개면 [새 필터] · 「이런 메일로 필터 만들기」를 끄고 까닭을 보인다. 지운 메일함을 가리키는 규칙은 목록에 빨간 안내. +- 이 단계에서 하지 않는 것: 이미 받은 메일에 적용, **바깥 주소로 전달**(엔진에 SRS 가 없어 Gmail 등이 DMARC 로 거절할 수 있고, 회사 메일이 개인 메일로 빠지는 통로다 — + 필요해지면 정책과 함께), 크기 · 첨부 · 날짜 조건, 정규식, 맞는 메일 미리 보기, mailcow(SOGo)의 필터 옮겨 오기(운영 전환 때 — imapsync 로는 옮겨지지 않는다). + ### 이 단계에서 하지 않는 것 탭을 모두 닫아도 오는 알림(웹 푸시 — 엔진은 `webpush-vapid` 를 지원한다), 알림 소리(운영체제 설정을 따른다), 휴대폰 푸시, 본문 안 이미지(인라인 cid — 붙여 넣은 이미지는 첨부로, 서명 그림만 예외), 외부 이미지 보기(이미지 중계),