Files
broccolini-bot/.scratch/forward-command/design.md

3.0 KiB

/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.jshandleForward(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.jsforwardThread() + 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).