docs: design notes for /forward and folder-lifecycle features
This commit is contained in:
54
.scratch/forward-command/design.md
Normal file
54
.scratch/forward-command/design.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# `/forward` — forward a ticket's email thread to a third party
|
||||
|
||||
## Goal
|
||||
Let staff forward a ticket's entire email conversation to any email address from
|
||||
inside the ticket channel. The original customer must **never** be looped in.
|
||||
|
||||
## Command
|
||||
```
|
||||
/forward email:<address> [note:<text>]
|
||||
```
|
||||
- `email` — required destination address; validated with `EMAIL_RE`, header-sanitized.
|
||||
- `note` — optional cover message prepended above the transcript (max 1000 chars).
|
||||
- Staff-only (the command dispatcher gates every command via `requireStaffRole`);
|
||||
`ManageMessages` default member permission, matching other ticket-mod commands.
|
||||
|
||||
## Customer-isolation guarantees (the hard requirement)
|
||||
1. `To:` = target address **only**. No `Cc`, no `Bcc`.
|
||||
2. The Gmail `messages.send` call carries **no `threadId`** → a brand-new thread,
|
||||
not an append to the customer's conversation.
|
||||
3. **No `In-Reply-To` / `References`** headers.
|
||||
4. The customer's `senderEmail` is never written into any recipient header.
|
||||
5. Fresh `Subject: Fwd: <original subject>`.
|
||||
|
||||
## Flow
|
||||
- `handlers/commands/forward.js` → `handleForward(interaction)`:
|
||||
1. `findTicketForChannel` — else ephemeral "not a ticket channel".
|
||||
2. Discord-origin ticket (`gmailThreadId` starts `discord-`) → ephemeral "no email thread".
|
||||
3. `deferReply` ephemerally (fetch + attachment download can exceed 3s).
|
||||
4. `forwardThread(gmailThreadId, email, note, userId)`.
|
||||
5. Ephemeral confirmation: "Forwarded N messages (M attachments) to x@y.com"
|
||||
(+ note if any attachments were skipped for size). `logTicketEvent`.
|
||||
- `services/gmail.js` → new `forwardThread(threadId, targetEmail, note, userId)`:
|
||||
1. Validate target (`EMAIL_RE`); throw `EBADRECIPIENT` if bad.
|
||||
2. `threads.get(format:'full')`; throw `EEMPTY` if no messages.
|
||||
3. Subject from first message → `Fwd: <subject>` (strip existing `Fwd:`), RFC2047-encode.
|
||||
4. Per message oldest→newest: header line (`From:`/`Date:`) + `getCleanBody`; build
|
||||
parallel text and HTML blocks (HTML body `escapeHtml`-ed — customer content).
|
||||
5. Collect attachment parts (recursive walk for `filename` + `body.attachmentId`);
|
||||
download via `messages.attachments.get`. **Cap total ~20 MB**; past it → skip + count.
|
||||
6. Compose a **new** outbound email: `From: support@`, `To: target only`,
|
||||
`multipart/mixed` (attachments) wrapping `multipart/alternative` (text+html).
|
||||
Plain-ASCII divider between messages in text; `<hr>` in HTML.
|
||||
7. `messages.send` (no `threadId`). Return `{ messageCount, attachmentCount, skipped }`.
|
||||
|
||||
## Files
|
||||
- `commands/register.js` — `/forward` builder (after `folder`).
|
||||
- `services/gmail.js` — `forwardThread()` + `collectAttachmentParts()` helper; export;
|
||||
import `getCleanBody` from `utils`.
|
||||
- `handlers/commands/forward.js` — new handler.
|
||||
- `handlers/commands/index.js` — import + `COMMAND_HANDLERS.forward`; `/help` line.
|
||||
|
||||
## Out of scope
|
||||
- Forwarding a single message or letting staff pick one (whole-thread only).
|
||||
- Native RFC822 re-send (we build a fresh email instead).
|
||||
Reference in New Issue
Block a user