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.

Which path do I want?
| Path | Latency | DNS work | Port 25 needed | Best for |
|---|---|---|---|---|
| Provider webhook (UseSend / Resend / Postmark / SendGrid) | seconds | none | no | Easiest 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 polling | up to 5 min | none | no | You already have a mailbox you want orboto to read |
| Self-hosted SMTP | seconds | MX + A + SPF + DMARC | yes | Per-project addresses, no third party in the loop |
| Cloudflare Email Routing → webhook | seconds | MX (Cloudflare) | no | Self-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.

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/25for 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+KEYalias is present. Leave empty to hard- reject unknown recipients with550. - Rate limit - per-IP requests per 5 minutes. Default 20 is generous for legitimate MTAs, low enough to throttle abuse.

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 25Cloudflare 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
- 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).
- Email Routing automatically writes the MX records into your zone.
- 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.comORBOTO_INBOUND_SECRET= the bearer secret shown underAdmin → Inbound mail.
Pros / cons vs self-hosted SMTP
| Cloudflare → webhook | Self-hosted SMTP | |
|---|---|---|
| Latency | seconds | seconds |
| Spam filtering | Cloudflare's | orboto's gates only |
| DKIM / SPF check | Cloudflare passes verdicts via headers | orboto checks them itself |
| Cost | free | VPS + monitoring |
| Port 25 needed | no | yes |
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 -
INBOXis the default; useInbox/orbototo 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.

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:
- Google account → Security → 2-Step Verification (must be on).
- App passwords, generate one for "Mail".
- Use that 16-character password in orboto.
Outlook / Microsoft 365
Microsoft is migrating off basic IMAP auth. For now:
- Account → Security → Advanced security options → App passwords (only available when 2FA is on).
- 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.

- 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'slast_errorcolumn 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.comEach mechanism reports pass / fail / none. orboto's policies:
off- no enforcement.permissive-dmarc=passrequired.strict-spf=passANDdmarc=passrequired.
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.