[SalesForce] How to use an Apex Email Service to store emails on a record EXACTLY as Salesforce does

I have used the Lightning Experience UI to send an Email from a Record Homepage via Activity-Tab. This looks like this (bottom-right!):

enter image description here

Now I want (vice versa) to store an incoming email received by an Apex Email Service in exactly the manner as Salesforce is doing that for outbound emails. At least I want to push I as close as possible or practical.

Note: I want that fancy stuff, too, like "Reply", "Forward", etc

I have reverse engineered what strange things Salesforce is doing for outbound emails:

  1. A Task of TaskSubtype = 'Email' will be created
  2. The WhatId of Task is pointing to the record from which I have sent the email
  3. An EmailMessage will be created with it's ActivityId set to the Task-Id
  4. Attachments will be stored as ContentDocument having ContentDocumentsLink to the EmailMessage, not to the Task

Now translating this recipe for inbound emails, my code should look something like this (which works fine, all but one line):

global class pfBCQuoteDocumentEmailService  implements Messaging.InboundEmailHandler {
    global Messaging.InboundEmailResult handleInboundEmail(Messaging.InboundEmail email, Messaging.InboundEnvelope envelope) {
        Messaging.InboundEmailResult result = new Messaging.InboundEmailresult();
        try {
            String                  tibisId     = email.Subject.substringAfter('_').substringBefore('_'); 
            pfBCQuote__c            quote       = (pfBCQuote__c)(xs.query(''
                + ' select * from pfBCQuote__c where pfBCTibisID__c = \''+tibisId+'\' ' 
            ).get(0));
            String                  closedLabel = (String)(xs.query(''
                + ' select * from TaskStatus where IsClosed = true limit 1 ' 
            ).get(0).get('ApiName'));

            Task                    createTask  = new Task(
                 TaskSubtype                    = 'Email'
                ,Subject                        = email.Subject + ' [EmailService]'
                ,ActivityDate                   = Date.today()
                ,Status                         = closedLabel
                ,WhatId                         = quote.Id
                ,Description                    = email.plainTextBody
            );
            insert createTask;
            EmailMessage            createEmail = new EmailMessage(
                 Subject                        = email.Subject + ' [EmailService]'
                // ,ActivityId                      = createTask.Id // NOT WRITABLE !
                ,Status                         = '1'
                ,CcAddress                      = xt.implode(',',email.ccAddresses)
                ,FromAddress                    = email.fromAddress
                ,FromName                       = email.fromName
                ,Headers                        = JSON.serialize( email.headers )
                ,HtmlBody                       = email.htmlBody
                ,TextBody                       = email.plainTextBody
                ,Incoming                       = true
                ,MessageDate                    = DateTime.now()

            );
            insert createEmail; 
            xt.logMail('pfBCQuoteDocumentEmailService.handleInboundEmail()',new Object[]{email,quote} );
        } catch(Exception e) {
            xt.logMail('ERROR :: pfBCQuoteDocumentEmailService.handleInboundEmail()',e);
        }
        return result;
    }
}

Sidenotes to the code (irrelevant to my main question)

  • xs.query() just selects stuff with * without all the hassel
  • pfBCQuote__c is the object to which I want to add the received Email
  • pfBCTibisID__c is a field on the record matching to a fragment of the incoming emails subject to get the appropriate record on which the email should be put in the activity tab.
  • xt.logMail() is simply send debug-emails to myself as alternative to creep through the logs – just ignore it.

What does NOT work is setting the ActivityId on the EmailMessage.

Insert failed. First exception on row 0; first error:
INSUFFICIENT_ACCESS_OR_READONLY, You cannot edit this field:
[ActivityId]

Now my question is: What is the BEST way to store incoming emails to work in the Activity-Tab best?

  • Should I only create the Task and go without the EmailMessage?
  • Is there a different way to link Task and EmailMessage?
  • How should I put received Attachments?
  • is there a way to do that more out-of-the-box with less reverse-engineering and try-to-replicate-what-salesforce-does?
  • is there specific documentation how Salesforce stores Emails on Record-Level?
  • Has anyone done something similar and is willing to share some code-snippets or conceptual approaches?
  • How would you store attachments? ContentDocument+Version or Attachment

What is working so far

  • Activity gets create with the right icon

enter image description here

What is still missing

  • fancy Reply, Forward stuff
  • handling of email attachments

UPDATE

I did some more research and found insufficient documentation – but at least a bit: https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_emailmessage.htm

Doing like my reverse-engineered recipe seems just to be right:

ActivityId

Type reference

Properties Create, Filter, Group, Nillable, Sort

ID of the activity that is associated with the email. Usually
represents an open task that is created for the case owner when a new
unread email message is received. ActivityId can only be specified for
emails on cases. It’s auto-created for other entities.

It looks like another castration of Apex compared to the API. Using the API the field ActivityId is usable on insert – only not updateable. Using APEX we get a punch and cannot even set it once on insert. We just can't set it at all. Hence we can't use it at all 🙁 Why that?

Also I think the only appropriate way to deal attachments is via ContentVersion with a link via something like Messaging.EmailFileAttachment. Going with the old Attachment just seems totally wrong. But I'm not sure on how to glue my EmailMessage to the ContentVersion. I will investigate that further, also happy to get hints from you!

Best Answer

From what I understand, You don't need to create a task to insert an EmailMessage. Email message has relatedToId field which you can use to directly link it to any desired record. As relatedToId is polymorphic like ParentId for Attachments, you should be fine.

Source: https://developer.salesforce.com/docs/atlas.en-us.212.0.api.meta/api/sforce_api_objects_emailmessage.htm

  1. fancy Reply, Forward stuff : This can be easily handled by creating a record of EmailMessageRelation. It can only be created if email-to-case is enabled in your org.

    SRC : https://developer.salesforce.com/docs/atlas.en-us.212.0.api.meta/api/sforce_api_objects_emailmessage.htm

SRC: Send and Receive Email in Account,Lead,Opportunity,Quotes,Contact and maintain email loop

2. handling of email attachments : I don't fancy using standard Salesforce Attachment. Because attaching them to different records means cloning it and you cant' controll access(Determined by parent always). Creating a content version and linking it to email message looks the best solution to me. I recently created a utility method for linking email attachments to emailMessage as content document , you can refer it.

    public void 
    createContentDocumentLinks(Messaging.InboundEmail.BinaryAttachment[] binAttachList,
        Id insertedEmailMessageId) {
    List<ContentVersion>cvList = new List<ContentVersion>();
    List<ContentDocumentLink> cdlList = new List<ContentDocumentLink>();
    if (binAttachList == null) {
        return null;
    }
    for (Messaging.InboundEmail.BinaryAttachment binAttach : binAttachList) {
        ContentVersion testContentInsert = new ContentVersion();
        testContentInsert.Title = binAttach.fileName;
        testContentInsert.VersionData = binAttach.body;
        testContentInsert.PathOnClient = '/' + binAttach.fileName ;
        cvList.add(testContentInsert);

    }
    insert cvList;
    cvList = [select id, ContentDocumentId from ContentVersion WHERE Id in :cvList];
    for (ContentVersion cv : cvList) {

        ContentDocumentLink cl = new ContentDocumentLink();
        cl.ContentDocumentId = cv.ContentDocumentId;
        cl.LinkedEntityId = insertedEmailMessageId;
        cl.ShareType = 'V';
        cl.Visibility = 'AllUsers';
        cdlList.add(cl);


    }
    insert cdlList;


    }
Related Topic