SfSkills Domains Docs GitHub

Home / apex / apex-email-services

apex-email-services

Use this skill when implementing inbound email processing via Apex: parsing emails sent to a Salesforce-hosted address, creating or updating records from email content, handling attachments, or configuring Email Service routing. Trigger keywords: InboundEmailHandler, email service address, handleInboundEmail, Messaging.InboundEmail, Email-to-Case alternative, process email in Apex. NOT for outbound email — use apex/apex-outbound-email-patterns. NOT for email templates or workflow email alerts — use admin/email-templates-and-alerts.

Open the skill folder on GitHub


Apex Email Services

This skill activates when a practitioner needs to receive, parse, and act on inbound emails using Apex. It covers the full lifecycle: implementing Messaging.InboundEmailHandler, configuring the Email Service address in Setup, parsing body and attachment content, handling rejection/bounce behavior, and writing testable handlers. It does NOT cover outbound email (Messaging.SingleEmailMessage), email alerts, or the declarative Email-to-Case setup.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

Mode 1: The InboundEmailHandler Interface

Messaging.InboundEmailHandler is the contract for all Apex inbound email processing. Your class must implement exactly one method:

global Messaging.InboundEmailResult handleInboundEmail(
    Messaging.InboundEmail email,
    Messaging.InboundEnvelope envelope
) { ... }

The return value matters. If you set result.success = false, Salesforce either bounces the message back to the sender or drops it silently — controlled by the Error Action setting on the Email Service configuration in Setup. Always return a populated InboundEmailResult; a null return is treated as failure.

The InboundEmail object carries the full message:

The InboundEnvelope carries transport-level metadata: toAddress, fromAddress, which can differ from the To: and From: headers.

Mode 2: Email Service Configuration

Each Apex handler class is associated with one or more Email Service configurations in Setup > Email Services. Each configuration generates a unique @[instance].salesforce.com address. Key settings:

SettingPurpose
ActiveMust be checked or all mail is dropped.
Accept Email FromRestrict to specific sender domains or addresses; leave blank to accept all.
Error ActionControls what happens when success = false — Bounce, Discard, or Requeue.
Apex ClassPoints to your InboundEmailHandler implementation.
Over Email Rate LimitAction when the daily limit is reached — Bounce, Discard, or Requeue.

You can create multiple Email Service addresses (each with a different configuration) backed by the same Apex class to support different routing scenarios (e.g., separate addresses per product line, each stamping a different record type on created Cases).

Mode 3: Attachment Parsing

Two attachment types exist, mapped separately:

CSV, plain text, and XML files arrive as TextAttachment when the MIME type is text-based. Images, PDFs, and binary formats arrive as BinaryAttachment. Handlers must check both lists and apply defensive null checks — either list can be null if no attachments of that type exist.

Attachments are processed synchronously inside your handler's governor limit budget. The inbound message as a whole — body plus all attachments — is capped at 25 MB, so that is the ceiling on what can arrive, against the 50 MB email-services heap allocation. Heap is consumed by transforming the payload rather than by receiving it: converting a Blob to a String (e.g., EncodingUtil.base64Encode, Blob.toString()) allocates a fresh full-size copy each time, and two or three copies of a large attachment will exhaust 50 MB. Use chunked processing, or persist the Blob and defer the heavy work to a Queueable called from within the handler — sizing that Queueable's work against the 12 MB asynchronous heap limit rather than the handler's 50 MB.


Common Patterns

Pattern: Create-or-Update Record from Inbound Email

When to use: An external system sends structured emails (e.g., order confirmations, sensor alerts) and you need to upsert Salesforce records based on parsed email content.

How it works:

  1. Implement InboundEmailHandler and parse email.plainTextBody or email.subject for a record identifier (e.g., an order number in the subject line).
  2. Use SOQL to find an existing record by the extracted identifier.
  3. Upsert or insert accordingly, populating fields from parsed email content.
  4. Return result.success = true on success, or log errors and return false if parsing fails.
global class OrderEmailHandler implements Messaging.InboundEmailHandler {
    global Messaging.InboundEmailResult handleInboundEmail(
        Messaging.InboundEmail email,
        Messaging.InboundEnvelope envelope
    ) {
        Messaging.InboundEmailResult result = new Messaging.InboundEmailResult();
        try {
            String orderNum = extractOrderNumber(email.subject);
            if (String.isBlank(orderNum)) {
                result.success = false;
                result.message = 'No order number found in subject.';
                return result;
            }
            List<Order__c> orders = [
                SELECT Id FROM Order__c WHERE OrderNumber__c = :orderNum LIMIT 1
            ];
            Order__c order = orders.isEmpty() ? new Order__c(OrderNumber__c = orderNum) : orders[0];
            order.LastEmailBody__c = email.plainTextBody;
            order.LastEmailDate__c = System.now();
            upsert order OrderNumber__c;
            result.success = true;
        } catch (Exception e) {
            result.success = false;
            result.message = e.getMessage();
        }
        return result;
    }

    private String extractOrderNumber(String subject) {
        if (subject == null) return null;
        Pattern p = Pattern.compile('ORD-\\d+');
        Matcher m = p.matcher(subject);
        return m.find() ? m.group() : null;
    }
}

Why not simpler approaches: A Flow with Email-to-Case only creates Cases. Custom Apex is needed when you need to target arbitrary sObjects, apply complex parsing logic, or conditionally reject mail.

Pattern: Async Attachment Processing via Queueable

When to use: Emails arrive with large or multiple binary attachments that would exceed heap or CPU limits if processed synchronously.

How it works:

  1. In handleInboundEmail, extract only the attachment metadata and Blob data. Store the Blob in a ContentVersion record immediately (avoids re-processing the email).
  2. Enqueue a Queueable job, passing the ContentVersion Id.
  3. The Queueable performs the expensive parsing (CSV, base64 decode, PDF text extraction stubs, etc.) in a separate transaction with its own governor limits.
  4. Return result.success = true from the handler immediately — the email is accepted, processing continues asynchronously.

Why not synchronous: The email-services heap ceiling is 50 MB, not the 6 MB synchronous / 12 MB asynchronous figure for ordinary Apex — a 10–15 MB attachment does not blow heap by arriving. What exhausts it is in-line transformation: EncodingUtil.base64Encode allocates a second, ~33%-larger copy, Blob.toString() a third, and a parse loop one per iteration.

Be precise about what deferring buys you. A Queueable runs under the ordinary 12 MB asynchronous heap limit, so moving work there lowers your heap ceiling from 50 MB — "defer it for more heap" is backwards. Defer for a fresh transaction with its own CPU and DML budget and a retry surface, and size the Queueable's work against 12 MB.

Pattern: Sender-Based Routing with Multiple Service Addresses

When to use: Different senders or subject patterns need to create different record types or trigger different workflows, but you want a single Apex class.

How it works:

  1. Create multiple Email Service configurations in Setup, each pointed at the same Apex class.
  2. Pass a routing signal via the email address itself (e.g., support-billing@... vs support-tech@...) or encode routing in a Custom Setting keyed by toAddress.
  3. Inside handleInboundEmail, read envelope.toAddress to determine routing context, then branch logic accordingly.

Decision Guidance

SituationRecommended ApproachReason
Customer email should create a Case automaticallyDeclarative Email-to-Case (Setup > Email-to-Case)No code needed; native threading and routing built in
Email from external system should upsert a custom sObjectApex Email Service with InboundEmailHandlerEmail-to-Case only targets Cases; Apex has full DML access
Email has large binary attachments requiring custom parsingApex handler + Queueable for attachment processingAvoids synchronous heap/CPU limits
Need to reject or bounce specific senders at processing timeApex handler returning result.success = falseEmail Services Error Action controls bounce/discard behavior
Outbound transactional emails to customersMessaging.SingleEmailMessage or email templates + workflowThis skill covers inbound only
Inbound volume approaches the org's daily ceiling (user licenses × 1,000, max 1,000,000)Platform architecture review — consider middleware or batchingThe ceiling is org-wide and shared with On-Demand Email-to-Case, scales with licenses rather than edition, and cannot be raised in code

Step-by-step instructions for an AI agent or practitioner activating this skill:

  1. Gather context — confirm the org's user license count, relevant objects, and current configuration state
  2. Review official sources — check the references in this skill's well-architected.md before making changes
  3. Implement or advise — apply the patterns from Core Concepts and Common Patterns sections above
  4. Validate — run the skill's checker script and verify against the Review Checklist below
  5. Document — record any deviations from standard patterns and update the template if needed

Review Checklist

Run through these before marking work in this area complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. The Default Access Mode Is Version-Gated — at apiVersion 66.0 and below, a handler class with no sharing keyword runs in system mode: no with sharing enforcement and no record-level access control unless you explicitly call into a helper class declared with sharing, so a handler that creates records with a bare insert bypasses all sharing rules silently. At 67.0+ the bare class runs with sharing and its SOQL/DML default to user mode against the Email Service's Context User — which moves the risk onto that user's permissions rather than removing it. The gate is the class's .cls-meta.xml, not the org's release; see references/gotchas.md Gotcha 2.
  2. Inactive Address Drops Mail Silently — If the Email Service address is set to Inactive in Setup, inbound emails do not bounce and do not generate errors. They are silently discarded. Practitioners regularly deploy a handler, forget to activate the service address, and spend hours debugging a "no traffic" problem.
  3. Both Body Fields Can Be Null — HTML-only emails leave plainTextBody null. Plain-text-only emails leave htmlBody null. Production handlers that assume one is always populated throw NullPointerExceptions on real mail. Always test both paths.

Output Artifacts

ArtifactDescription
InboundEmailHandler Apex classGlobal Apex class implementing Messaging.InboundEmailHandler, ready to associate with an Email Service configuration
Email Service configuration checklistVerified settings in Setup > Email Services: active flag, accepted senders, error action, rate limit action
Attachment parsing strategyDocumented decision on sync vs async processing based on expected attachment size and volume
Test classApex test constructing Messaging.InboundEmail objects directly and asserting on DML outcomes

Package files