Overview
EuroMail can receive emails on your behalf, parse them, and deliver the contents to your application via webhooks or the REST API. The inbound pipeline handles MIME parsing, attachment extraction, route resolution, and webhook delivery automatically.
How It Works
When someone sends an email to an address on your verified domain, it flows through these stages:
- SMTP reception -- EuroMail's inbound SMTP server accepts the message (port 25), validates the sender IP rate limit, and captures the raw message.
- Queue -- The raw message is published to a Redis stream (
email:inbound) for reliable, ordered processing. - Worker processing -- A worker consumer picks up the message, decodes the MIME content, extracts headers, body parts, and attachments, then resolves the recipient against your configured routes.
- Storage -- The parsed email is stored in the database with all extracted metadata (subject, from, to, cc, text body, HTML body, headers, attachments).
- Webhook delivery -- If the matched route has a webhook URL, an
email.inboundevent is fired to your endpoint with the full email contents.
Setup
1. Add MX Records
Point your domain's MX records to EuroMail so incoming mail is directed to our servers:
| Type | Host | Value | Priority |
|---|---|---|---|
| MX | yourdomain.com | mail1.euromail.dev | 10 |
| MX | yourdomain.com | mail2.euromail.dev | 20 |
The accepted MX targets are mail1.euromail.dev and mail2.euromail.dev (publish both for redundancy; mail1 alone also verifies, as does the legacy inbound.euromail.dev). Verification fails for any other target.
Note that this is a different record from the return-path MX in domain verification. The two coexist because they live on different hosts:
- Inbound MX goes on the domain you receive mail at (
yourdomain.comabove) and points tomail1.euromail.dev - Return-path MX goes on your sending subdomain (
em.yourdomain.com) and points tobounce.euromail.dev
Don't point the sending subdomain's MX at mail1.euromail.dev; that breaks return-path verification without making inbound work.
2. Verify and Enable Inbound on Your Domain
Run a verification check from the domain page in the dashboard (or POST /v1/domains/{domain_id}/verify). The response includes an mx check that confirms your MX records point to an accepted EuroMail target; the failure detail names the target it found instead. Then enable inbound email processing in the domain's settings to mark the domain as eligible to receive mail.
3. Create Inbound Routes
Routes determine which recipient addresses your account accepts and where the emails are delivered. Without a matching route, incoming emails are dropped.
Match Types
| Match Type | Pattern | Matches | Example |
|---|---|---|---|
exact | support@ | Only [email protected] | Customer support inbox |
prefix | noreply | Any address starting with noreply | [email protected] |
catch_all | *@ | All addresses on the domain | Accept everything |
Routes are evaluated by priority (highest first), then by specificity: exact matches take precedence over prefix matches, which take precedence over catch-all.
Create a Route via API
curl -X POST https://api.euromail.dev/v1/inbound-routes \
-H "X-EuroMail-Api-Key: em_live_..." \
-H "Content-Type: application/json" \
-d '{
"domain_id": "your-domain-uuid",
"pattern": "support@",
"match_type": "exact",
"priority": 10,
"webhook_url": "https://yourapp.com/webhooks/inbound"
}'Create a Route via Dashboard
Navigate to Inbound > Routes in the dashboard. Click New Route, select the domain, choose the match type, enter the pattern, and optionally set a webhook URL.
Webhook Payload
When an inbound email matches a route with a webhook URL configured, EuroMail fires an email.inbound event. The payload includes the full parsed email:
{
"event": "email.inbound",
"inbound_email_id": "550e8400-e29b-41d4-a716-446655440000",
"account_id": "123e4567-e89b-12d3-a456-426614174000",
"domain": "yourdomain.com",
"from": "[email protected]",
"to": "[email protected]",
"subject": "Question about my order",
"text_body": "Hi, I have a question about order #12345...",
"html_body": "<html><body><p>Hi, I have a question...</p></body></html>",
"attachments": [
{
"filename": "receipt.pdf",
"content_type": "application/pdf",
"size": 45231
}
],
"message_id": "<[email protected]>",
"authentication": { "spf": "pass" },
"source_ip": "203.0.113.42",
"timestamp": "2026-03-09T14:32:01Z"
}| Field | Description |
|---|---|
from | SMTP envelope sender (MAIL FROM) |
to | The matched recipient address |
subject | Parsed Subject header (may be null) |
text_body | Plain text body part (may be null) |
html_body | HTML body part (may be null) |
attachments | Array of attachment metadata (filename, content type, size in bytes), or null if none |
message_id | The Message-ID header value (may be null) |
authentication.spf | Sender SPF verdict: pass, fail, softfail, neutral, none, temperror, or permerror (null if the check couldn't run). Use it to decide how much to trust the sender. A fail means the sending server is not authorized for the envelope domain and the sender is very likely forged. |
source_ip | IP address of the sending server |
Note: euromail also uses this verdict itself. An agent mailbox with an auto-responder will not reply to a message whose SPF
failed orsoftfailed, to avoid sending backscatter to a likely-forged sender.
The webhook is signed with HMAC-SHA256 using your webhook signing secret, delivered in the X-EuroMail-Signature header. See the webhooks guide for signature verification details and retry behavior.
API Reference
All inbound API endpoints require authentication via the X-EuroMail-Api-Key header.
Inbound Emails
List Inbound Emails
GET /v1/inbound?page=1&per_page=25
Returns a paginated list of received emails for your account.
Response:
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"mail_from": "[email protected]",
"rcpt_to": ["[email protected]"],
"subject": "Question about my order",
"from_header": "John Doe <[email protected]>",
"to_header": "[email protected]",
"source_ip": "203.0.113.42",
"size_bytes": 12480,
"status": "delivered",
"created_at": "2026-03-09T14:32:01Z"
}
],
"pagination": {
"page": 1,
"per_page": 25,
"total": 42,
"total_pages": 2
}
}Get Inbound Email
GET /v1/inbound/{id}
Returns the full inbound email including text body, HTML body, raw headers, and attachment metadata.
Delete Inbound Email
DELETE /v1/inbound/{id}
Permanently deletes an inbound email. Returns 204 No Content on success.
Inbound Routes
Create Route
POST /v1/inbound-routes
Request body:
{
"domain_id": "uuid",
"pattern": "support@",
"match_type": "exact",
"priority": 10,
"webhook_url": "https://yourapp.com/webhooks/inbound"
}| Field | Required | Description |
|---|---|---|
domain_id | Yes | UUID of a verified domain you own |
pattern | Yes | Address pattern to match (see match types above) |
match_type | Yes | One of exact, prefix, or catch_all |
priority | No | Higher priority routes are evaluated first (default: 0) |
webhook_url | No | HTTPS URL to receive email.inbound webhook events |
Pattern rules:
exactpatterns must end with@(e.g.support@)prefixpatterns must not contain@(e.g.noreply)catch_allpattern must be*@
List Routes
GET /v1/inbound-routes?page=1&per_page=25Get Route
GET /v1/inbound-routes/{id}Update Route
PUT /v1/inbound-routes/{id}
Request body:
{
"pattern": "support@",
"match_type": "exact",
"priority": 10,
"webhook_url": "https://yourapp.com/webhooks/inbound",
"is_active": true
}Delete Route
DELETE /v1/inbound-routes/{id}
Returns 204 No Content on success.
Processing a Webhook in Your Application
from flask import Flask, request, jsonify
import hmac
import hashlib
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_signing_secret"
@app.route("/webhooks/inbound", methods=["POST"])
def handle_inbound():
# Verify signature
signature = request.headers.get("X-EuroMail-Signature", "")
expected = hmac.new(
WEBHOOK_SECRET.encode(), request.data, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(f"sha256={expected}", signature):
return "Invalid signature", 401
data = request.json
print(f"From: {data['from']}")
print(f"Subject: {data['subject']}")
print(f"Body: {data['text_body']}")
# Process attachments
for att in data.get("attachments") or []:
print(f"Attachment: {att['filename']} ({att['content_type']}, {att['size']} bytes)")
return jsonify({"status": "ok"}), 200const express = require("express");
const crypto = require("crypto");
const app = express();
const WEBHOOK_SECRET = "your_webhook_signing_secret";
app.post("/webhooks/inbound", express.raw({ type: "*/*" }), (req, res) => {
const signature = req.headers["x-euromail-signature"] || "";
const expected = "sha256=" +
crypto.createHmac("sha256", WEBHOOK_SECRET).update(req.body).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send("Invalid signature");
}
const data = JSON.parse(req.body);
console.log(`From: ${data.from}`);
console.log(`Subject: ${data.subject}`);
res.json({ status: "ok" });
});
app.listen(3000);Limits
| Limit | Value |
|---|---|
| Maximum message size | 25 MB |
| Rate limit per source IP | 50 connections per minute |
| Attachment metadata | Filename, content type, and size are stored per attachment |
Dashboard
The dashboard provides a complete interface for managing inbound email:
- Inbound list -- Search, filter by status and date range, view sender, subject, and recipients at a glance.
- Email detail -- View parsed headers, toggle between text and HTML body rendering, see attachment metadata, and delete individual emails.
- Route management -- Create, edit, activate/deactivate, and delete inbound routes from the Routes tab.