6

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.

1 Answer 1

6
+50

This is how I solved the problem, 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.

This solution is inspired by Bhawani Sharma.

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

}

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .