orbotodocs
Admin guideEmail

Inbound email

Four ways to land email in orboto as tickets - pick the one that matches your deployment.

Four ways to land email in orboto as tickets. Pick the one that matches your deployment constraints.

Inbound mail settings

Which path do I want?

PathLatencyDNS workPort 25 neededBest for
Provider webhook (UseSend / Resend / Postmark / SendGrid)secondsnonenoEasiest setup, managed spam handling, needs a third-party email-receive account. UseSend is the EU / DSGVO-clean pick (open-source, self-hostable, AWS SES eu-central-1 underneath).
IMAP pollingup to 5 minnonenoYou already have a mailbox you want orboto to read
Self-hosted SMTPsecondsMX + A + SPF + DMARCyesPer-project addresses, no third party in the loop
Cloudflare Email Routing → webhooksecondsMX (Cloudflare)noSelf-hosted SMTP isn't viable (port 25 blocked) but you still want Cloudflare to receive mail at your domain

All four paths share the same back-end: every message runs the auth-policy gate, the sender allowlist, the known-user check, the project-routing chain (per-project address → tickets+KEY@host regex → semantic embedding vote → catchall → default), AI cleanup, and ticket creation. The only difference between paths is how the message reaches that pipeline.

Provider webhook

Admin → Inbound mail shows a cheat sheet with the webhook URL and the bearer-secret header to paste into your provider's inbound-webhook config

  • generate the secret there first, then wire up Resend / Postmark / SendGrid / UseSend to POST to that URL. The same page holds the sender allowlist, the "require known orboto user" toggle, the SPF/DKIM/DMARC authentication policy, and the default fallback project.

The inbound mail cheat sheet with the webhook URL and bearer secret

Self-hosted SMTP

Bind orboto's built-in SMTP server to port 25 (or any port) and let external MTAs deliver mail directly.

VPS / host requirements

  • Port 25 must be open outbound AND inbound. AWS, GCP, and DigitalOcean block port 25 by default and require a support ticket to lift. Hetzner, OVH, Vultr, and most bare-metal hosts allow it.
  • A static public IPv4 address (or IPv6 with proper PTR).
  • A DNS zone you control for the email domain.

Configure orboto

Admin → Inbound SMTP:

  • Hostname: mail.example.com - what orboto announces in the SMTP banner and what your MX record will point to.
  • Listen host / port: 0.0.0.0 / 25 for production. 127.0.0.1
    • a high port works for local testing.
  • TLS mode:
    • proxy - Caddy or Traefik terminates TLS in front; orboto speaks plaintext on the bound port. Cleanest for Coolify deploys.
    • in_process - orboto terminates TLS itself. Provide cert + key paths; you own renewal.
    • off - no STARTTLS. Only for trusted private networks.
  • Catchall project - fallback project when no per-project address matches and no tickets+KEY alias is present. Leave empty to hard- reject unknown recipients with 550.
  • Rate limit - per-IP requests per 5 minutes. Default 20 is generous for legitimate MTAs, low enough to throttle abuse.

The inbound SMTP configuration page with the DNS checklist

Saving restarts the listener in-process - no redeploy needed.

DNS records

Replace mail.example.com and 203.0.113.10 with the real host / IP. The DNS checklist in the admin UI surfaces these with copy-to- clipboard buttons.

; A record - pin the SMTP hostname to the IP
mail.example.com.  IN  A     203.0.113.10

; MX record - tell senders to deliver mail for example.com here
example.com.       IN  MX    10  mail.example.com.

; SPF - authorise the host to send for the domain (matters when you
; later send notifications from the same hostname)
example.com.       IN  TXT   "v=spf1 ip4:203.0.113.10 -all"

; DMARC - enable reporting + authentication policy
_dmarc.example.com. IN TXT   "v=DMARC1; p=quarantine; rua=mailto:postmaster@example.com"

Reverse DNS (PTR)

Most major receivers (Gmail, Outlook, Yahoo) reject mail from hosts without a PTR record. Set 203.0.113.10 → mail.example.com via your hoster's control panel. Without this, your outbound notifications will likely land in spam.

Reverse proxy (TLS mode = proxy)

Add a stream block to Caddy or Traefik so external connections to port 25 reach the orboto container's bound port internally - see the reverse-proxy examples in the self-hosting guide.

Per-project addresses

Once SMTP is enabled workspace-wide, a new Inbound Email tab appears in Project Settings. Project admins register addresses like support@mail.example.com (the local-part plus the SMTP hostname). Mail to that address routes to that project as a new ticket.

The tickets+KEY@mail.example.com alias is always accepted in addition: it routes by project key (tickets+ACME@mail.example.com posts to project ACME).

Testing

# swaks (apt install swaks) - full SMTP transaction.
swaks --to support@mail.example.com --from you@gmail.com \
      --header "Subject: Hello from swaks" --body "Test body"

# Confirm the MX record resolves
dig mx example.com +short
# Confirm the host accepts on port 25
nc -zv mail.example.com 25

Cloudflare Email Routing → webhook

Free workaround when port 25 is blocked. Cloudflare receives mail at your domain and forwards it to a Worker that POSTs to orboto's /inbound-mail webhook.

Cloudflare setup

  1. Cloudflare dashboard → your domain → Email → Email Routing. Click Get started and verify the destination address (your personal inbox - required even though it isn't used for anything).
  2. Email Routing automatically writes the MX records into your zone.
  3. Add a catch-all rule that runs an Email Worker.

Email Worker

In Cloudflare dashboard → Workers & Pages → Create → Email Worker, paste:

export default {
  async email(message, env) {
    const raw = await new Response(message.raw).text();

    // Pull a few headers we need before re-encoding the body.
    const headers = [];
    for (const [name, value] of message.headers.entries()) {
      headers.push({ name, value });
    }

    const payload = {
      from: message.from,
      to: [message.to],
      subject: message.headers.get('subject') ?? '',
      text: raw,                        // raw MIME - the ingest service parses it
      html: null,
      headers,
    };

    const res = await fetch(`${env.ORBOTO_URL}/inbound-mail`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${env.ORBOTO_INBOUND_SECRET}`,
      },
      body: JSON.stringify(payload),
    });

    if (!res.ok) {
      const body = await res.text();
      // Bouncing back to sender is rude in this flow - Cloudflare
      // already accepted the mail. Log + drop instead.
      console.error('orboto forward failed', res.status, body);
    }
  },
};

Set the Worker's environment variables:

  • ORBOTO_URL = https://orboto.example.com
  • ORBOTO_INBOUND_SECRET = the bearer secret shown under Admin → Inbound mail.

Pros / cons vs self-hosted SMTP

Cloudflare → webhookSelf-hosted SMTP
Latencysecondsseconds
Spam filteringCloudflare'sorboto's gates only
DKIM / SPF checkCloudflare passes verdicts via headersorboto checks them itself
CostfreeVPS + monitoring
Port 25 needednoyes

IMAP polling

Best when you already have a mailbox (a Gmail group, an M365 shared inbox) and don't want to change DNS or run an SMTP server.

Add an account

Admin → Inbound IMAP → New account:

  • Label - operator-facing name only.
  • Host / port / TLS - imap.gmail.com / 993 / on for Gmail and most providers.
  • Username - full email address.
  • Password - see provider-specific section below.
  • Folder - INBOX is the default; use Inbox/orboto to partition.
  • Poll interval - default 5 minutes. Lower to 1 minute on hosts that allow it; some providers throttle aggressive polling.
  • Use IMAP IDLE - push notifications instead of polling. Falls back to polling on failure.

The inbound IMAP account form

Test connection connects, selects the folder, and returns the message count. Poll now runs the same fetch logic the cron uses - useful to verify a fix without waiting for the next interval.

Gmail / Google Workspace

Gmail no longer accepts plain account passwords for IMAP; you need an App Password:

  1. Google account → Security → 2-Step Verification (must be on).
  2. App passwords, generate one for "Mail".
  3. Use that 16-character password in orboto.

Outlook / Microsoft 365

Microsoft is migrating off basic IMAP auth. For now:

  1. Account → Security → Advanced security options → App passwords (only available when 2FA is on).
  2. Use the generated app password.

Generic IMAP

Whatever your provider's IMAP settings page says. Standard ports: 143 STARTTLS, 993 implicit TLS (recommended). Passwords are AES-256-GCM-encrypted at rest and never written or returned in plaintext.

Troubleshooting

"Email not received"

Open Admin → Inbound log first - every accepted, rejected, or duplicate message lands there with the reason.

The inbound mail log with accepted rows and a rejected row showing the rejection chip

  • No row at all - the message never reached the ingest pipeline. For SMTP: check the listener is running (admin SMTP page + /ready). For webhook: confirm the bearer secret is set and the provider isn't throwing 401s on its side. For IMAP: the account row's last_error column carries the most recent fetcher error.
  • Accepted, but the ticket isn't where you expect - check the project routing chain. The log row links to the ticket; click through.
  • Rejected - the reason appears as a chip:
    • auth_policy_failed - the SPF/DKIM/DMARC gate. Check the authentication policy and the inbound headers (see below).
    • sender_domain_not_allowed - the sender's domain isn't on the allowlist. Add it, or clear the allowlist to accept any domain.
    • sender_not_known - "require known user" is on and the sender's email isn't an orboto user.
    • no_project_routing - none of the routing steps matched. Add a per-project address, set a catchall, or set a default project.
    • duplicate - the same Message-ID was already processed. Replies in an existing thread route via the In-Reply-To map, not the dedup index.

For SMTP specifically, also check your hoster hasn't quietly blocked port 25 (telnet your-host 25 from outside).

Reading Authentication-Results headers

The header line looks like:

Authentication-Results: example.com;
  spf=pass smtp.mailfrom=sender@gmail.com;
  dkim=pass header.d=gmail.com;
  dmarc=pass action=none header.from=gmail.com

Each mechanism reports pass / fail / none. orboto's policies:

  • off - no enforcement.
  • permissive - dmarc=pass required.
  • strict - spf=pass AND dmarc=pass required.

DKIM is informational; orboto does not gate on DKIM alone.

Outbound deliverability

Notifications landing in spam is a SEND problem, not a receive problem - see your provider's (UseSend / Resend / SMTP) sending configuration, set up SPF + DKIM for the sending domain, and consider warming a fresh IP.

On this page