function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Ido Greenbaum 10Ido Greenbaum 10 

custom Email Service - inline images appear as '[Image is no longer available]' upon Reply/ReplyAll

I recently completed a customized Email Service, to replace Email-To-Case, which handles duplicates by matching the incoming email subject with existing cases.
As part of the InboundEmailHandler class, I am handling the binary attachment Images under the following function:
 
private static String getImageReference(List<Messaging.InboundEmail.Header> headers){
        for(Messaging.InboundEmail.Header header: headers){
            if(header.name.startsWith('Content-ID') || header.name.startsWith('X-Attachment-Id') || (header.value.startsWith('ii') || header.value.startsWith('< image'))){
                return header.value.replaceAll('<', '').replaceAll('>', '');
            }
        }
        return null;
    }


This is working fine, and the Images appear inline when viewing the incoming Email. The problem I'm facing - when using 'Reply' / 'Reply All' of the Email Message feed item in the Case object, the Email Publisher sets the Email Thread, but the inline images are replaces with: [Image is no longer available]
Can anyone suggest a valid solution for this? As a side note - in the traditional Email-To-Case, the email thread includes the inline images properly upon reply. If they only shared the source code of this solution....
Thank you.
NagendraNagendra (Salesforce Developers) 
Hi Greenbaum,

Please find the suggested solution from stack exchange community for a similar issue.
After saving off the attachments then I updated the message body to replace the cid references with URL to the files. For some magical reason, Salesforce will properly format the reply in a way that the inline images appear.

There are two files: 1. EmailUtils 2. Email2CaseHandler

The magic occurs in Email2CaseHandler lines 40-56.

EmailUtils :
/**
 * Developed by Doug Ayers
 */
public class EmailUtils {

    /**
     * Builds a map of Attachment objects copying values from the Files attached to the InboundEmail argument.
     * The map keys are the attachment header 'Content-ID' values.
     * The map values are unsaved Attachment records with data copied from the email.
     *
     * Use of Content-ID header is inspired by http://simplyforce.blogspot.com/2014/08/handling-inline-images-in-salesforce.html
     * so that Email2CaseHandler can replace image sources in the html body with links to actual attachments
     * so that the images render when viewing the HTML version of this email message.
     *
     * No database changes are made, you must save the Attachments.
     */
    public static Map<String, Attachment> buildAttachments( Messaging.InboundEmail email, ID parentId ) {

        Map<String, Attachment> attachmentsMap = new Map<String, Attachment>();

        // if attachment does not have Content-ID header
        // then we'll use this to generate a unique map key instead
        Integer noHeaderCount = 0;

        if ( email.binaryAttachments != null ) {

            for ( Messaging.InboundEmail.BinaryAttachment binaryAttachment : email.binaryAttachments ) {

                String contentId = getHeaderValue( binaryAttachment.headers, 'Content-ID' );

                if ( String.isBlank( contentId ) ) {
                    contentId = 'no-content-id-header-' + noHeaderCount++;
                }

                attachmentsMap.put( contentId, new Attachment(
                    name = binaryAttachment.fileName,
                    body = binaryAttachment.body,
                    contentType = binaryAttachment.mimeTypeSubType,
                    parentId = parentId
                ));

            }

        }

        if ( email.textAttachments != null ) {

            for ( Messaging.InboundEmail.TextAttachment textAttachment : email.textAttachments ) {

                String contentId = getHeaderValue( textAttachment.headers, 'Content-ID' );

                if ( String.isBlank( contentId ) ) {
                    contentId = 'no-content-id-header-' + noHeaderCount++;
                }

                attachmentsMap.put( contentId, new Attachment(
                    name = textAttachment.fileName,
                    body = Blob.valueOf( textAttachment.body ),
                    contentType = textAttachment.mimeTypeSubType,
                    parentId = parentId
                ));

            }

        }

        return attachmentsMap;
    }

    /**
     * Builds a new EmailMessage object copying values from the InboundEmail argument.
     * Sets the RelatedToId and/or ParentId value as appropriate.
     *
     * No database changes are made, you must save the EmailMessage.
     */
    public static EmailMessage buildEmailMessage( Messaging.InboundEmail email, ID relatedToId ) {

        // for really long emails need to truncate text
        Integer maxLengthPlainTextBody = EmailMessage.TextBody.getDescribe().getLength();
        Integer maxLengthHtmlBody = EmailMessage.HtmlBody.getDescribe().getLength();
        Integer maxLengthSubject = EmailMessage.Subject.getDescribe().getLength();

        String plainTextBody = ( String.isBlank( email.plainTextBody ) ? '' : email.plainTextBody.abbreviate( maxLengthPlainTextBody ) );
        String htmlBody = ( String.isBlank( email.htmlBody ) ? '' : email.htmlBody.abbreviate( maxLengthHtmlBody ) );
        String subject = ( String.isBlank( email.subject ) ? '' : email.subject.abbreviate( maxLengthSubject ) );

        EmailMessage message = new EmailMessage(
            textBody = plainTextBody,
            htmlBody = htmlBody,
            subject = subject,
            fromName = email.fromName,
            fromAddress = email.fromAddress,
            incoming = true
        );

        // check if enhanced email feature is enabled
        // if yes then the 'RelatedToId' field exists, otherwise it doesn't
        // https://help.salesforce.com/apex/HTViewHelpDoc?id=enable_enhanced_email.htm&language=en_US
        Map<String, Schema.SObjectField> fieldsMap = EmailMessage.sObjectType.getDescribe().fields.getMap();
        if ( fieldsMap.containsKey( 'RelatedToId' ) ) {
            // use dynamic put method because can't compile if field doesn't actually exist for dot notation
            message.put( 'RelatedToId', relatedToId );
        }

        // check if the relate to id is for a case
        // if yes then populate the 'ParentId' (precursor to Enhanced Email feature)
        String caseKeyPrefix = Case.sObjectType.getDescribe().getKeyPrefix();
        String relatedToIdPrefix = relatedToId.getSobjectType().getDescribe().getKeyPrefix();
        if ( caseKeyPrefix == relatedToIdPrefix ) {
            message.parentId = relatedToId;
        }

        if ( email.headers != null && email.headers.size() > 0 ) {
            message.headers = toString( email.headers );
        }

        if ( email.toAddresses != null && email.toAddresses.size() > 0 ) {
            message.toAddress = String.join( email.toAddresses, ';' );
        }

        if ( email.ccAddresses != null && email.ccAddresses.size() > 0 ) {
            message.ccAddress = String.join( email.ccAddresses, ';' );
        }

        return message;
    }

    private static String getHeaderValue( List<Messaging.InboundEmail.Header> headers, String name ) {

        String value = null;

        if ( headers != null ) {
            for ( Messaging.InboundEmail.Header header : headers ) {
                if ( header.name == name ) {
                    value = header.value;
                    break;
                }
            }
        }

        return value;
    }

    private static String toString( List<Messaging.InboundEmail.Header> headers ) {

        String text = '';

        if ( headers != null ) {
            for ( Messaging.InboundEmail.Header header : headers ) {
                text += header.name + '=' + header.value + '\n';
            }
        }

        return text;
    }

}
Email2CaseHandler :
/**
 * Developed by Doug Ayers
 */
public with sharing class Email2CaseHandler implements Messaging.InboundEmailHandler {

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

        Messaging.InboundEmailResult result = new Messaging.InboundEmailResult();

        SavePoint sp = Database.setSavePoint();

        try {

            System.debug( 'Handling inbound email: ' + email );
            System.debug( envelope );

            Case myCase = null;

            // ... logic to find or create case ...

            // create EmailMessage related to Case
            System.debug( 'Creating email message' );
            EmailMessage message = EmailUtils.buildEmailMessage( email, myCase.id );
            insert message;

            // add attachments to EmailMessage
            System.debug( 'Adding attachments' );
            Map<String, Attachment> attachmentsMap = EmailUtils.buildAttachments( email, message.id );
            if ( attachmentsMap.size() > 0 ) {
                insert attachmentsMap.values();
            }

            // Parse email body and replace references to "cid" content ids
            // with attachment file urls so render when email message viewed in salesforce.
            //
            // Use of Content-ID header is inspired by http://simplyforce.blogspot.com/2014/08/handling-inline-images-in-salesforce.html
            // so that Email2CaseHandler can replace image sources in the html body with links to actual attachments
            // so that the images render when viewing the HTML version of this email message.

            Organization org = [ SELECT instanceName FROM Organization WHERE id = :UserInfo.getOrganizationId() LIMIT 1 ];

            String attachmentDownloadURL = 'https://c.' + org.instanceName.toLowerCase() + '.content.force.com/servlet/servlet.FileDownload?file=';

            for ( String contentId : attachmentsMap.keySet() ) {

                String cid = contentId.replace( '<', '' ).replace( '>', '' );
                String url = attachmentDownloadURL + String.valueOf( attachmentsMap.get( contentId ).id ).left( 15 );

                System.debug( 'replacing image references in html body: cid=' + cid + ', url=' + url );

                message.textBody = message.textBody.replaceAll( 'cid:' + cid, url );
                message.htmlBody = message.htmlBody.replaceAll( 'cid:' + cid, url );

            }

            update message;

            // if result is false then salesforce does not commit DML changes
            result.success = true;

        } catch ( Exception e ) {

            System.debug( LoggingLevel.ERROR, e.getStackTraceString() );

            result.message = e.getMessage() + '\n' + e.getStackTraceString();
            result.success = false;

        }

        if ( result.success == false ) {
            if ( sp != null ) {
                System.debug( 'Rolling back transaction' );
                Database.rollback( sp );
            }
        }

        return result;
    }

}
Hope this helps.

Please mark this as solved if it's resolved so that it gets removed from the unanswered queue which results in helping others who are encountering a similar issue.

Thanks,
Nagendra