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