本文へ移動
cccskills
無料GitHub で公開

email-service-inbound

Inbound email processing in Salesforce via Email Services + the `Messaging.InboundEmailHandler` Apex interface. Covers EmailService configuration (running user, accept-from address, attachment handling, error / failure routing), the EmailServicesAddress per-routing-address pattern, the handler's `Messaging.InboundEmail` payload (text body, HTML body, headers, attachments, in-reply-to threading), and the canonical Email-to-Case alternative for case creation. NOT for outbound email (use admin/email-templates-and-alerts), NOT for Email-to-Case flow customization itself (use admin/email-to-case-configuration). Trigger keywords: EmailServicesFunction, EmailServicesAddress, runAsUser, localPart, EmailDomainName, attachmentOption, overLimitAction, errorRoutingAddress, routing address, inbound email limit.

インストール方法を見る

含まれるファイル(8)

  • SKILL.md26.5 KB
  • references/examples.md7.0 KB
  • references/gotchas.md15.3 KB
  • references/llm-anti-patterns.md7.4 KB
  • references/metadata-examples.md16.7 KB
  • references/well-architected.md7.1 KB
  • scripts/check_email_service_inbound.py15.4 KB
  • templates/email-service-inbound-template.md2.5 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

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 — admin/email-to-case-configuration. 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 each Email Services Address (runAsUser). 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 (~25 MB combined email size, max attachment size, total org file storage). Decide store vs discard before traffic ramps.

Questions to Ask Before Configuring

Each answer sets a specific, Required element on the EmailServicesFunction record. Skipping one does not leave a blank — it leaves a default whose failure mode nobody chose.

AskWhy it mattersWhat a good answer adds
"If we are switched off during a cutover, or the org hits its daily inbound limit — hold the mail, bounce it, or drop it?"functionInactiveAction and overLimitAction are Required; Discard loses messages with no notice to anyoneTwo explicit enum values instead of UseSystemDefault, and a documented data-loss position
"Who gets paged when a message fails to process — the sender, or us?"Without isErrorRoutingEnabled + errorRoutingAddress, notifications go to the sender and intake breaks silentlyA monitored internal address, and a rule that failure text sent outward stays generic
"Which username does each address run as, and what can that user see?"runAsUser is Required per address, so two addresses on one service can run the same Apex with different visibilityOne named integration user per address plus its permission set
"Which senders are legitimate, and must we verify the sending server?"authorizedSenders filters before Apex; isAuthenticationRequired turns on SPF / SenderId / DomainKeys checksAn allow-list, and a decision on authenticationFailureAction for unverified mail
"What do we need from attachments — nothing, filenames only, text, binary, or everything?"attachmentOption filters at the platform layer before the bytes reach the heapThe right enum, plus size / count / MIME caps for whatever survives it
"How many inbound messages a day, across every email service and On-Demand Email-to-Case?"The daily limit is org-wide (user licences x 1,000, max 1,000,000), not per serviceA capacity number checked against licence count before the service exists
"Who owns re-pointing the forwarding rule after each sandbox refresh?"The routing domain is generated per address per org and cannot be promoted from sandboxA named owner and the address stored in Custom Metadata, not in Apex

What a proper configuration adds over just writing the handler: the mail that arrives while you are deploying, over quota, or failing is held rather than lost, the people who need to know are told, and the address survives a sandbox refresh as a data edit rather than a re-deployment.


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.

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.

EmailServicesAddress is not a separate deployable type: addresses are nested inside the EmailServicesFunction file as repeated <emailServicesAddresses> elements. The fields that decide behaviour:

ElementLevelRequiredWhat it decides
apexClassserviceyesThe handler. Must exist in the target org, or ship in the same payload
attachmentOptionserviceyesNone / NoContent / TextOnly / BinaryOnly / All — platform-level attachment filter
isTextAttachmentsAsBinaryservicenotrue delivers text attachments as BinaryAttachment (Blob), not TextAttachment (String)
authorizedSendersservice and addressnoSender allow-list. Blank accepts anyone
authorizationFailureActionserviceyesWhat an unlisted sender gets — even when the allow-list was set on the address
isAuthenticationRequired / authenticationFailureActionserviceno / yesSPF, SenderId and DomainKeys verification of the sending server
isErrorRoutingEnabled / errorRoutingAddressservicenoSend failure notifications to an internal address instead of the sender
functionInactiveActionserviceyesMail arriving while the service is off: UseSystemDefault / Bounce / Discard / Requeue
overLimitActionserviceyesMail arriving after the org's daily limit: same four values
runAsUseraddressyesUsername whose permissions the handler assumes for mail to that address
localPartaddressyesThe string before the @. The domain is system-generated and read-only

Deployable XML, the handler, the test class and the post-deploy verification SOQL: references/metadata-examples.md.

Messaging.InboundEmailHandler interface

public with sharing class IncomingQuoteHandler implements Messaging.InboundEmailHandler {
    public Messaging.InboundEmailResult handleInboundEmail(
        Messaging.InboundEmail email,
        Messaging.InboundEnvelope envelope
    ) {
        Messaging.InboundEmailResult result = new Messaging.InboundEmailResult();
        try {
            processQuote(email);
            result.success = true;
        } catch (Exception ex) {
            ApplicationLogger.error('Quote email failed', ex);
            result.success = false;
            // Mailed to the sender: no exception text, no record Ids.
            result.message = 'We could not process this request. '
                + 'Please contact support@acme.example.';
        }
        return result;
    }
}

Three things to know:

  1. global is not required. The Apex Developer Guide's own samples (CreateTaskEmailExample, unsubscribe) are public with sharing. Use global only when the class must be visible outside its namespace — a managed package. (references/gotchas.md § 1.)
  2. InboundEmail payload — subject, fromAddress, fromName, toAddresses, ccAddresses, replyTo, plainTextBody, htmlBody, the matching plainTextBodyIsTruncated / htmlBodyIsTruncated flags, messageId, inReplyTo, references, authenticationResults, binaryAttachments, textAttachments, and headers (InboundEmail.Header[], exposing only name and value). InboundEnvelope carries just fromAddress and toAddress.
  3. Return value. success = true → delivery confirmed. success = false → Salesforce rejects the email and replies to the sender with message. message is sent irrespective of success, so a successful acknowledgement reply is also possible.

Email threading via In-Reply-To and References headers

Email clients thread replies by Message-Id and In-Reply-To headers. Salesforce has already parsed all three onto the object — do not iterate headers for them:

Set<String> parents = new Set<String>();
if (String.isNotBlank(email.inReplyTo)) {
    parents.add(email.inReplyTo);          // String
}
if (email.references != null) {
    parents.addAll(email.references);      // String[]
}
String mine = email.messageId;             // this message's own Message-ID

references is documented as containing the parent emails' References and message IDs "and possibly the In-Reply-To fields", so it often survives a client that dropped inReplyTo. Check both, then fall back to a subject token. Store your own outbound Message-Id on the record (or use EmailMessage.MessageIdentifier, which is idLookup-enabled) so the lookup has something to match.

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-To against 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;
}

The attachment lists are null, not empty, when nothing was sent, and attachmentOption = NoContent delivers metadata with att.body set to null — guard both before touching body. Collect the records and issue one DML statement after the loop.

Limits:

  • Max email size: email services reject a message whose combined body text, body HTML and attachments exceeds approximately 25 MB, varying with language and character set. This is a platform ceiling, not a per-service setting — there is no maxEmailSize element. (references/gotchas.md § 4.)
  • Heap: email services get 50 MB, not the 6 MB synchronous budget. Do the Blob work inside the handler; hand off a ContentVersion Id, not a Blob. (references/gotchas.md § 11.)
  • Daily messages: org-wide across all email services and On-Demand Email-to-Case — number of user licences multiplied by 1,000, maximum 1,000,000. Overflow is handled by overLimitAction.
  • Org-wide file storage: every saved attachment counts against the org's File Storage allocation. Plan retention.

Email-to-Case vs custom service decision

NeedUse
Create a Case from an emailEmail-to-Case
Auto-response from a templateEmail-to-Case (or On-Demand E2C)
Threading replies to existing CaseEmail-to-Case with [ref:...] token
Create a Lead / Opportunity / Custom ObjectCustom service
Upload a file to a recordCustom service
Trigger a downstream API calloutCustom service
Complex routing (e.g. "if subject starts with X, do Y")Custom service (or E2C with assignment rules — depends)
Multi-language / encoded payloadsCustom 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.

public with sharing class LeadFromEmail implements Messaging.InboundEmailHandler {
    public 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'
};

public 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. Note the platform already does the coarse version for free: authorizedSenders on the service or the address rejects unlisted senders before Apex runs, and authorizationFailureAction (service level) decides whether they are bounced or discarded. Reach for Apex allow-listing when the rule is per-sender-plus-content, not per-domain.


Decision Guidance

SituationApproachReason
Create a Case from emailEmail-to-CaseBuilt-in; threading, routing, auto-response included
Create any other record from emailCustom Email Service + InboundEmailHandlerE2C only creates Cases
Public address receives spamCustom service with allow-listE2C also has spam handling but per-Case
Need to upload filesCustom serviceE2C Email Message attachments tied to Case
Threading replies to existing recordinReplyTo + references first, subject token as fallbackThe identifiers come from the sender's client and are not always present; the token is in the visible subject
Inbound volume approaching (user licences x 1,000)Model the org-wide daily budget before adding the serviceThe limit is shared across all email services and On-Demand Email-to-Case; overflow is handled by overLimitAction
Multi-language / RTL / encoded text attachmentCustom handler reading TextAttachment.charsetThe body is re-encoded as UTF-8 for Apex; the original character set survives only on that property
Inbound email triggers a calloutCustom service + Platform Event for async workDon't do the callout in the handler synchronously
Anonymous public access undesirableauthorizedSenders + authorizationFailureActionPlatform-level allow-list; rejects before Apex runs. Add isAuthenticationRequired to verify the sending server via SPF / SenderId / DomainKeys

Recommended Workflow

  1. Route the requirement. Case creation goes to admin/email-to-case-configuration — stop here. Anything else continues. Run the ## Questions to Ask Before Configuring table and record the answers in templates/email-service-inbound-template.md; each row names the element it sets.
  2. Provision the running user(s). One integration user per address, with a named permission set. runAsUser is a username, per address, and Required — an address without one fails the deploy.
  3. Write the handler against the shape in references/metadata-examples.md § The handler class: public with sharing, thread on inReplyTo / references with a subject-token fallback, null-guard both attachment lists, collect then insert once, and keep exception detail out of InboundEmailResult.message.
  4. Write the test class from references/metadata-examples.md § The test class — new Messaging.InboundEmail() and new Messaging.InboundEnvelope() build the payload directly. Cover first email, threaded reply, binaryAttachments = null, and an attachment refused by policy.
  5. Author the EmailServicesFunction XML. Copy the service block from references/metadata-examples.md, then set the six Required elements deliberately — apexClass, attachmentOption, authenticationFailureAction, authorizationFailureAction, functionInactiveAction, overLimitAction — plus one nested <emailServicesAddresses> per intake population.
  6. Check before deploying.
    python3 skills/admin/email-service-inbound/scripts/check_email_service_inbound.py \
        --manifest-dir force-app/main/default
    
    It flags an unresolvable apexClass, an inactive service, an address missing runAsUser, isErrorRoutingEnabled with no errorRoutingAddress, an invented maxEmailSize, DML inside an attachment loop, a stack-trace leak and a synchronous callout. Then sf project deploy start --dry-run with the handler test.
  7. Read the routing address back and smoke-test. The domain part is generated per org, so query SELECT LocalPart, EmailDomainName, IsActive FROM EmailServicesAddress WHERE Function.FunctionName = '<name>', send a real message to LocalPart@EmailDomainName, and verify the success path, the failure reply, and that errorRoutingAddress received the failure notification. Store the address in Custom Metadata — it changes on every sandbox refresh.

Review Checklist

  • Handler is public with sharing unless it ships in a managed package.
  • Handler returns InboundEmailResult with an explicit success value (never silently throws); message carries no exception detail.
  • Both attachment lists null-guarded; att.body null-guarded; one DML statement after the loop, not inside it.
  • runAsUser set on every address, each a documented integration user with a named permission set.
  • attachmentOption chosen deliberately; size / count / MIME caps in Apex on whatever it lets through.
  • authorizedSenders + authorizationFailureAction set for public-facing addresses.
  • isErrorRoutingEnabled true with a monitored errorRoutingAddress.
  • functionInactiveAction and overLimitAction are not UseSystemDefault.
  • Threading reads inReplyTo and references, with a subject-token fallback.
  • Truncation flags (plainTextBodyIsTruncated, htmlBodyIsTruncated) read before parsing.
  • Test class covers first email, threaded reply, null attachment list, and a policy-refused attachment.
  • Routing address read from EmailServicesAddress.EmailDomainName, stored in Custom Metadata, never hardcoded.
  • scripts/check_email_service_inbound.py --manifest-dir … is clean.

Salesforce-Specific Gotchas

  1. global is a packaging decision, not an interface requirement. The guide's own samples are public with sharing. (§ 1.)
  2. email.headers is InboundEmail.Header[], not a Map — and the three headers you want are already parsed onto the object. (§ 2.)
  3. success = false mails your message to the sender. Never a stack trace. message is also sent when success is true. (§ 3.)
  4. The ~25 MB ceiling is a platform limit, not a service setting. There is no maxEmailSize element to raise. (§ 4.)
  5. The address domain is generated per address per org and is read-only. Query EmailDomainName; never hardcode. (§ 5.)
  6. runAsUser is Required per address, not per service — two addresses on one service can run the same Apex as different users. (§ 6.)
  7. inReplyTo comes from the sender's client; check references too and keep a subject-token fallback. (§ 7.)
  8. The daily message limit is org-wide and shared with On-Demand Email-to-Case: licences x 1,000, max 1,000,000. (§ 10.)
  9. Email-service Apex gets a 50 MB heap, not 6 MB — keep the Blob work in the handler. (§ 11.)
  10. Sandbox routing addresses cannot be copied to production, and refreshes regenerate them. (§ 12.)
  11. authorizationFailureAction is service-level even when authorizedSenders was set on the address. (§ 13.)
  12. Bodies and text attachments arrive truncated with a flag you must read before parsing. (§ 14.)
  13. EmailServicesFunction accepts no * wildcard in package.xml. Name every service. (§ 16.)

Output Artifacts

ArtifactDescription
Messaging.InboundEmailHandler classThe handler implementation (references/metadata-examples.md)
Test classSynthetic InboundEmail / InboundEnvelope payloads; four named cases
emailservices/<Name>.emailservices-meta.xmlThe service plus its nested address records
package.xml entriesApexClass + EmailServicesFunction (no wildcard)
Running-user permission setOne per address, named in the config workbook
Allow-list sourceCustom Metadata / Custom Setting for admin-managed sender list
Threading strategyinReplyTo / references lookup plus the subject-token format
Routing-address recordLocalPart + EmailDomainName per org, stored in Custom Metadata

Reference Files

FileRead it when
references/metadata-examples.mdYou are writing the EmailServicesFunction XML, the handler, the test class or the package.xml, or you need the post-deploy SOQL that reveals the routing address
references/gotchas.mdA configured service behaves unexpectedly — mail bounced or vanished, attachments missing or the wrong Apex type, an address that worked in sandbox and not in production, a global/public compile argument
references/examples.mdYou want worked before/after code: safe failure messages, threaded case lookup, Custom-Metadata allow-listing, and the two-layer attachment policy
references/llm-anti-patterns.mdYou are reviewing AI-generated inbound-email code or advice, or want the detection hints for each recurring mistake
references/well-architected.mdYou are justifying the design — pillar mapping, the tradeoffs behind one-service-many-addresses, and the official sources behind every claim here

Supporting files: templates/email-service-inbound-template.md (work template mapping each decision to its metadata element) and scripts/check_email_service_inbound.py --manifest-dir <dir> (metadata + handler checks).


Related Skills

  • admin/email-to-case-configuration — when the requirement is case creation; this skill is the custom-service alternative.
  • admin/email-templates-and-alerts — outbound email infrastructure, including the reply your handler acknowledges with.
  • apex/apex-email-services — the Apex-side treatment of the same handler; this skill owns the service configuration.
  • 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.
  • admin/custom-metadata-types — where the sender allow-list and the per-org routing address belong.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Use this skill when writing test-first, behavior-driven acceptance criteria in Given/When/Then format for a Salesforce user story. Covers happy path, edge cases, negative paths, permission boundaries, and data-state preconditions so the AC block can drive UAT scripts and Apex test design downstream. Trigger keywords: given when then, gherkin, behavior driven AC, test first acceptance criteria, scenario outline, BDD acceptance criteria. NOT for the user-story format itself (use admin/user-story-writing-for-salesforce). NOT for UAT script writing (use admin/uat-test-case-design). NOT for Apex test method generation (use agents/test-class-generator). NOT for high-level UAT planning (use admin/uat-and-acceptance-criteria). More triggers: acceptance criteria record, ac_id, req_id on a criterion, artefact under test, negative case for a rule-type requirement, no-match fall-through criterion, oracle for a Then clause, test type apex flow manual, criteria linter, check_ac_format.py, --manifest-dir.

日本語の概要は準備中です。原文の説明を表示しています。

PranavNagrecha/AwesomeSalesforceSkills192026年10月4日 更新

Task and Event objects: polymorphic WhatId/WhoId, Activity object model, ActivityHistory vs OpenActivity, activity timeline customization, bulk task creation, Einstein Activity Capture boundaries. NOT for turning on EAC or calendar sync — use admin/einstein-activity-capture-setup. NOT for Email-to-Case — use admin/case-management-setup. Covers ActivitiesSettings metadata, enableActivities, shared Task/Event custom fields and FLS, TaskStatus value sets, Shared Activities and TaskRelation/EventRelation, recurring and child activities, and partial-success bulk DML on Task.

日本語の概要は準備中です。原文の説明を表示しています。

PranavNagrecha/AwesomeSalesforceSkills192026年10月4日 更新

Design Invocable Apex actions that return deterministic, agent-friendly errors instead of surfacing raw exceptions to the LLM. NOT for authoring the action itself — @InvocableMethod schema, labels, security context — use agentforce/custom-agent-actions-apex. NOT for the Apex tests that force each error branch — use agentforce/agent-action-unit-tests.

日本語の概要は準備中です。原文の説明を表示しています。

PranavNagrecha/AwesomeSalesforceSkills192026年10月4日 更新

Use when designing how an Agentforce agent extracts structured input parameters (slots) from a user's natural-language utterance to invoke an action. Triggers: 'agent extract date from user', 'agent action argument extraction', 'agentforce slot filling', 'agent invocable input mapping', 'agent fails to fill required parameter'. NOT for action authoring (use agentforce/agent-actions) or for prompt-template variable binding (use agentforce/prompt-builder-templates).

日本語の概要は準備中です。原文の説明を表示しています。

PranavNagrecha/AwesomeSalesforceSkills192026年10月4日 更新

Apex test patterns for @InvocableMethod agent actions: per-reason-code branch coverage, bulk safety, callout mocking, deterministic assertions. NOT for testing whether the agent routes to the right topic or answers well — use agentforce/agent-testing-and-evaluation. NOT for designing the error envelope the tests assert on — use agentforce/agent-action-error-handling.

日本語の概要は準備中です。原文の説明を表示しています。

PranavNagrecha/AwesomeSalesforceSkills192026年10月4日 更新

Designing or reviewing Agentforce actions: Flow actions, Apex invocable actions, prompt-template actions, action naming, input and output contracts, confirmation requirements, and safe error behavior. Triggers: 'agent actions', 'build an agent that can look up an order', 'make the agent able to check order status', 'give the agent a capability', 'agent that can retrieve a record', 'flow action for agent', 'agent invocable action', 'action schema design'. NOT for writing the Apex class itself — use agentforce/custom-agent-actions-apex. NOT for the reason_code error envelope that stops an agent retry loop — use agentforce/agent-action-error-handling.

日本語の概要は準備中です。原文の説明を表示しています。

PranavNagrecha/AwesomeSalesforceSkills192026年10月4日 更新

PranavNagrecha のスキルをすべて見る

このスキルの問題を報告する