Skip to content
Docs
opendesk.shOpen app

Email setup

Inbound forwarding, outbound replies and platform email on a self-hosted opendesk.

opendesk handles three kinds of email, each configured separately.

Kind What Configured by
Inbound, mailbox connections IMAP, Google and Microsoft mailboxes are read directly each mailbox (nothing to set on the server, except OAuth apps)
Inbound, forwarding Mail forwarded to <token>@INBOUND_DOMAIN INBOUND_DOMAIN, plus a receiver below
Outbound replies Replies to customers each mailbox: SMTP, Google or Microsoft
Platform email Verification, password reset, invitations MAIL_DRIVER and friends

However mail comes in, it goes through the same intake: the same loop and sender checks, the same threading and the same routing, whichever mailbox it arrived at.

How email becomes a ticket. Mail arrives by forwarding (the Email Worker, the built-in SMTP receiver or your own pipeline), from Google, from Microsoft or over IMAP. Intake skips loops and automatic mail, checks the sender's DMARC result, threads a reply to its ticket, stores each message once and finds or adds the contact. A reply is added to its ticket and never re-routed. A new conversation is routed to a team and becomes a new ticket, assigned by the team's method.ForwardingEmail Worker, SMTPreceiver or a pipelineGoogleGmail and WorkspaceMicrosoftMicrosoft 365, OutlookIMAPany other providerIntakeskips loops and automatic mailchecks the sender's DMARCthreads a reply to its ticketstores each message oncefinds or adds the contactAdded to its ticketwherever it was sent;a reply is never re-routedRouted to a teamaddress team, rules,then triageNew ticketassigned by the team's methoda replynew
Every way in ends at the same intake, so threading, sender checks and routing work the same for every mailbox.

A new ticket gets its team in this order. Routing rules live in Settings, under Routing.

The order in which a new ticket gets its team. First the place it arrived: the address's default team, or a chat's topic or widget. Then routing rules, in order, where the first match sets the team. Then triage, only while the ticket has no team. Automations run last and may still assign another team.Where it arrivedthe address's team, achat's topic or widget1Routing rulesin order; the firstmatch sets the team2Triageonly while the tickethas no team3Automationsmay still assignanother team4

Forwarding mailboxes get an address <token>@INBOUND_DOMAIN. Choose a domain you control, such as in.desk.example.com, and set:

Terminal window
INBOUND_DOMAIN=in.desk.example.com

Then pick one way to receive that mail.

If the domain is on Cloudflare, Email Routing can receive mail and hand it to a worker that posts it to opendesk. This is how the hosted service works, and it needs no open port 25.

  1. Set a shared secret

    Set INBOUND_SECRET to a long random value (openssl rand -hex 24).

  2. Turn on Email Routing

    In Cloudflare, enable Email Routing for the inbound domain. Cloudflare adds the MX records.

  3. Deploy the worker

    Deploy the worker in deploy/cloudflare/email-worker with wrangler, setting its INBOUND_SECRET secret to the same value and its INBOUND_URL variable to https://desk.example.com/v1/inbound/email.

  4. Route every address to it

    Add a catch-all routing rule that sends every address to the worker.

Set INBOUND_AUTHSERV_ID=mx.cloudflare.net so opendesk believes the sender checks (SPF, DKIM, DMARC) Email Routing writes into each message.

The worker posts each raw message to POST /v1/inbound/email?to=<recipient> with the OD-Inbound-Secret header and the envelope sender in OD-Envelope-From. It retries briefly on network errors and 5xx; if the API stays unreachable, Email Routing treats it as a temporary failure and the sending server retries later, so mail is not lost. Unknown addresses and oversized messages bounce with a short reason.

opendesk can receive mail itself:

  1. Listen for mail

    Set INBOUND_SMTP_ADDR=:25 (or another port with a port forward from 25).

  2. Open port 25

    Open port 25 to the server. Many cloud providers block it by default; check yours.

  3. Point MX at the server

    Add an MX record for the inbound domain pointing at the server:

    in.desk.example.com. MX 10 desk.example.com.

The installer’s --wizard can set this up.

Anything that can make an HTTPS request can deliver mail: post the raw RFC 822 message to /v1/inbound/email?to=<recipient> with OD-Inbound-Secret, and the SMTP envelope sender in OD-Envelope-From (empty for the null sender of a delivery report). See Mailboxes API.

Redelivery is safe: each raw message is stored once per mailbox, keyed by its Message-ID and a hash of its content, so a different message reusing a Message-ID is still received.

A customer is identified by the From address. A message whose From failed DMARC, as reported by a server in INBOUND_AUTHSERV_ID (or by Gmail or Microsoft 365 for those mailboxes), opens a ticket marked spam; on an existing conversation it is kept, flagged for agents, and changes nothing: the ticket is not reopened, and no automation, auto-reply or Ollie answer runs for it. Reply-To never changes who the customer is; replies go to it only when it is at the same domain as From. The one exception is a website contact form the mailbox trusts (Settings, the mailbox, Contact forms): when its message passed DMARC, the visitor in Reply-To is the customer.

How a reply goes out. The agent sends a reply. opendesk picks the address: the one chosen for this reply, else the ticket's reply address, else the brand's address for the ticket's team. The mailbox sends it through SMTP, Gmail or Microsoft from your own address, retrying up to 8 times. The customer receives it from your address, in the same thread, with the signature.Your replywritten in the reply box,sent with Send1The reply addressthe one picked for it,else the ticket's, elsethe team's in the brand2The mailbox sends itSMTP, Gmail or Microsoft,retried up to 8 times3The customerFrom your address, inthe same thread, withthe signature4The message shows its delivery status: pending, sent, failed or bounced.

Replies to customers go out through the customer’s own mailbox: SMTP, Gmail or Microsoft, as set on each mailbox. This keeps replies in the mailbox’s Sent folder, authenticated by the domain’s own SPF, DKIM and DMARC, and from the address the customer wrote to. opendesk does not need to be a trusted sender for your customers’ domains.

Failed sends are retried, up to 8 attempts, and the message shows its delivery status in the ticket.

The system outbound option sends replies through platform email instead. Use it only when a mailbox cannot send for itself.

Verification, password reset and invitation emails come from MAIL_FROM through MAIL_DRIVER:

Driver Use
log Default. Prints emails to the opendesk log. Fine for trying it out; read reset links with opendesk-ctl logs opendesk.
smtp Any SMTP provider: set SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_TLS.
ses Amazon SES in SES_REGION, with AWS credentials from the standard environment (variables, profile or instance role).
Terminal window
MAIL_DRIVER=smtp
MAIL_FROM=opendesk <notifications@desk.example.com>
SMTP_HOST=smtp.postmarkapp.com
SMTP_PORT=587
SMTP_USERNAME=...
SMTP_PASSWORD=...
SMTP_TLS=starttls

Set up SPF and DKIM for the MAIL_FROM domain with your provider so these emails reach inboxes.

Edit this page on GitHub