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/Referencesheaders, 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.
frommust be the bare address. The gateway/forwarder is responsible for parsing the RFC 5322Fromheader 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
messageIdonto the ticket (service_ticket.email_thread_id). - On an agent's public reply, Floh sends an email with a fresh
Message-ID(persisted toemail_log.message_id), plusIn-Reply-To/Referencespointing 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 rootemail_thread_id, then against a prior outboundemail_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, andticket.email_reply_sentaudit 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_idcolumn (migration20260702171107_email_log_message_id) has adown()for rollback.