---
title: "Inbound email services in Apex"
url: "https://trailassembly.com/apex-email-services/"
topic: "Apex and Development"
published: "2026-09-18T03:54:54+00:00"
updated: "2026-10-06T13:57:41+00:00"
---

# Inbound email services in Apex

**Short answer:** an email service is an Apex class implementing `Messaging.InboundEmailHandler`, bound to one or more generated email addresses, that receives a parsed message and returns a result object. Setting `result.success = false` tells the platform the message was not handled, and what happens then depends on the failure response setting on the service. The two things that catch people out are attachments, which are off until you enable them, and the daily processing cap, which is a function of your user licence count.

## The handler

```apex
global class CaseIntakeHandler implements Messaging.InboundEmailHandler {

    global Messaging.InboundEmailResult handleInboundEmail(
        Messaging.InboundEmail email,
        Messaging.InboundEnvelope env
    ) {
        Messaging.InboundEmailResult result = new Messaging.InboundEmailResult();

        try {
            Case c = new Case(
                Subject     = email.subject,
                Description = String.isNotBlank(email.plainTextBody)
                                ? email.plainTextBody
                                : email.htmlBody,
                SuppliedEmail = env.fromAddress,
                Origin      = 'Email'
            );
            insert c;
            attachFiles(c.Id, email);
            result.success = true;
        } catch (Exception e) {
            result.success = false;
            result.message = 'Could not create case: ' + e.getMessage();
        }
        return result;
    }
}
```

The class must be global and the method must be global, because the platform calls it from outside your namespace. The envelope carries the SMTP level addresses, which can differ from the header addresses in `email.fromAddress`. For anything that matters, trust the envelope.

## What is on the InboundEmail object

- `subject`, `plainTextBody`, `htmlBody` and the matching `plainTextBodyIsTruncated` flags.

- `fromAddress`, `fromName`, `toAddresses`, `ccAddresses`, `replyTo`.

- `headers`, a list of name and value pairs, which is where you find custom tracking headers.

- `messageId`, `inReplyTo` and `references`, which together let you thread a reply onto the right record.

- `binaryAttachments` and `textAttachments`.

Threading deserves a mention. If you put a token in the subject, users will edit it. `inReplyTo` and `references` come from the mail client and are far more reliable for matching a reply to the original [outbound message](https://trailassembly.com/salesforce-outbound-messages/), provided you recorded the message id when you sent it.

## Attachments

Attachments arrive in two collections. `Messaging.InboundEmail.BinaryAttachment` has `body` as a Blob, plus `fileName` and `mimeTypeSubType`. `Messaging.InboundEmail.TextAttachment` has `body` as a String.

```apex
private void attachFiles(Id parentId, Messaging.InboundEmail email) {
    if (email.binaryAttachments == null) { return; }

    List<ContentVersion> versions = new List<ContentVersion>();
    for (Messaging.InboundEmail.BinaryAttachment a : email.binaryAttachments) {
        versions.add(new ContentVersion(
            Title           = a.fileName,
            PathOnClient    = a.fileName,
            VersionData     = a.body,
            FirstPublishLocationId = parentId
        ));
    }
    if (!versions.isEmpty()) { insert versions; }
}
```

Both collections are null rather than empty when there is nothing, so guard before iterating. And attachments are not delivered at all unless the email service has [attachment](https://trailassembly.com/files-vs-attachments/) handling enabled in setup, which is a separate setting from the service being active. Code that works in one org and drops files in another is almost always this.

Watch the heap. Attachment bodies land in the 6 MB synchronous heap along with everything else, so a handler that loops several files into memory at once can fail on size alone.

## The failure response

Returning `success = false`, or throwing an uncaught exception, marks the message as unhandled. The email service configuration then decides what the sender sees: a bounce, silence, or a requeue for some conditions. The `result.message` text is what goes back to the sender when a response is sent, so write it for a human and never put an exception stack trace in it.

There is a separate set of settings for messages from unauthorised senders, oversized messages and inactive addresses. Each can bounce, discard or requeue. Discarding silently is the usual production choice for unauthorised senders, because bouncing tells a spammer the address is live.

## Addresses and limits

One service can have many addresses, each with its own accepted sender list and its own active flag. That is how you route regional intake to the same class with a context value, and how you retire an address without touching Apex.

The daily processing limit is the number of user licences multiplied by 1,000, capped at 1,000,000 messages a day across the org. Messages beyond the cap are rejected according to the over limit setting. For high volume intake, this is the number to check before promising anything.

The handler runs under normal Apex [governor limits](https://trailassembly.com/governor-limits-reference/) per message, in the context user configured on the service. That user’s permissions and sharing apply, so a handler that works for you as an admin can fail for a restricted context user.

## Frequently asked

**Can I make callouts from the handler?** Not directly in a way you should rely on. Enqueue a [Queueable](https://trailassembly.com/apex-choosing-async/) with the parsed data and do the callout there, which also keeps the handler fast.

**How do I test it?** Construct a `Messaging.InboundEmail` and a `Messaging.InboundEnvelope` in the test, populate the fields, and call `handleInboundEmail` directly. Both classes are instantiable in Apex, so no mocking framework is needed.

**Why is plainTextBody empty?** Because the sender sent HTML only. Always fall back to `htmlBody`, and strip the markup before writing it to a text field.

**Does [Email-to-Case](https://trailassembly.com/email-to-case-threading/) use this?** On Demand Email-to-Case is a separate managed feature with its own routing. An email service is what you build when you need behaviour Email-to-Case does not offer.

---

Source: https://trailassembly.com/apex-email-services/
Trail Assembly — independent Salesforce write-ups.
