Automating Emails to Opportunity Contact Roles

The Opportunity Contact Role is an odd beast, perhaps because it exists largely for information purposes in the B2B sales world for which Salesforce is built, but takes on weightier roles in nonprofit usage. In the nonprofit world, opportunities represent donations, grants applied for and won, and sometimes memberships and event registrations. From there arise a lot of solid use cases for sending automated emails to Contacts linked by Opportunity Contact Roles, but Email Alerts don’t allow you to access these contacts for automated mailing.

Facing this constraint, I built this Apex class that allows you to send an Opportunity-based email to contact roles using Process Builder to Call Apex and to define the template, roles of the desired recipients, and an attachment as needed. The class and related test class are below, or you can download them in a single text file; the image shows how to configure a Process Builder Action to take the place of an email alert to send your email based on the criteria you defined elsewhere in the flow.

Configuration

There are three required fields when you select this class: OpportunityID, which should be set as a field reference to the related opportunity; TemplateID, the id of the email template you’d like to use (be it html, Visualforce, or plain text), and one or more toRoleNames.

Like ccRoleNames and bccRoleNames, toRoleNames takes a comma-separated list of role names. If you want to send to the primary opportunity contact, use the name Primary, which shouldn’t also be the name of a Contact Role. To function correctly the role names need to be in proper case, and there should be no extra spaces before or after the role names. If multiple “to” contacts are specified, you have no control over which of them will end up being the “to” of record, for mail merge and links to the generated activity. If your list of roles doesn’t yield a “to” contact for a given opportunity, the primary contact will be used instead.

You can optionally define the displayName and replyTo for the email, list a single ID for a resource to attach, and set the boolean field saveAsActivity. By default, the class *will* save the email as an activity. If you prefer that it not do so, you must define this parameter as False.

A couple of potential improvements for you intrepid developers out there: the addition of cc and bcc addresses added as strings, typically for including a staff member on a given email; adding trim() and toLowerCase() so that spacing and capitalization don’t cause issues; the ability to send separate emails to all of the “to” addresses; and the ability to add multiple attachments. Please share the code back here if you make those improvements.

Process Builder screenshot to show configuration

Main Emailer Class

global class EmailOpportunityContactRole {

    @InvocableMethod(label='Send Email to Opportunity Contact Roles' description='Sends email related to an opportunity,  based on opportunity contact roles')

    global static void SendToOppConRoles(list<OCREmailParameters> EParam){    
       
        for(OCREmailParameters EP: Eparam){

            Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage(); 

            string emailsubject = '';

            string bodytext = ''; 

         

            String[] toRoles = ep.toRoleNames.split(',');

            list<string> bccRoles = new list <string>();

            if(string.isNotBlank(ep.bccRoleNames) ){   ep.bccRoleNames.split(',');}

            

            list<string> ccRoles = new list <string>();

            if(string.isNotBlank(ep.ccRoleNames) ){ ccRoles = ep.ccRoleNames.split(',');}

            

            system.debug(ccroles);

            

            list<string> toEmails = new list<string>();

            list<string> ccEmails = new list<string>();

            list<string> bccEmails = new list<string>();

            id toContact = null;

            id primaryContact = null;

            

            list<opportunitycontactrole> OCRs = [select contactid, isprimary, contact.firstname, contact.email, role from opportunitycontactrole
                                                 where isdeleted = false and opportunityid = :ep.OpportunityId and contact.email != null and
                                                 (role in :toroles or role in :bccroles or role in :ccroles or isprimary = true)];

            

            system.debug(OCRs);

            

            for(opportunitycontactrole OCR: OCRs){

                if(toRoles.contains(OCR.role) || (toRoles.contains('Primary')&&OCR.IsPrimary)){

                    if(string.isBlank(toContact)){

                        toContact = OCR.ContactId;

                    }

                    toEmails.add(OCR.Contact.Email);

                } else if (ccRoles.contains(OCR.role)|| (ccRoles.contains('Primary')&&OCR.IsPrimary)){

                    ccEmails.add(OCR.Contact.Email);

                } else if (bccRoles.contains(OCR.role)|| (bccRoles.contains('Primary')&&OCR.IsPrimary)){

                    bccEmails.add(OCR.Contact.Email);

                }

                

                if(OCR.IsPrimary){

                    primaryContact = OCR.ContactId;

                }

            }

            

            //work on contingencies if there is no "to" contact

            if(string.isblank(toContact)){

                if(!string.isblank(primaryContact)){

                    toContact = primaryContact;

                } else if (!OCRs.isempty()) {

                    toContact = OCRs[0].ContactID;

                    toEmails.add(OCRs[0].Contact.email);

                } else {

                    // if no recipients found, stop

                    system.debug('No recipients found');

                    return;

                }

            }

            

            email.setToAddresses( toEmails );

            if(!ccEmails.isempty()){

              email.setCCAddresses( ccEmails);

            }

            if(!bccEmails.isempty()){

              email.setBCCAddresses( bccEmails);

            }

            if(EP.saveAsActivity == null || EP.saveAsActivity){

              email.setSaveAsActivity(true);

            }

            if(EP.replyto != null){

              email.setReplyTo(EP.replyto);

            }

            if(EP.displayname != null){

              email.setSenderDisplayName(EP.displayName);

            }

            

            

            //template related settings

            email.setWhatId(ep.opportunityid);

            email.settargetObjectId(toContact);

            email.setTemplateId(ep.TemplateId);

            //email.setPlainTextBody( bodytext );

            

            system.debug(email);

            

            if(ep.ResourceToAttachId!=null){

                //Attachments

                List<StaticResource> objPDF = [Select body, name from StaticResource where id = :ep.ResourceToAttachId];

                if(!objPDF.isempty()){

                    Messaging.EmailFileAttachment[] objEmailAttachments = new Messaging.EmailFileAttachment[1];

                    Messaging.EmailFileAttachment objPDFAttachment = new Messaging.EmailFileAttachment();

                    objPDFAttachment.setBody(objPDF[0].Body);

                    objPDFAttachment.setFileName(objPDF[0].name + '.pdf');

                    objEmailAttachments[0] = objPDFAttachment;

                    email.setFileAttachments(objEmailAttachments);

                }

            }

            

            // Sends the email

            Messaging.SendEmailResult [] r = 

                Messaging.sendEmail(new Messaging.SingleEmailMessage[] {email});   

            

        }

       

        

    }

    global class OCREmailParameters {




      @InvocableVariable(required=true)

    global ID OpportunityId;

        

        @InvocableVariable(required=true)

    global ID TemplateId;

        

        @InvocableVariable(required=true)

    global string toRoleNames;

        

        @InvocableVariable

    global string ccRoleNames;

        

        @InvocableVariable

    global string bccRoleNames;

        

        @InvocableVariable

    global string displayName;

        

        @InvocableVariable

    global string replyTo;

        

        @InvocableVariable

    global boolean saveAsActivity;    

        

        @InvocableVariable

    global id ResourceToAttachId;     




    }

    

}

Test Class

For the test to perform optimally, you should have at least three potential contact roles in the contact role pick list, and at least one pdf somewhere in your static resources.

@isTest  (SeeAllData=true)

private class test_EmailOpportunityCR  {

    static testMethod void Test_ECR(){

        // get opportunity contact role values

        List<String> CRList= new List<String>();

        Schema.DescribeFieldResult fieldResult = OpportunityContactRole.Role.getDescribe();

        List<Schema.PicklistEntry> ple = fieldResult.getPicklistValues();

        for( Schema.PicklistEntry pickListVal : ple){

            CRList.add(pickListVal.getLabel());

        } 

    integer CRCount = CRList.size()>3?3:CRList.size();

        

        system.debug(crlist);

        

        //create account

        account a = new account(name='Test',BillingCountry='USA');

        insert a;

        

        //create contacts

        list<contact> cons = new list<contact>();

        for (Integer i = 0; i < CRCount; i++){

            contact c = new contact(firstname='Test', lastname='Test'+i,email='test'+i+'@test.com', accountid=a.id);

            cons.add(c);

        }

        insert cons;

        

        //opportunity

        opportunity o = new opportunity(name='test',accountid=a.id,closedate=system.today(),stagename='Payment Requested');

        insert o;

        

        //opportunity contact roles

        list<opportunitycontactrole> ocrs = new list<opportunitycontactrole>();

        for (Integer i = 0; i < CRCount; i++){

            opportunitycontactrole ocr = new opportunitycontactrole(contactid=cons[i].id,role=CRList[i],opportunityid=o.id);

          ocrs.add(ocr);

        }

        ocrs[0].isprimary = true;

        insert ocrs;

        

        list<EmailOpportunityContactRole.OCREmailParameters> OCREPs= new list<EmailOpportunityContactRole.OCREmailParameters>();

    EmailOpportunityContactRole.OCREmailParameters OCREP = new EmailOpportunityContactRole.OCREmailParameters();

      OCREP.OpportunityId = o.id;

        OCREP.TemplateId = [select id from emailtemplate where isActive=true limit 1][0].id;

        OCREP.toRoleNames = CRList[0];

        if(CRCount > 1){

            OCREP.ccRoleNames = CRList[1];

        } else {

            OCREP.ccRoleNames = 'Primary';

        }

        if(CRCount > 2){

            OCREP.bccRoleNames= CRList[2];

        }

        OCREP.displayName ='Hank';

        OCREP.replyTo = 'test@test.com';

        list<staticresource> SRList = [Select id from StaticResource limit 1];

        if(!SRList.isEmpty()){

          OCREP.ResourceToAttachId = SRList[0].id;  

        } 

        OCREPS.add(OCREP);

        

    EmailOpportunityContactRole.SendToOppConRoles(OCREPS);      

    }

}
Posted in Uncategorized.

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *