For example, our customer writes an email with the subject "another question". They send the email to our support email address which forwards the email to our instance of Salesforce. The built-in handler creates a new Case. Our staff will respond using the Salesforce email tools and the customer gets their answer. The subject line of our email to the customer looks like this:
RE: another question [ ref:00D7JFzw.5007KcZYu:ref ]
When the customer writes back the email handler will decipher the reference and append the response to the original case. All this is great. The problem comes when that reference Id is not part of the incoming email because this creates a new Case. The problem is worse when many people are involved in the email problem and they all continue correspondence without including the reference. Our staff can spend many hours manually combining the resulting Cases.
My plan is to build my own email handler that can match email subject lines and combine the cases for my users.
This handler can also solve another problem for my users: get rid of those cryptic and long references. Who wants to see "[ ref:00D7JFzw.5007KcZYu:ref ]" in every subject line. What does this mean to our users? Why not link the email to the case based on Case Number?
RE: another problem Case: 0031452
Doesn't that look better? It is also easy to train our staff and regular customers to include the Case Number in any email to discuss a particular issue.
Here is class with code that works for us. I'm posting it without comment just to get this code out. Perhaps in a follow up post I'll explain the parts.
public with sharing class CaseEmailInBoundUtilities { private Static Boolean TRACE = true; protected Case theCase = null; protected Messaging.InboundEmail inboundEmail; protected String defaultCaseOwnerId; public CaseEmailInBoundUtilities() { Group[] queues = [select Id, Name from Group where Name = 'Triage']; this.defaultCaseOwnerId = queues[0].Id; } public Messaging.InboundEmailResult processInboundEmail(Messaging.InboundEmail email) { Messaging.InboundEmailResult result = new Messaging.InboundEmailresult(); result.success = true; this.inboundEmail = email; String caseNumber = extractRef(email.subject); if(caseNumber != null) { if(TRACE)system.debug(Logginglevel.ERROR,'CaseEmailInBoundUtilities. extracted case number: "' + caseNumber + '"'); this.theCase = locateByCaseNumberAsString(caseNumber); if(this.theCase == null) { // TODO email error message to SysAdmin system.debug(Logginglevel.ERROR,'CaseEmailInBoundUtilities. Create a new case. Could not find case number: "' + caseNumber + '"'); } } else { // try to match subject String mainSubject = extractMainSubject(email.subject); Case[] matchingCases = [Select Id, CaseNumber, Subject, Description from Case where Subject = :mainSubject and CreatedDate = LAST_N_DAYS:5]; if(matchingCases.size() == 1) { this.theCase = matchingCases[0]; } else { system.debug(Logginglevel.ERROR,'CaseEmailInBoundUtilities. Create a new case because we found '+matchingCases.size() + ' cases with the subject: "' + mainSubject + '"'); } } if(this.theCase == null) { // else create a new Case this.theCase = new Case(); theCase.SuppliedEmail = email.fromAddress; theCase.SuppliedName = email.fromName; theCase.Status = 'New'; theCase.Priority = 'Low'; theCase.OwnerId = this.defaultCaseOwnerId; theCase.Origin = 'Email'; theCase.Subject = email.Subject; theCase.Description = email.plainTextBody; Contact[] contacts = [SELECT Id, Name, AccountId, Email FROM Contact WHERE Email = :email.fromAddress]; if(contacts.size() >0) { Contact theContact = contacts[0]; theCase.ContactId = theContact.Id; theCase.AccountId = theContact.AccountId; if(contacts.size() > 1) { // Could-Should create a new Case here to get CS to resolve this.... theCase.Description = 'Note: there is more than on Contact with this email address. Fix this. ' + theCase.Description; } } insertSObject(this.theCase); } createEmailMessage(theCase,email); handleAttachments(theCase, email); // send success notification. This can be disabled once we know the code is stable. // if(result.success) // { // String successMessage = 'Successful processing of inbound email \n'+ noteBody; // TriggerErrorNotification.reportInfo(messagePrefix+ ' success ',successMessage); // } return result; } // Save attachments, if any private void handleAttachments(Case theCase, Messaging.InboundEmail email) { if(email.textAttachments!=null && email.textAttachments.size() >0) { for (Messaging.Inboundemail.TextAttachment tAttachment : email.textAttachments) { Attachment attachment = new Attachment(); attachment.Name = tAttachment.fileName; attachment.Body = Blob.valueOf(tAttachment.body); attachment.ParentId = theCase.Id; insertSObject(attachment); } } if(email.binaryAttachments!=null && email.binaryAttachments.size() >0) { for (Messaging.Inboundemail.BinaryAttachment bAttachment : email.binaryAttachments) { Attachment attachment = new Attachment(); attachment.Name = bAttachment.fileName; attachment.Body = bAttachment.body; attachment.ParentId = theCase.Id; insertSObject(attachment); } } } private void insertSObject(sObject obj) { try {insert obj;} catch (System.DmlException e) {handleError(e, 'Could not insert obj '+ obj);} } private String limitLength(String input, Integer maxLength) { String results; if(input != null && input.length() > maxLength) results = input.substring(0,maxLength); else results = input; return results; } private void createEmailMessage(Case theCase, Messaging.InboundEmail email) { String value; Integer maxlength; EmailMessage theEmail = new EmailMessage(); theEmail.ParentId = theCase.Id; theEmail.Incoming = true; Schema.DescribeFieldResult F = EmailMessage.HtmlBody.getDescribe(); //.HtmlBody.getDescribe(); maxlength = F.getLength(); theEmail.Subject = limitLength(email.Subject, EmailMessage.Subject.getDescribe().getLength()); theEmail.MessageDate = datetime.now(); theEmail.HtmlBody = limitLength(email.htmlBody,EmailMessage.HtmlBody.getDescribe().getLength()); theEmail.TextBody = limitLength(email.plainTextBody,EmailMessage.TextBody.getDescribe().getLength()); /* **** To */ value = ''; if(email.toAddresses != null) { Boolean seenOne= false; for(String to : email.toAddresses) { if(seenOne) { value += ';\n'; } to = extractAddress(to); system.debug('ToAddress: ' + to); value += to; seenOne = true; } } theEmail.ToAddress = limitLength(value,EmailMessage.ToAddress.getDescribe().getLength()); /* **** From */ theEmail.FromName = email.fromName; theEmail.FromAddress = email.fromAddress; /* **** CC */ value = ''; if(email.ccAddresses != null) { Boolean seenOne= false; for(String cc : email.ccAddresses) { if(seenOne) { value += ';\n'; } cc = extractAddress(cc); system.debug('CcAddress: ' + cc); value += cc; seenOne = true; } } theEmail.CcAddress = limitLength(value,EmailMessage.CcAddress.getDescribe().getLength()); insertSObject(theEmail); } private void handleError(System.DmlException e, String message){ String baseURL = URL.getSalesforceBaseUrl().toExternalForm() + '/'; if(TRACE)system.debug(baseURL); String caseURL; String msg = message + '\n'; if(this.theCase != null) { caseURL = baseURL + theCase.Id; msg += '\n'; msg += 'Originating Case Number: ' + theCase.CaseNumber + ' '+ caseURL+'\n'; } if(this.inboundEmail != null) { msg += '\nEmail:'; msg += ' subject: ' + inboundEmail.Subject + '\n'; msg += ' from: ' + inboundEmail.FromName + '\n'; msg += ' address: ' + inboundEmail.FromAddress + '\n'; } if(e != null) { // compose the DmlException message on one line to minimize the number of untested lines. AFAIK easy to instantiate a DmlException in a unit test. msg += '\n'; msg += 'EXCEPTION:\n Error: ' + e.getMessage() + '\n Type: ' + e.getTypeName() + '\n Line Number: ' + e.getLineNumber() + '\n Trace:\n' + e.getStackTraceString() + '\n(end stack trace)\n'; } Case errCase = new Case(); errCase.OwnerId = this.defaultCaseOwnerId; errCase.Status = 'New'; errCase.Priority = 'Low'; errCase.Origin = 'Email'; errCase.Subject = 'Error processing incoming email'; errCase.Description = limitLength(msg,Case.Description.getDescribe().getLength()); insert errCase; errCase = [Select Id, CaseNumber from Case where Id = :errCase.Id limit 1]; caseURL = baseURL + errCase.Id; msg += '\n\n'; msg += 'Created new Case number ' + errCase.CaseNumber + ' for this error. See: ' + caseURL +'\n'; TriggerErrorNotification.reportError('CaseEmailInBoundUtilities', msg); } /* Given a case number such as 8144 find the exact case that use this number. Note that CaseNumber is a string field that may have any number of leading zeros. */ private Case locateByCaseNumberAsString(String caseNumberStr){ Integer target = Integer.valueOf(caseNumberStr); Case theResult = null; String caseNumber = '%' + String.valueOf(target); Case[] matchingCases = [Select Id, CaseNumber, Subject, Description from Case where CaseNumber like :caseNumber]; for(Case aCase: matchingCases) { Integer cnum = Integer.valueOf(aCase.CaseNumber); if(cnum == target) { theResult = aCase; break; } } return theResult; } /* Look for the case reference in the email subject line. First search for a case reference using the standard Salesforce method of creating that complicated and non-user-friendly reference. Do this first so it takes precedence. But, also search for the case number itself. This is user-friendly! */ private String extractRef(String emailSubject) { String itemRef = null; String target = emailSubject.toLowerCase(); String patternString; Pattern thePattern; Matcher matcher; /* Take the text between the period and ":ref" For example in the ref [ ref:00D7JFzw.5007H3Rh8:ref ] extract 5007H3Rh8 Take that text and remove the 5007. For example H3Rh8 Append H3Rh8 to https://na5.salesforce.com/5007000000 to produce https://na5.salesforce.com/5007000000H3Rh8. This is your link to get to the case. */ patternString = '.*ref:(.{8}).(.{4})(.+):ref.*'; thePattern = Pattern.compile(patternString); matcher = thePattern.matcher(emailSubject); // do not change to lower case for this test because Id's are case sensitive if (matcher.matches()) { String caseId = matcher.group(2) + '000000' + matcher.group(3); if(TRACE) system.debug(Logginglevel.ERROR,'extractRef "' + caseId + '"'); Case[] matchingCases = [Select CaseNumber from Case where Id = :caseId]; if(matchingCases.size() == 1) { Case theCase = matchingCases[0]; itemRef = theCase.CaseNumber; } } if(itemRef == null) { // extract the Case Number from the email Subject // Re: Test two numbers Case: 30088 and Case: 30089' // returns 30089, the last pattern matched patternString = '.*case[;:=]?\\s*([0-9]+).*'; thePattern = Pattern.compile(patternString); matcher = thePattern.matcher(target); if (matcher.matches()) { itemRef = matcher.group(1); if(TRACE) system.debug('Extracted case number ' + itemRef); } } return itemRef; } private String extractMainSubject(String emailSubject) { if(emailSubject == null || emailSubject.length() < 3) return emailSubject; String[] prefixes = new String[] {'fw:','re:', 'automatic reply:', 'out of office autoreply:', 'out of office'}; String target = emailSubject.toLowerCase(); for(String prefix: prefixes) { Integer index = target.indexOf(prefix); if(index == 0 ){ String mainSubject = emailSubject.substring(prefix.length(),emailSubject.length()); return mainSubject.trim(); } } return emailSubject; } private String extractAddress(String inAddress) { String address; String patternString; Pattern thePattern; Matcher matcher; patternString = '.*<(.*)>.*'; thePattern = Pattern.compile(patternString); matcher = thePattern.matcher(inAddress); if (matcher.matches()) { address = matcher.group(1); system.debug('Extracted address ' + address); } else { address = inAddress; system.debug('Did not match angle-address ' + address); } return address; } /* ***************************************************************** TEST METHODS */ static testmethod void testExtractAddress() { system.debug('testExtractAddress '); String expected = 'b.g@g.com'; String target = 'Bryan <'+expected+'>'; CaseEmailInBoundUtilities prutil = new CaseEmailInBoundUtilities(); String results = prutil.extractAddress(target); System.assertEquals(expected,results); target = 'Helen W'; expected = 'Helen.W@on.com'; results = prutil.extractAddress(target); System.assertEquals(expected,results); } static testmethod void testExtractRef() { Test.startTest(); String emailSubject = 'Subject Case: 987 asdas asdasd '; CaseEmailInBoundUtilities prutil = new CaseEmailInBoundUtilities(); String caseNumber = prutil.extractRef(emailSubject); system.assertEquals('987',caseNumber); emailSubject = 'Subject cAse=987 asdas asdasd '; caseNumber = prutil.extractRef(emailSubject); system.assertEquals('987',caseNumber); Case aCase = new Case(); insert aCase; Case bCase = [Select Id, CaseNumber from Case where Id = :aCase.Id limit 1]; String caseId = bCase.Id; String left = caseId.substring(0, 4); String right = caseId.substring(caseId.length()-8, caseId.length()); right = right.substring(0,5); system.debug(Logginglevel.ERROR,'testExtractRef case id: "' + caseId + '"'); system.debug(Logginglevel.ERROR,'testExtractRef case n: "' + bCase.CaseNumber + '"'); emailSubject = 'For example in the ref [ ref:00D7JFzw.5007H3Rh8:ref ] '; emailSubject = 'For example in the ref [ ref:00D7JFzw.'+left+right+':ref ] '; system.debug(Logginglevel.ERROR,'testExtractRef email subject: "' + emailSubject + '"'); caseNumber = prutil.extractRef(emailSubject); system.assertEquals(bCase.CaseNumber,caseNumber); Test.stopTest(); //TRIM(" [ ref:" + LEFT( $Organization.Id, 4) + RIGHT($Organization.Id, 4) +"."+ LEFT( Id, 4) // + SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(Id, RIGHT( Id, 4), ""), LEFT( Id, 4), ""), "0", "") // + RIGHT( Id, 4) + ":ref ] ") // LEFT( Id, 4) //SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(Id, R4, ""), L4, ""), "0", "") + R4 } static testmethod void testExtractMain() { CaseEmailInBoundUtilities prutil = new CaseEmailInBoundUtilities(); String emailSubject = 'Some Subject Case'; String fwSubject = 'FW: ' + emailSubject; String mainSubject; mainSubject = prutil.extractMainSubject(fwSubject); system.debug(Logginglevel.ERROR,'testExtractMain "' + mainSubject + '"'); system.assertEquals(emailSubject,mainSubject); String reSubject = 'Re: ' + emailSubject; mainSubject = prutil.extractMainSubject(reSubject); system.debug(Logginglevel.ERROR,'testExtractMain "' + mainSubject + '"'); system.assertEquals(emailSubject,mainSubject); } static testMethod void testHandleError() { CaseEmailInBoundUtilities emailProcess = new CaseEmailInBoundUtilities(); // create a new email Messaging.InboundEmail email; email = new Messaging.InboundEmail() ; email.subject = 'Test subject'; email.plainTextBody = 'Test email'; email.fromname = 'FirstName LastName'; email.toAddresses = new String[] {'someaddress@email.com'}; emailProcess.inboundEmail = email; emailProcess.handleError(null,'unit testing handle error'); } static testMethod void testProcessInboundEmailGoodWithAttachment() { Test.startTest(); // create a new email Messaging.InboundEmail email = new Messaging.InboundEmail() ; email.subject = 'Subject asdas asdasd '; email.plainTextBody = 'Test email'; email.fromname = 'FirstName LastName'; email.toAddresses = new String[] {'someaddress@email.com'}; email.ccAddresses = new String[] {'one@email.com','two@email.com'}; String csv = 'this is just a test'; // add an attachment Messaging.InboundEmail.BinaryAttachment attachment = new Messaging.InboundEmail.BinaryAttachment(); attachment.body = blob.valueOf(csv); attachment.fileName = 'data.csv'; attachment.mimeTypeSubType = 'text/plain'; email.binaryAttachments = new Messaging.inboundEmail.BinaryAttachment[] { attachment }; Messaging.InboundEmail.TextAttachment tattachment = new Messaging.InboundEmail.TextAttachment(); tattachment.body = csv; tattachment.fileName = 'data.csv'; tattachment.mimeTypeSubType = 'text/plain'; email.textAttachments = new Messaging.inboundEmail.TextAttachment[] { tattachment }; // call the email service class and test it with the data in the testMethod CaseEmailInBoundUtilities emailProcess = new CaseEmailInBoundUtilities(); Messaging.InboundEmailResult result = emailProcess.processInboundEmail(email); System.assert(result.success == true); Test.stopTest(); } static testMethod void testProcessInboundEmailWithMatchingSubject() { Test.startTest(); CaseEmailInBoundUtilities emailProcess = new CaseEmailInBoundUtilities(); Messaging.InboundEmail email; Messaging.InboundEmailResult result; Case[] matchingCases; EmailMessage[] matchingEmailMessages; Case theCase; String mainSubject = 'Subject asdas asdasd'; // Test Part 1 // create a new email email = new Messaging.InboundEmail() ; email.subject = mainSubject; email.plainTextBody = 'Test email'; email.fromname = 'FirstName LastName'; email.toAddresses = new String[] {'someaddress@email.com'}; // call the email service class and test it with the data in the testMethod result = emailProcess.processInboundEmail(email); System.assert(result.success == true); matchingCases = [Select Id, CaseNumber, Subject, Description from Case where Subject = :mainSubject]; System.assertEquals(1,matchingCases.size()); theCase = matchingCases[0]; matchingEmailMessages = [Select Id from EmailMessage where ParentId = :theCase.Id]; System.assertEquals(1,matchingEmailMessages.size()); // TEST Part 2 // create a new email email = new Messaging.InboundEmail() ; email.subject = mainSubject; email.plainTextBody = 'Test email'; email.fromname = 'FirstName LastName'; email.toAddresses = new String[] {'someaddress@email.com'}; // call the email service class and test it with the data in the testMethod result = emailProcess.processInboundEmail(email); System.assert(result.success == true); matchingCases = [Select Id, CaseNumber, Subject, Description from Case where Subject = :mainSubject]; // still only one Case because the system will append the second email to the case Case System.assertEquals(1,matchingCases.size()); System.assertEquals(theCase.Id,matchingCases[0].Id); // Should be two emails on the case matchingEmailMessages = [Select Id from EmailMessage where ParentId = :theCase.Id]; System.assertEquals(2,matchingEmailMessages.size()); // TEST Part 3 // create a new email email = new Messaging.InboundEmail() ; email.subject = 'Different subject line but include Case: ' + theCase.CaseNumber; email.plainTextBody = 'Test email'; email.fromname = 'FirstName LastName'; email.toAddresses = new String[] {'someaddress@email.com'}; // call the email service class and test it with the data in the testMethod result = emailProcess.processInboundEmail(email); System.assert(result.success == true); matchingCases = [Select Id, CaseNumber, Subject, Description from Case where Subject = :email.subject]; // Should be no cases created with that subject System.assertEquals(0,matchingCases.size()); // Should be three emails on the case matchingEmailMessages = [Select Id from EmailMessage where ParentId = :theCase.Id]; System.assertEquals(3,matchingEmailMessages.size()); Test.stopTest(); } static testMethod void testProcessInboundEmailWithContacts() { Test.startTest(); CaseEmailInBoundUtilities emailProcess = new CaseEmailInBoundUtilities(); Messaging.InboundEmail email; Messaging.InboundEmailResult result; Case[] matchingCases; EmailMessage[] matchingEmailMessages; Case theCase; Contact theContact = new Contact(); theContact.FirstName = 'FirstName'; theContact.LastName = 'LastName'; theContact.Email = 'someaddress@email.com'; String mainSubject = 'Subject asdas asdasd'; // Test Part 1 // create a new email email = new Messaging.InboundEmail() ; email.subject = mainSubject; email.plainTextBody = 'Test email'; email.fromname = 'FirstName LastName'; email.toAddresses = new String[] {theContact.Email}; // call the email service class and test it with the data in the testMethod result = emailProcess.processInboundEmail(email); System.assert(result.success == true); matchingCases = [Select Id, CaseNumber, Subject, Description from Case where Subject = :mainSubject]; System.assertEquals(1,matchingCases.size()); theCase = matchingCases[0]; matchingEmailMessages = [Select Id from EmailMessage where ParentId = :theCase.Id]; System.assertEquals(1,matchingEmailMessages.size()); } }
Nice write down.
ReplyDeleteCan you elaborate how to integrate your class into an existing Salesforce installation? For example we have EmailtoCase already enabled. How would I set it up or replace the default handler with your implementation?
Thanks,
Ulf
Thanks Ulf for the question. See the next posting and let me know if this works for you. http://sforcehacks.blogspot.ca/2012/01/email-to-case-custom-email-handler.html
ReplyDeleteHI Bryan,
ReplyDeleteAfter using the above code the cases are generated properly but still the response doesn't include the case id.
Would you please guide me what setup do I need to include case id in the response?
Regards,
James
Hi Suchismita,
ReplyDeleteIf I understand your question you just need to edit the email templates in the Salesforce user interface. Go to Setup ... Communication Templates ... Email Templates.
As you construct each template add the merge field to insert the case number. For example,
Re: {!Case.Subject} Case: {!Case.CaseNumber}
Hey Bryan ,
ReplyDeleteThanks for the workaround as it can solve case creation duplicates bug in existing functionality. But every time, I try to implement this functionality it is giving me this error.
Error: Compile Error: Variable does not exist: TriggerErrorNotification at line 207 column 2
Can you please advice what to do ?
Hi
ReplyDeleteI'm sorry I did not include the code for the method TriggerErrorNotification(). It is just a helper method that sends an email to the sysAdmin. If you can't make your own then perhaps you can just comment out this line of code?
Bryan
hi Bryan,
ReplyDeleteCould this be used to parse the email and attach it to an account and/or opportunity? Just like the email could be attached to a case?
Thanks
Diyan
Hi Diyan,
DeletePerhaps but I'm not sure. I've moved on from Salesforce development so maybe others can answer. Generally, when working with SF I preferred to has my users use Cases to manage a single Issue. If the staff were trained to make sure every Case was associated with a Contact then the emails would appear also under the Contact's Account.
As for opportunities did you see this posting: https://sforcehacks.blogspot.ca/2013/02/opportunities-are-so-hard-to-find.html I did automatically create a Case associated with the Opportunity and that allowed us to see emails related to the Case/Opportunity; all at the Case, Contact, Opportunity and Account levels.