Monday, January 2, 2012

Email-To-Case custom email handler

See my previous post to explain why a custom Email-To-Case handler is required and for the complete code that provides the solution. In this posting I'll explain how it works.

First, here is the email handler class that invokes the email procesor:

global class CaseEmailInBoundHandler implements Messaging.InboundEmailHandler {

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

 CaseEmailInBoundUtilities handler = new CaseEmailInBoundUtilities();
        Messaging.InboundEmailResult result = handler.processInboundEmail(email);
        return result;        
    }
}

This class is kept very simple to put all the working code into a class that can be covered by unit tests. All Salesforce code needs to be covered by unit tests or it can not be deployed to the production server.

Unit Testing

I do not like the fact that all Salesforce Apex class are in the same namespace. This makes it impossible for developers to group classes into folders or packages. This is a common practice in all software languages and it is sorely missed in Apex development. To workaround this limitation I build larger classes and include all unit test methods in the class file; with the test methods placed at the bottom of the file.


Email Handler

The whole handler is contained in one class CaseEmailInBoundUtilities. The constructor locates the main queue where all new Cases are placed pending attention of the support staff. I store the main Case and incoming email message to access them in some error handling methods.

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

The main entry point is the public method processInboundEmail
public Messaging.InboundEmailResult processInboundEmail(Messaging.InboundEmail email)
{
// set up
 Messaging.InboundEmailResult result = new Messaging.InboundEmailresult();
 result.success = true;
 this.inboundEmail = email;
 
// locate Case Number in email subject line, if present locate the main Case
 String caseNumber = extractRef(email.subject);
 if(caseNumber != null)
 {  
  this.theCase = locateByCaseNumberAsString(caseNumber);
  if(this.theCase == null) {
   // TODO email error message to SysAdmin
  }
 } 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 
  {
  // because there no one Case can be considered the primary case.
  // do nothing which will create a new Case 
  }
 }
 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 create a new Case here to get CS to resolve this....
    theCase.Description = 'Note: there is more than one Contact with this email address. Fix this. ' + theCase.Description;
   }
  }
  insertSObject(this.theCase);
 }

 createEmailMessage(theCase,email);
 handleAttachments(theCase, email);

 return result;
}

There are a number of key helper methods in the above that I'll explain next.

extractRef

The method extractRef will look into the email subject and either find the Saleforce reference Id (This is needed to support a transition from the SFDC default handler!) or the case number.

Go read the previous post to see the code. It contains some interesting stuff that locates the cryptic reference and deciphers it into a object reference ID that can be used to locate the Case.


extractMainSubject

The method extractMainSubject looks for the "main" subject line. It strips off any leading prefix such as FW or RE. This leaves the main original subject line that can be used to locate existing cases.

locateByCaseNumberAsString

The method locateByCaseNumberAsString overcomes the problem of locating the Case by number when there might be leading zeros. For more on this see my question on stackoverflow


insertSObject

The method insertSObject simply wraps the call to insert an object into the SF system. The code mainly handles errors with the error handling methods described in an earlier post.

createEmailMessage and handleAttachments

The last two methods do the main work of creating the EmailMessage object, attaching it to the Case and doing the same for all email attachments. See the code below.

private void createEmailMessage(Case theCase, Messaging.InboundEmail email) {
 String value;
 Integer maxlength;
 EmailMessage theEmail = new EmailMessage();
 theEmail.ParentId = theCase.Id;
 theEmail.Incoming = true;
 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);
}


limitLength

The method limitLength is used to keep the value within the bounds, otherwise the insert will fail. The typical invocation uses the Salesforce schema methods to describe the system.

theEmail.TextBody = limitLength(email.plainTextBody,
  EmailMessage.TextBody.getDescribe().getLength());

12 comments:

  1. Thanks for the Article Bryan,

    Can we know from where we suppose to include this implementation? Which one to replace or which class to replace?

    ReplyDelete
    Replies
    1. If I understand your question I think you need to realize that you add two new classes: CaseEmailInBoundHandler and CaseEmailInBoundUtilities. The first is an email handler and is kept simple letting the second class do the work. All the code shown above, form the email handler section and beyond, is in this second class. Let me know if this doesn't answer your question.

      Delete
  2. Bryan, this is a very intriguing work around! We want to use Email2Case but need to strip off all attachments coming into salesforce when the case is created and save those attachments to a secure on-premise drive (Sharepoint, or other). Do you think your custom email handler could accomplish this with a bit more coding?

    We have no concerns about SFDC security it is more of an audit requirement for us. Thanks!

    ReplyDelete
  3. Hi David, Sorry for the delay in responding. Yes, I think it would be possible to save attachments with this approach somewhere else but if it was my goal I would put that code somewhere else too. Leave this email code to handle the email attachments. Then, in some other location, perform an extraction of the attachment. I won't have a chance to work out that solution for you right now. Let me know if you find something

    ReplyDelete
  4. Hi Bryan

    We're implementing a similar solution, but we're experiencing an issue with the email's status and icon, it shows sent for a new incoming email, even if we do set the email.incoming= true value, any idea what may be the cause of this?

    Appreciate the help

    ReplyDelete
    Replies
    1. Hi Jacob, Sorry but I don't know what could be the cause of your problem. I can confirm that on our system any incoming email does appear with the incoming icon and status. Double check that your email handler is actually implementing Messaging.InboundEmailHandler and that you are fully creating the EmailMessage sobject before inserting it.

      Delete
    2. Hey there Bryan

      For what its worth, found that you need to set the status on the incoming email, else it defaults to "Sent"

      Adding the line below setting the incoming status did the trick:
      .....
      theEmail.Incoming = true;
      theEmail.Status = '0'; //set to new
      .............


      Other values that can be used:

      '0' = New
      '1' = Read
      '2' = Replied
      '3' = Sent

      Hope this is of benefit to others as well.

      Delete
    3. Hi Jacob,
      Thank you for taking the time to share this information. I'm sure it will help someone in the future. In my case, for some reason, it seems my system is defaulting to 'new' rather than 'sent'. Still it is good practice to be explicit and not depend on defaults so I may add this setting into my code.
      Cheers, Bryan

      Delete
  5. Hi Brian,

    I'm having a error:

    Method does not exist or incorrect signature: extractRef(String)

    ReplyDelete
  6. Hi, The method extractRef is described in another of my posts. Here is the link http://sforcehacks.blogspot.ca/2012/01/email-to-case-custom-email-handler_23.html
    Bryan

    ReplyDelete
  7. Hi Bryan,

    I am still getting the reference number and not the case number. Please suggest what i need to do

    ReplyDelete
    Replies
    1. Hi Rohit, I'm in an area with very limited access to the Internet for a while. So I may not be able to help. But, tell me if you tried the code in the extactRef method described above.

      Bryan

      Delete