Skip to content

Email Ticketing connector (inbound email channel)

The email-ticketing connector lets customers open and reply to Floh service tickets by email (LSA-9066). An email gateway (or a small serverless forwarder) parses inbound messages addressed to your support inbox and POSTs a normalized, HMAC-signed JSON payload to Floh's inbound webhook. Floh then:

  • creates a ticket (source = email) for a new message from a known sender,
  • appends a public comment to the matching ticket for a reply, correlating the thread via the In-Reply-To / References headers, and
  • quarantines mail from a sender with no matching Floh user (no ticket is created; an admin is notified).

When an agent posts a public comment on an email-sourced ticket, Floh sends a threaded reply email from the support inbox so the customer's mail client keeps the conversation together. Internal notes never generate an email.

This connector holds configuration only — it exposes no workflow step. The inbound flow is driven entirely by the webhook below.

Configuration

Create an email-ticketing connector in Admin → Connectors and set:

Field Secret Description
inboxAddress no The support inbox mail is forwarded from (e.g. support@example.com). Used as the From on outbound replies so customer replies route back through the gateway.
webhookSecret yes HMAC-SHA256 shared secret. The gateway signs the raw request body with this secret. Encrypted at rest; never logged.
adminNotifyEmail no Address that receives quarantine notifications for unknown senders.

Only one email-ticketing connector should be configured — the outbound reply path resolves the connector by type and uses the earliest-created one.

Inbound webhook

POST /api/email-ticketing/inbound/:connectorId
Content-Type: application/json
X-Webhook-Signature: <hex HMAC-SHA256 of the raw request body>

:connectorId is the connector definition's id (visible in the Admin Connectors screen). The route is CSRF-exempt and authenticates solely via the signature — there is no cookie/bearer requirement.

Signature

Compute HMAC-SHA256(webhookSecret, rawBody) and hex-encode it, where rawBody is the exact JSON string you send:

import { createHmac } from "node:crypto";
const raw = JSON.stringify(payload);
const signature = createHmac("sha256", webhookSecret).update(raw).digest("hex");

A missing or invalid signature returns 401 with no side effect. An unknown, wrong-typed, or misconfigured connector returns 404. A payload that fails schema validation returns 400.

Payload

{
  "messageId": "<CADp...@mail.example.com>", // required — inbound Message-ID
  "from": "customer@example.com", // required — bare sender address
  "subject": "Printer is broken", // required — becomes the ticket title on create
  "body": "It won't turn on.", // required — plain text; sanitized before persist
  "bodyHtml": "<p>It won't turn on.</p>", // optional — used only if `body` is empty
  "inReplyTo": "<root@mail.example.com>", // optional — the Message-ID this replies to
  "references": "<root@...> <prev@...>", // optional — space-separated Message-ID chain
}

Unknown properties are rejected. Attachments are not modeled or persisted in this version.

from must be the bare address. The gateway/forwarder is responsible for parsing the RFC 5322 From header and sending only the address (customer@example.com), not the display-name form ("Customer Name" <customer@example.com>). Floh matches the sender case-insensitively against known users; a mismatched value quarantines the mail. As a defensive fallback Floh unwraps an angle-bracketed address when a display-name form slips through, but forwarders should not rely on it.

Threading

  • On create, Floh stamps the inbound messageId onto the ticket (service_ticket.email_thread_id).
  • On an agent's public reply, Floh sends an email with a fresh Message-ID (persisted to email_log.message_id), plus In-Reply-To/References pointing at the ticket's root thread id.
  • On a customer reply, the gateway forwards In-Reply-To/References. Floh matches each candidate id first against the root email_thread_id, then against a prior outbound email_log.message_id, to find the ticket. If nothing matches, the message opens a new ticket.

Security notes

  • Unknown senders are never auto-provisioned. Mail from an address with no matching Floh user is quarantined; a user must be invited before their mail creates a ticket.
  • Inbound bodies are HTML-sanitized to plain text before persistence.
  • Comment/email bodies are never written to audit metadata. The ticket.email_received, ticket.email_quarantined, and ticket.email_reply_sent audit actions record ids and lengths only (the quarantine event records the sender domain, not the full address).

Operations / rollback

  • Disable the channel by removing the connector row (the webhook then returns 404). No data migration is required to turn it off.
  • The email_log.message_id column (migration 20260702171107_email_log_message_id) has a down() for rollback.