Associate Email to Salesforce Task to Opportunity
I use the Email to Salesforce functionality every single day. This feature allows you to get a random email address similar to emailtosalesforce@0235ffdsdfsad98dvfj4i549540njh3.in.salesforce.com (this is just a sample) that you bcc on emails and Tasks get created in your system that are auto-associated to your Leads/Contacts.
There is an option to associate the Task to Opportunities, but instead of creating a Task and associating it to the Lead/Contact AND the Opportunity, 2 tasks are created: one against the Lead/Contact and another against the Opportunity. I have no idea why it was designed this way, but it was. Given that, I don’t use the option to associate the Task to Opportunities.
After months of manually assigning to Opportunities after I send the email, I got fed up and wrote a trigger that senses an email to salesforce record and auto associates it to the nearest Open Opportunity. I thought I’d share it with y’all.
This code assumes the following:
- You are using Email to Salesforce
- You have the Email to Salesforce option for Leads & Contacts enabled and the one for Opportunities disabled
- You associate Contacts to your Opportunities via the OpportunityContactRole object
Trigger
trigger Tasks on Task (before insert) {
// BEFORE INSERT
if(Trigger.isBefore && Trigger.isInsert){
Tasks t = new Tasks();
t.AssociateOpportunity(Trigger.new);
}
}
Class
public class Tasks {
// Default Constructor
public Tasks()
{
}
// Associates a new Task generated by Email to Salesforce to an open opportunity, if one exists for the Account
public void AssociateOpportunity(Task[] tasks)
{
/***************
* Variables
***************/
list<Task> l_Tasks = new list<Task>(); // Tasks we'll be updating
set<ID> s_ContactIDs = new set<ID>(); // Set of Contact IDs
/***************
* Initial Loop
***************/
for(Task t:tasks) {
// Add Task to working list and collect the Contact ID
if (t.WhatId == null && t.Subject.startsWith('Email:') && t.WhoId != null) {
// only for Contacts
if (String.valueOf(t.WhoId).startsWith('003')){
l_Tasks.add(t);
s_ContactIDs.add(t.WhoId);
}
}
}
/***************
* Create Maps
***************/
// Maps Contact ID to an Opportunity ID
map<ID, ID> map_cID_to_oID = new map<ID, ID>();
// Query for the Contact's Open Opportunities. Sort by CloseDate DESC so the Task gets assigned to the earliest Opportunity as it loops
for (OpportunityContactRole ocr:[select Id, OpportunityId, ContactId
from OpportunityContactRole
where ContactId in :s_ContactIDs
AND Opportunity.IsClosed = false
order by Opportunity.CloseDate DESC
]) {
map_cID_to_oID.put(ocr.ContactId, ocr.OpportunityId);
}
/***************
* Process Records
***************/
for (Task t:l_Tasks) {
// If the Contact has an Opportunity mapped to it, update the Task with that Opportunity
if (map_cID_to_oID.get(t.WhoId) != null) {
t.WhatId = map_cID_to_oID.get(t.WhoId);
}
}
}
}
Test Class
@isTest
private class Tasks_Test {
static testMethod void AssociateOpportunity_Test() {
// Create a Lead
Lead l = new Lead();
l.FirstName = 'Test';
l.LastName = 'Lead';
l.Company = 'Test Company';
l.Email = 'leademail@example.com';
insert l;
// Create an Account
Account a = new Account();
a.Name = 'Test Account';
insert a;
// Create a Contact
Contact c = new Contact();
c.FirstName = 'Test';
c.LastName = 'Contact';
c.AccountId = a.Id;
c.Email = 'contactemail@example.com';
insert c;
// Create Opportunities
list<Opportunity> l_Opps = new list<Opportunity>();
Opportunity o = new Opportunity();
o.AccountId = a.id;
o.Name = 'Test Opportunity';
o.CloseDate = date.today();
o.StageName = 'Qualified';
o.Description = 'Test Opportunity Description';
l_Opps.add(o);
Opportunity o2 = new Opportunity();
o2.AccountId = a.id;
o2.Name = 'Test Opportunity';
o2.CloseDate = date.today().addDays(30);
o2.StageName = 'Qualified';
o2.Description = 'Test Opportunity Description';
l_Opps.add(o2);
Opportunity o3 = new Opportunity();
o3.AccountId = a.id;
o3.Name = 'Test Opportunity';
o3.CloseDate = date.today().addDays(60);
o3.StageName = 'Closed Won';
o3.Description = 'Test Opportunity Description';
l_Opps.add(o3);
insert l_Opps;
// Create Opportunity Contact Roles
list<OpportunityContactRole> l_Ocr = new list<OpportunityContactRole>();
OpportunityContactRole ocr1 = new OpportunityContactRole();
ocr1.ContactId = c.id;
ocr1.OpportunityId = o.id;
ocr1.IsPrimary = true;
ocr1.Role = 'Decision Maker';
l_Ocr.add(ocr1);
OpportunityContactRole ocr2 = new OpportunityContactRole();
ocr2.ContactId = c.id;
ocr2.OpportunityId = o2.id;
ocr2.IsPrimary = true;
ocr2.Role = 'Decision Maker';
l_Ocr.add(ocr2);
insert l_Ocr;
/* Create Tasks for Test Cases */
list<Task> l_Tasks = new list<Task>();
// Task associated to Lead, not Contact
Task t1 = new Task();
t1.Subject = 'Email: something';
t1.Status = 'Completed';
t1.WhoId = l.id;
t1.ActivityDate = Date.today();
l_Tasks.add(t1);
// Task with wrong subject
Task t2 = new Task();
t2.Subject = 'something';
t2.Status = 'Completed';
t2.WhoId = c.id;
t2.ActivityDate = Date.today();
l_Tasks.add(t2);
// Task with no WhoId
Task t3 = new Task();
t3.Subject = 'something';
t3.Status = 'Completed';
t3.ActivityDate = Date.today();
l_Tasks.add(t3);
// Task with a What ID already
Task t4 = new Task();
t4.Subject = 'something';
t4.Status = 'Completed';
t4.WhoId = c.id;
t4.WhatId = o2.id;
t4.ActivityDate = Date.today();
l_Tasks.add(t4);
// Task that should get triggered fully
Task t5 = new Task();
t5.Subject = 'Email: something';
t5.Status = 'Completed';
t5.WhoId = c.id;
t5.ActivityDate = Date.today();
l_Tasks.add(t5);
insert l_Tasks;
/* Asserts */
// Task 1 should not have a What ID populated
Task t = [select Id, WhoId, WhatId from Task where Id = :t1.id limit 1];
system.assertEquals(t.WhatId, null);
// Task 2 should not have a What ID populated
t = [select Id, WhoId, WhatId from Task where Id = :t2.id limit 1];
system.assertEquals(t.WhatId, null);
// Task 3 should not have a What ID populated
t = [select Id, WhoId, WhatId from Task where Id = :t3.id limit 1];
system.assertEquals(t.WhatId, null);
// Task 4 should have the same What ID it had originally populated
t = [select Id, WhoId, WhatId from Task where Id = :t4.id limit 1];
system.assertEquals(t.WhatId, o2.id);
// Task 5 is the one that should've had the Opportunity ID auto populated
t = [select Id, WhoId, WhatId from Task where Id = :t5.id limit 1];
system.assertEquals(t.WhatId, o.id);
}
}
Let me know if you have any suggestions.
Chris Said,
November 13, 2009 @ 8:47 pm
This looks like an awesome solution to this problem – but I have zero idea how to implement this.
Have you considered putting it in the appexchange? (i have no idea if this is a difficult request or not).
I suspect it would be popular. http://sites.force.com/appexchange/browse?type=Apps
Chris
Rob Crawley Said,
December 9, 2009 @ 9:57 pm
Scott – If I have lead data coming to me in emails on a daily basis from a Lead gen service is there a way to use this Email to Salesforce to have the data populated into a lead record like email to case? Pardon me if I’m having a senior moment, but I don’t think I can?
I know of a utlity called Copy2Contact (formely Anagram) that could be the solution if we were a PC shop.
Would welcome your thoughts and thanks for this posting.
Rob
Scott Hemmeter Said,
December 10, 2009 @ 3:04 pm
@Rob Crawley, yes, this could be done. The key is building an email service inside of Salesforce that is expecting to receive emails in a certain way. The Apex Code would be written to parse those lead gen emails you get. Trick #2 is being able to setup a forwarding rule that routes those emails you get to the assigned Salesforce email service. This is a simple task in Google Mail (gmail or apps) and I assume its simple in other email systems.
Scott Fletcher Said,
January 31, 2014 @ 6:44 am
This is a wonderful code sample and I was able to adapt it to relate emails-to-Salesforce to active campaigns of which the contact is a member. The code works in orgs where ‘shared activities’ have not been enabled, but not in orgs where that feature has been enabled. The difference seems to relate to the TaskRelation object – it appears that it may be necessary to insert a TaskRelation object for each task to be related to a campaign, which would make the querying and updating slightly more complex.