Salesforce Inbound Email Services: Create Records from Email

Blog / Salesforce · March 11, 2018 · Updated June 10, 2026 · 12 min read
Salesforce Inbound Email Services: Create Records from Email

To create Salesforce records from inbound email, build an Apex Email Service: an Apex class that implements the Messaging.InboundEmailHandler interface and a Salesforce-generated email address that invokes it. When mail arrives at that address, Salesforce calls your handleInboundEmail(Messaging.InboundEmail email, Messaging.InboundEnvelope env) method, where you read the subject, body, sender, and attachments, insert a Lead, Case, or custom record, and return a Messaging.InboundEmailResult with result.success = true.

That is the whole pattern. Everything below shows how to set it up, a working handler, the limits to respect in 2026, and when a no-code option like Email-to-Case is the smarter choice.

Key takeaways

  • An Email Service = a Salesforce-generated inbound address + an Apex class implementing Messaging.InboundEmailHandler.
  • The entry point is handleInboundEmail(Messaging.InboundEmail email, Messaging.InboundEnvelope env), which returns a Messaging.InboundEmailResult.
  • You parse email.subject, email.plainTextBody / email.htmlBody, email.fromAddress, env.fromAddress, and email.binaryAttachments / email.textAttachments, then insert whatever record you need.
  • Governor limits apply inside the handler, and Salesforce caps how many inbound messages an org can process per day (it scales with your user licenses).
  • Set result.success = false to make Salesforce treat the message as failed (so it bounces or follows your failure response) instead of silently swallowing errors.
  • For standard customer support, Email-to-Case (no code) is usually a better fit than a custom handler. Reach for Apex when you need bespoke parsing, custom objects, or system-to-system email automation.

What is a Salesforce inbound Email Service?

A Salesforce Email Service is an automated process that hands an incoming email to Apex for processing. Every email service has one or more Salesforce-generated email addresses; when one of those addresses receives a message, Salesforce parses it into an object model and invokes the Apex class you nominated.

That class must implement the Messaging.InboundEmailHandler interface, which has a single method:

  • Messaging.InboundEmailResult handleInboundEmail(Messaging.InboundEmail email, Messaging.InboundEnvelope env)

Inside the method you have access to a small family of system-provided types under the Messaging namespace:

  • Messaging.InboundEmail - the parsed message: subject, fromAddress, fromName, plainTextBody, htmlBody, ccAddresses, headers, plus the attachment collections.
  • Messaging.InboundEmail.BinaryAttachment - binary files (PDF, images, etc.); the file bytes live in body as a Blob.
  • Messaging.InboundEmail.TextAttachment - text files; body is a String.
  • Messaging.InboundEnvelope - the SMTP envelope: fromAddress and toAddress as the mail server saw them.
  • Messaging.InboundEmailResult - what you return; set success (Boolean) and an optional message (String).

Because it is Apex, you control everything: validation, deduplication, which object to create, and how to react to malformed mail. A classic example is turning a forwarded inquiry into a Lead, or an alert from a monitoring system into a custom record.

How do you set up an inbound Email Service in Salesforce?

First write and deploy the Apex handler (next section), then wire up the service in Setup. In Lightning Experience, go to Setup -> search "Email Services" -> Email Services.

  1. Click New Email Service and give it a name.
  2. In Apex Class, select the class that implements Messaging.InboundEmailHandler.
  3. Choose which attachment types to accept (none, text only, binary only, or all). Accepting attachments increases the work your handler must do.
  4. Set Accept Email From to a comma-separated allowlist of addresses or domains, or leave it blank to accept mail from anywhere. Restricting senders is your first line of spam defense.
  5. Optionally enable Advanced Email Security Settings so Salesforce verifies the sender with SPF, Sender ID, or DKIM before processing.
  6. Configure the Failure Response Settings (what to do when the address is inactive, the sender is not allowed, attachments are not allowed, or the daily limit is hit) and click Save.
  7. On the service detail page, click New Email Address under Email Addresses. Activate it and save. Salesforce generates a long, unique address such as youralias@<random>.<region>.apex.salesforce.com.

That long auto-generated address is what actually triggers the Apex. It is rarely user-friendly, so most teams forward mail to it from a friendlier address - for example, set up a distribution list or alias like leads@yourcompany.com that forwards to the generated address, or use a routing rule in your mail provider. To run a quick test, just send an email to the generated address and check that the record appears.

How do you write the Apex InboundEmailHandler?

The handler below creates a Lead from each inbound message. It prefers the SMTP envelope sender (env.fromAddress) for the email field, falls back from plain text to HTML for the body, and wraps everything in a try/catch so failures are reported rather than swallowed. Note the class is global - inbound email handlers must be global so the platform can call them.

global class CreateLeadFromEmail implements Messaging.InboundEmailHandler {

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

        try {
            // Prefer the plain-text body; fall back to HTML if it is empty.
            String body = String.isNotBlank(email.plainTextBody)
                ? email.plainTextBody
                : email.htmlBody;

            Lead newLead = new Lead(
                LastName    = String.isNotBlank(email.fromName) ? email.fromName : 'Email Lead',
                Company     = 'Inbound Email',
                Email       = env.fromAddress,        // sender as the mail server saw it
                LeadSource  = 'Inbound Email',
                Description = 'Subject: ' + email.subject + '\n\n' + body
            );
            insert newLead;

            // Report success so Salesforce does not treat the message as failed.
            result.success = true;
            result.message = 'Lead created: ' + newLead.Id;
        } catch (Exception e) {
            // Returning success = false triggers your configured failure response.
            result.success = false;
            result.message = 'Could not create Lead: ' + e.getMessage();
        }
        return result;
    }
}

email vs env, and how to read the message

Two objects describe the sender, and the difference matters:

  • email.fromAddress is the address in the message header (the From: the user typed). It can be spoofed or be a friendly display address.
  • env.fromAddress is the envelope sender the SMTP server actually used. Use env.fromAddress when you need the most reliable sender, and env.toAddress to see which of your service addresses received the mail (useful when one handler serves several inboxes).

The body is exposed two ways: email.plainTextBody and email.htmlBody. Many automated systems send only one, so always handle the case where one is blank. You can also read email.subject, email.ccAddresses, email.fromName, and the raw email.headers list if you need to route on custom headers.

The same pattern works for Cases, Contacts, or any custom object - swap the insert target and map the fields you parse out of the subject or body. For example, you could regex an order number out of the subject line and update an existing record instead of creating a new one.

How do you save email attachments to the record?

Attachments arrive as two typed collections: email.binaryAttachments (a List<Messaging.InboundEmail.BinaryAttachment> whose body is a Blob) and email.textAttachments (text files, body is a String). The modern way to store files is Salesforce Files (ContentVersion + ContentDocumentLink) rather than the legacy Attachment object. This snippet saves each binary attachment and links it to the Lead created above:

if (email.binaryAttachments != null && !email.binaryAttachments.isEmpty()) {
    List<ContentVersion> versions = new List<ContentVersion>();
    for (Messaging.InboundEmail.BinaryAttachment att : email.binaryAttachments) {
        versions.add(new ContentVersion(
            Title        = att.fileName,
            PathOnClient = att.fileName,
            VersionData  = att.body            // att.body is a Blob
        ));
    }
    insert versions;

    // Link each uploaded file to the record (here, newLead.Id).
    Set<Id> versionIds = new Set<Id>();
    for (ContentVersion v : versions) versionIds.add(v.Id);

    Map<Id, ContentVersion> docByVersion = new Map<Id, ContentVersion>(
        [SELECT Id, ContentDocumentId FROM ContentVersion WHERE Id IN :versionIds]
    );

    List<ContentDocumentLink> links = new List<ContentDocumentLink>();
    for (ContentVersion v : docByVersion.values()) {
        links.add(new ContentDocumentLink(
            ContentDocumentId = v.ContentDocumentId,
            LinkedEntityId    = newLead.Id,
            ShareType         = 'V'
        ));
    }
    insert links;
}

What are the governor and processing limits?

An inbound email handler runs in a normal Apex transaction, so all the usual governor limits apply - SOQL queries, DML statements, CPU time, and heap. If a single message could create or update many records, bulkify your DML and never put SOQL or DML inside a loop.

Beyond per-transaction limits, Salesforce caps how much email an org can process:

  • Daily processing limit: the number of inbound messages your org can process per day is your number of user licenses multiplied by 1,000, up to a fixed org-wide daily maximum (1,000,000 messages as of 2026). Messages over the limit bounce or are discarded per your failure settings.
  • Message size: there is a maximum total size for the message including attachments (on the order of tens of MB - confirm the current figure in Salesforce's email-service limits, as it varies by encoding and edition). Oversized mail is rejected before your Apex runs.
  • Body size: the text and HTML bodies each have their own size ceiling; very large bodies are truncated.

Because limits and rejections happen, your handler must handle failure gracefully. Always confirm current figures in the Salesforce documentation rather than hard-coding assumptions, and design for the day a malformed or oversized message arrives.

Custom Email Service vs Email-to-Case vs Web-to-Lead/Case

A custom Apex Email Service is powerful, but it is not always the right tool. For standard support inboxes or simple web capture, Salesforce ships no-code features that are faster to stand up and easier to maintain. Compare the three:

Custom Apex Email Service Email-to-Case Web-to-Lead / Web-to-Case
Trigger Email to a generated address Email to your support address HTML form on your website
Code required Yes (Messaging.InboundEmailHandler) No - point and click No - Salesforce generates the HTML
Records created Any object: Lead, Case, custom Case (plus an EmailMessage) Lead or Case
Custom parsing / logic Full control in Apex Limited (routing rules, templates) Field mapping only
Threading / replies You build it Built in (email threading) N/A
Best for Bespoke email-to-record automation, custom objects, system-to-system mail Customer support inboxes Capturing web inquiries

If your goal is support email becoming Cases, start with Email-to-Case and On-Demand Email-to-Case - it gives you threading, auto-response, and routing without Apex. If you are capturing inquiries from a public website, Web-to-Case (or Web-to-Lead) is the no-code path. Choose a custom Email Service only when those cannot model your logic.

When should you use a custom Apex Email Service?

Reach for a custom InboundEmailHandler when:

  • You need to create or update objects other than Case - Leads, custom objects, or several records from one message.
  • The data is structured inside the email (an order confirmation, a monitoring alert, an EDI-style message) and must be parsed and mapped to fields.
  • You are doing system-to-system integration where another platform emails Salesforce and you control the format.
  • You need deduplication, matching, or upsert logic - for example, attach the email to an existing record by reference number instead of always creating a new one.

Stick with the no-code features when the job is ordinary support email or simple web capture. A good rule: if Email-to-Case or Web-to-Lead/Case can do it with configuration, use them - they need no test class, no deployment, and no maintenance. Save Apex for the cases that genuinely need code.

How do you test an inbound email handler?

Inbound email handlers are testable because Messaging.InboundEmail and Messaging.InboundEnvelope can be instantiated directly in a test - you build a fake message, call handleInboundEmail, and assert the records were created. You need this coverage anyway: Apex requires at least 75% test coverage to deploy to production.

@isTest
private class CreateLeadFromEmailTest {

    @isTest
    static void createsLeadFromInboundEmail() {
        Messaging.InboundEmail email = new Messaging.InboundEmail();
        Messaging.InboundEnvelope env = new Messaging.InboundEnvelope();

        email.subject       = 'Demo request';
        email.fromName      = 'Jane Prospect';
        email.plainTextBody = 'Please send me a demo of your platform.';
        env.fromAddress     = 'jane@example.com';

        Test.startTest();
        Messaging.InboundEmailResult result =
            new CreateLeadFromEmail().handleInboundEmail(email, env);
        Test.stopTest();

        System.assertEquals(true, result.success, 'Handler should report success');

        Lead created = [SELECT Id, Email, LeadSource FROM Lead LIMIT 1];
        System.assertEquals('jane@example.com', created.Email);
        System.assertEquals('Inbound Email', created.LeadSource);
    }
}

Best practices for production inbound email handlers

  • Decide bounce vs swallow deliberately. Returning result.success = false triggers your failure response (the sender can be notified), while true accepts the message. Do not blanket-catch exceptions and always return true, or genuinely failed mail disappears silently.
  • Bulkify and respect limits. Use collections and a single DML per object; never query or insert inside a loop, so a heavy message does not blow a governor limit.
  • Validate and dedupe. Check the sender against your allowlist, sanity-check parsed values, and use matching logic or upsert to avoid duplicate Leads or Cases.
  • Alert a team on failure. When a handler hits an error, notify the right people - for instance, send an email to a public group so support or ops can investigate the unprocessed message.
  • Guard against spoofing. Use env.fromAddress for trust decisions, enable Advanced Email Security (SPF/DKIM), and restrict accepted senders on the service.
  • Log what you process. Persist a short audit (message id, sender, outcome) so you can trace what created which record.

Build it right with a Salesforce partner

Inbound email automation is easy to prototype and easy to get subtly wrong - silent failures, duplicate records, governor-limit errors under load, and spoofed senders are the usual traps. MicroPyramid has 12+ years and 50+ delivered projects designing Salesforce automation that holds up in production, from Apex email services and triggers to Flow, Email-to-Case, and Data Cloud. If you want inbound email turned into clean, deduplicated records, explore our Salesforce consulting and development services or get in touch.

Frequently Asked Questions

How do I create a Salesforce record from an inbound email?

Create an Apex class that implements the Messaging.InboundEmailHandler interface and a Salesforce Email Service that points to it. When email arrives at the service's generated address, Salesforce calls your handleInboundEmail(email, env) method. Inside it you read the subject, body, sender, and attachments, insert a Lead, Case, or custom record, and return a Messaging.InboundEmailResult with result.success = true.

What is the difference between an Email Service and Email-to-Case?

An Email Service runs your own Apex, so it can create any object with fully custom parsing and logic, but it requires code, a test class, and maintenance. Email-to-Case is a no-code Salesforce feature purpose-built to turn support email into Cases, with threading, auto-responses, and routing rules out of the box. Use Email-to-Case for standard support; use a custom Email Service when you need bespoke logic or non-Case objects.

How do I access email attachments in Apex?

Attachments arrive in two collections on the Messaging.InboundEmail object: binaryAttachments (a list of BinaryAttachment whose body is a Blob) for files like PDFs and images, and textAttachments (a list of TextAttachment whose body is a String) for text files. Loop over them and save each one as a ContentVersion (Salesforce Files), then link it to your record with a ContentDocumentLink.

What are the inbound email processing limits in Salesforce?

Normal Apex governor limits apply inside the handler (SOQL, DML, CPU, heap). On top of that, Salesforce caps how many inbound messages an org can process per day - the number of user licenses multiplied by 1,000, up to an org-wide daily maximum. There are also maximum message and body sizes. Confirm the current figures in Salesforce's email-service limits, as they change between releases.

What is the difference between email.fromAddress and env.fromAddress?

email.fromAddress is the address in the message's From: header - what the sender typed, which can be a friendly or spoofed address. env.fromAddress is the envelope sender the SMTP server actually used, so it is more reliable for trust and matching decisions. Use env.fromAddress for security checks and env.toAddress to see which service address received the mail.

How do I handle failures so emails do not get lost?

Wrap your logic in try/catch and set result.success = false when processing fails, which triggers the failure response you configured on the Email Service (such as bouncing the message back to the sender) instead of silently dropping it. Avoid catching every exception and always returning true. For visibility, log each outcome and notify a team or public group when a message cannot be processed so it can be handled manually.

Share this article