Confluye
Features

Mailer

Inbox threads, signed inbound delivery, Gmail/Outlook polling, replies, and attachments.

The Confluye Mailer stores inbound mail as threads and messages, then queues workflow or Command Center delivery through a durable trigger outbox. Authenticated Gmail and Outlook routes can poll an inbox and send replies. Generic inbound routes accept a webhook payload instead of provider polling.

Inbox model

A route maps one mailbox address to a destination (workflow, command, or both). Routes also carry enabled, principalEnabled, and integrationEnabled. Delivery and polling require all three to be on.

Routes can also belong to a workflow destination through executionTargetId. The same address can have separate routes for Production and named previews. Each destination must be published and on before it accepts mail. Use the route-specific signed inbound URL for a preview; address-only inbound lookup retains the legacy Production route. Creating a preview does not copy routes.

To configure one in the editor, select the destination and open Manage versions → Add event source → Signed inbound email. Save the returned URL and one-time signing secret in your provider. This creates an inbound route, not an SMTP mailbox. Adding a route to an on destination can immediately trigger its currently approved version.

A thread is keyed by workspace, route, provider, and externalThreadId. A message is keyed by route, provider, and externalMessageId. Re-ingesting the same provider identity returns the existing message with deduplicated: true and does not create a second row.

Public message serialization drops triggerRequired and any _triggerOutbox metadata. HTML is stored as sanitizedHtml after sanitizeMailHtml strips script/style/iframe/form/svg/math blocks, event handlers, javascript: / vbscript: / data: URLs, and inline style.

<Note>Inbox routing is treated as on when the workspace has at least one enabled route (isMailerInboxEnabled).</Note>

fluxus.local addresses

The UI helper defaultMailerAddress builds a stable local address from the workspace id:

inbound@<slug>.fluxus.local

The slug is the workspace id without a leading ws_, lowercased, with non-alphanumeric characters turned into hyphens. Empty ids become inbound@workspace.fluxus.local. Example: ws_testinbound@test.fluxus.local. This is a local compatibility address, not an SMTP hostname Confluye listens on.

Signed inbound uses the legacy compatibility headers x-fluxus-timestamp and x-fluxus-signature.

Two inbound paths

Confluye does not run an SMTP server.

Generic inbound (POST /api/v1/mailer/inbound, workspace API key) looks up an inbound route by to address. Required fields: to, from, subject. Optional: text, html, externalMessageId, externalThreadId. Missing message/thread ids are generated as inbound:<uuid>. The recipient must match the route address. Gmail/Outlook routes reject this path.

Signed inbound (POST /api/v1/mailer/inbound/{routeId}) is unauthenticated until HMAC verification. The route must be provider: inbound and enabled. Headers:

HeaderFormat
x-fluxus-timestampUnix seconds, 9–12 digits
x-fluxus-signaturev1= plus 64 hex chars of HMAC-SHA256

The HMAC is HMAC-SHA256(secret, "{timestamp}." + rawBody). Timestamps older or newer than 5 minutes are rejected. The raw body is capped at 1 MiB. The JSON body must include externalMessageId, from, and subject. Do not send workspaceId or routeId; those come from the URL. Optional to must match the route address.

Replay protection claims timestamp + signature for the signature window. A completed claim returns 409. A concurrent in-progress claim also returns 409. After a successful ingest the reservation is marked completed; a failed ingest releases it so a later signed request can retry.

Route creation returns signingSecret once. The store keeps only a hash and an encrypted copy (APP_ENCRYPTION_KEY in production). PATCH with rotateSigningSecret: true issues a new secret and immediately invalidates the old one. Creation and rotation require Idempotency-Key and replay the original secret for the same key.

Both paths call runInboundMail, persist the message, enqueue the outbox, and return 202 with route, message, run: null, outbox[], and deduplicated. A first delivery writes mailer.inbound.queued. A duplicate externalMessageId returns the same message with deduplicated: true and does not write a second audit event.

The in-app inbound test helper treats a durable outbox (no immediate WorkflowRun) as queued delivery. A deduplicated payload reports that the email was already queued.

Gmail and Outlook polling

Polling groups (gmail or outlook) share a credential and optional Google DWD config with matching routes. Default interval is 300 seconds; allowed range is 60–86400. The platform scheduler (pnpm worker:scheduler, default SCHEDULER_POLL_INTERVAL_MS=60000) claims due groups (30-minute lease, concurrency 4) and polls them.

Matching routes share one provider query, cursor and attachment download. Each enabled destination receives its own message and workflow execution. When no route has an enabled destination, the group is excluded from due polling. The destination panel and publication check warn when sibling destinations share a mailbox; keeping both on can perform the same action twice.

Gmail uses History after a profile historyId bootstrap, scoped to INBOX. Messages labeled SENT, SPAM, TRASH, or DRAFT are skipped. A 404 History response restarts bootstrap. Outlook uses Inbox delta; a cursor that is not an Inbox delta URL is discarded.

A polled message is ingested only when a recipient equals a route address (case-insensitive). The cursor advances only after the batch is processed. Transient attachment failures leave the cursor unchanged so the next poll retries. Permanent attachment rejections dead-letter the message: it is stored without attachments, triggerRequired is false, and providerMetadata.attachmentProcessing records { status: "rejected", reason }.

Accepted target deliveries retain their original activation across retries and provider redelivery. Deleting a workflow retains its stored mail history; its historical route does not become a Production route for another workflow.

OAuth refreshes persist with compare-and-swap. DWD access tokens are not persisted as credentials.

Replies and reservations

POST /api/v1/mailer/messages/{id}/reply requires Idempotency-Key and a text or html body. Only inbound messages on enabled Gmail/Outlook routes with a credential can be replied to. Subject becomes Re: … unless it already starts with Re:. Gmail sets In-Reply-To / References from providerMetadata.messageIdHeader when present.

The store reserves the key before calling the provider. A SHA-256 fingerprint covers message id, text, sanitized HTML, and sorted attachment file ids.

Reservation resultHTTPMeaning
sent (same fingerprint)200 { deduplicated: true }Replay the stored outbound message
mismatch409 MAIL_REPLY_IDEMPOTENCY_MISMATCHSame key, different payload; not reconciliable
pending409 MAIL_REPLY_PENDINGUncertain; do not resend
failed409 MAIL_REPLY_FAILEDProvider attempt failed with uncertain delivery

Admin/Owner PATCH /api/v1/mailer/reply-reservations/{id} with { "action": "authorize_retry" } moves a failed or expired-pending reservation to retryable. Only then may the same key be sent again. The reservation lease is two minutes.

Attachments

Inbound polling imports at most 10 attachments and 25 MB total into Workspace Files. Imports are claimed by provider + message + attachment id + content fingerprint so concurrent polls reuse one file. Quarantined or mismatched content never returns a file id for dispatch.

Replies attach ready Workspace Files only (same 10 / 25 MB limits). File ids must already exist in the workspace.

Sending email from workflows

Outbound email is not a built-in mailer block. To send mail from a workflow, use an integration block — for example the Gmail or Outlook blocks, or a generic HTTP request to your provider. (The only place Confluye itself sends mail is transactional authentication email, configured separately with FLUXUS_AUTH_EMAIL_FROM plus either FLUXUS_AUTH_RESEND_API_KEY (preferred) or FLUXUS_AUTH_SMTP_URL.)

Next steps