Inbound Email Service
Salesforce can accept inbound email and run Apex against each
message. The mechanism: an Email Service (org-level
configuration) maps one or more Email Service Addresses (the
local-part + Salesforce-supplied domain) to a class implementing
Messaging.InboundEmailHandler. Apex receives the parsed email,
returns a Messaging.InboundEmailResult, and the platform
responds (delivery success, error reply, drop) accordingly.
The classic alternative is Email-to-Case — a built-in service that creates a Case from each email, with extensive admin configuration (auto-response, routing, threading, contact lookup). Email-to-Case is the right answer for case creation; custom email service is the right answer for everything else (file uploads, custom-object creation, audit logging, complex routing).
What this skill is NOT. Outbound email — admin/email-templates-and-alerts.
Email-to-Case-specific configuration — service/email-to-case.
This skill is the custom-handler path.
Before Starting
- Decide custom service vs Email-to-Case. Case creation? E2C is built; don't reinvent. Anything else? Custom service.
- Plan the running user. The handler runs as the user configured on the Email Service. Their permissions determine what the handler can do. Use a dedicated integration user.
- Plan the accept-from policy. Anonymous public addresses receive spam; consider authorized-senders allow-listing or rate limiting.
- Plan attachment handling. Salesforce has limits (max email size, max attachment size, total org file storage). Decide store vs discard before traffic ramps.
Core Concepts
Email Service vs Email Services Address
- Email Service (org-level) — the configuration: running user, max retention, accept-from policy, accept attachments, error response template.
- Email Services Address (per address) — the actual local-part
- Salesforce-supplied subdomain. One Email Service can have
many Addresses (e.g.
quotes-prod@...,quotes-dev@...,quotes-eu@...), all routing to the same handler.
- Salesforce-supplied subdomain. One Email Service can have
many Addresses (e.g.
The handler doesn't see which Address received the email
directly — it sees the recipient in email.toAddresses. Branch
on the recipient if you need per-address logic.
Messaging.InboundEmailHandler interface
global class IncomingQuoteHandler implements Messaging.InboundEmailHandler {
global Messaging.InboundEmailResult handleInboundEmail(
Messaging.InboundEmail email,
Messaging.InboundEnvelope envelope
) {
Messaging.InboundEmailResult result = new Messaging.InboundEmailResult();
try {
processQuote(email);
result.success = true;
} catch (Exception ex) {
result.success = false;
result.message = 'Could not process: ' + ex.getMessage();
ApplicationLogger.error('Quote email failed', ex);
}
return result;
}
}
Three things to know:
globalis required. Same asSandboxPostCopy.InboundEmailpayload —subject,fromAddress,fromName,toAddresses,ccAddresses,plainTextBody,htmlBody,headers(a list, not a map — thereferences/inReplyToheaders are critical for threading),binaryAttachments,textAttachments.- Return value.
success = true→ delivery confirmed.success = false+message→ bounce-back response. Themessageis what the sender sees.
Email threading via In-Reply-To and References headers
Email clients thread replies by Message-Id and In-Reply-To
headers. Salesforce's email service parses them in email.headers:
String inReplyTo = null;
for (Messaging.InboundEmail.Header h : email.headers) {
if (h.name.toLowerCase() == 'in-reply-to') {
inReplyTo = h.value;
break;
}
}
For threading inbound emails to existing Salesforce records:
- Email-to-Case threading — uses
[ref:...]token in subject / body. The system inserts the token in outbound replies; inbound replies preserve it; E2C extracts it and links the email to the existing case. - Custom service threading — implement your own. Either embed
a token in your outbound emails (case-insensitive, robust against
client mangling) or look up by
In-Reply-Toagainst a stored Message-Id of your previous outbound.
Attachment handling
for (Messaging.InboundEmail.BinaryAttachment att : email.binaryAttachments) {
ContentVersion cv = new ContentVersion(
Title = att.fileName,
PathOnClient = att.fileName,
VersionData = att.body,
FirstPublishLocationId = parentRecordId
);
insert cv;
}
Limits:
- Max email size: ~25 MB total (configured per Email Service; email-with-attachments above this are bounced).
- Max single attachment: limited by the email-size cap.
- Org-wide file storage: every saved attachment counts against the org's File Storage allocation. Plan retention.
Email-to-Case vs custom service decision
| Need | Use |
|---|---|
| Create a Case from an email | Email-to-Case |
| Auto-response from a template | Email-to-Case (or On-Demand E2C) |
| Threading replies to existing Case | Email-to-Case with [ref:...] token |
| Create a Lead / Opportunity / Custom Object | Custom service |
| Upload a file to a record | Custom service |
| Trigger a downstream API callout | Custom service |
| Complex routing (e.g. "if subject starts with X, do Y") | Custom service (or E2C with assignment rules — depends) |
| Multi-language / encoded payloads | Custom service for full control |
Common Patterns
Pattern A — Lead-from-email-form
When to use. Marketing landing page submits a form via email to a known address; need to create a Lead from each.
global class LeadFromEmail implements Messaging.InboundEmailHandler {
global Messaging.InboundEmailResult handleInboundEmail(
Messaging.InboundEmail email, Messaging.InboundEnvelope envelope
) {
Messaging.InboundEmailResult res = new Messaging.InboundEmailResult();
try {
Lead l = new Lead(
Email = email.fromAddress,
LastName = email.fromName != null ? email.fromName : '(unknown)',
Company = parseCompanyFromBody(email.plainTextBody),
LeadSource = 'Email Form'
);
insert l;
res.success = true;
} catch (DmlException ex) {
res.success = false;
res.message = 'Could not create lead: ' + ex.getMessage();
}
return res;
}
}
Pattern B — File upload to existing record via subject token
When to use. Users email attachments to a routing address with
the record Id in the subject (Upload — 0061a000007ABC).
The handler parses the Id from the subject, validates it, attaches files. Returns success / failure to the sender.
Pattern C — Anti-spam allow-list
When to use. Public-facing routing address that gets spam.
private static final Set<String> ALLOWED_DOMAINS = new Set<String>{
'acme.com', 'partner.example.com'
};
global Messaging.InboundEmailResult handleInboundEmail(
Messaging.InboundEmail email, Messaging.InboundEnvelope envelope
) {
String fromDomain = email.fromAddress.substringAfter('@').toLowerCase();
if (!ALLOWED_DOMAINS.contains(fromDomain)) {
Messaging.InboundEmailResult res = new Messaging.InboundEmailResult();
res.success = false;
res.message = 'Sender domain not authorized';
return res;
}
// ... legitimate processing ...
}
For more nuanced allow-listing, store the list in Custom Metadata or Custom Setting so admins can manage without redeploying Apex.
Decision Guidance
| Situation | Approach | Reason |
|---|---|---|
| Create a Case from email | Email-to-Case | Built-in; threading, routing, auto-response included |
| Create any other record from email | Custom Email Service + InboundEmailHandler | E2C only creates Cases |
| Public address receives spam | Custom service with allow-list | E2C also has spam handling but per-Case |
| Need to upload files | Custom service | E2C Email Message attachments tied to Case |
| Threading replies to existing record | Subject token ([ref:...]) or In-Reply-To header parsing |
Email clients mangle these; tokens are more robust |
| Inbound volume > 1K / day per address | Plan governor budget + dedicated running user | High volume can hit Apex governor in a single batch |
| Multi-language / RTL / encoded subject | Custom handler with explicit charset handling | E2C parses for the common cases; edge cases break it |
| Inbound email triggers a callout | Custom service + Platform Event for async work | Don't do the callout in the handler synchronously |
| Anonymous public access undesirable | Email Service Authorize Email Addresses |
Per-Service allow-list at the platform level |
Recommended Workflow
- Decide custom service vs Email-to-Case. Case creation → E2C; everything else → custom.
- Provision a dedicated running user for the Email Service. Document its required permissions.
- Implement the handler.
- Write
Messaging.InboundEmailHandlerwith the right business logic. - Plan threading if applicable (subject token,
In-Reply-Toparse).
- Write
- Configure the Email Service and its addresses.
- Setup → Email → Email Services. Set running user, max email size, accept attachments, error-response template.
- Create one or more Email Services Addresses. Each gets a Salesforce-supplied subdomain.
- Test and monitor.
- Send real emails to the address. Verify success / failure paths.
- Monitor inbound email volume, handler exceptions, attachment storage growth.
Review Checklist
- Class implements
Messaging.InboundEmailHandlerwithglobalaccess. - Handler returns
InboundEmailResultwith explicitsuccessvalue (never silently throws). - Running user is dedicated to the Email Service, with documented permissions.
- Allow-list / spam handling for public-facing addresses.
- Attachment retention policy explicit.
- Threading via subject token or
In-Reply-Toif applicable. - Test class covers success path, malformed input, attachment-too-large, allow-list-rejection.
- Email Service
Max Email SizeandAccept Attachmentsconfigured deliberately.
Salesforce-Specific Gotchas
Messaging.InboundEmailHandlerrequiresglobalaccess. Same asSandboxPostCopy. (Seereferences/gotchas.md§ 1.)email.headersis aList<Header>, not aMap. Iterate to find the header you want. (Seereferences/gotchas.md§ 2.)- Returning
success = falsetriggers a bounce-back to the sender. Themessageis what the sender sees — don't expose stack traces. (Seereferences/gotchas.md§ 3.) - Max email size has a hard ceiling; emails above are bounced before the handler sees them. (See
references/gotchas.md§ 4.) - Email Services Address subdomain is Salesforce-supplied, not customer-domain-mappable. For customer domains, use email forwarding from the customer side. (See
references/gotchas.md§ 5.) - The handler runs in the configured running user's context, not the sender's. FLS / sharing applies to that user. (See
references/gotchas.md§ 6.) - In-Reply-To / Message-Id headers are unreliable across email clients; subject-token threading is more robust. (See
references/gotchas.md§ 7.)
Output Artifacts
| Artifact | Description |
|---|---|
Messaging.InboundEmailHandler class |
The handler implementation |
| Test class | Covers handler with synthetic InboundEmail payloads |
| Email Service configuration | Documented setup steps in Setup → Email → Email Services |
| Allow-list source | Custom Metadata / Custom Setting for admin-managed sender list |
| Threading strategy | Subject token format or In-Reply-To parsing logic |
Related Skills
service/email-to-case— when the requirement is case creation; this skill is the custom-service alternative.admin/email-templates-and-alerts— outbound email infrastructure.apex/apex-event-bus-subscriber— when the handler publishes a Platform Event for async downstream work.apex/dynamic-apex— when the handler needs Schema describe to create records of varying types.