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.

2 Answers 2

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

}
1
  • Great implementation for a custom emailhandler. Only thing I’d suggest is to override the result.success and force it to true. There is a poorly documented fact, that if result.success = false it generates an Exception Email back to the Sender regardless of your Email Service settings in Salesforce. This can tarnish your organisations reputation when customers start getting weird failure emails if something goes wrong. To avoid, handle the error notification to your admins manually inside your handler code (inside the catch block) and then force result.success = true before returning result Commented Apr 27 at 14:19
0

I figured out the root cause of this issue after spending 2 full weeks on it.

Change this:

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

To this:

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

You must log in to answer this question.

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