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 (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 Tasks on Task (before insert) {

	if(Trigger.isBefore && Trigger.isInsert){
		Tasks t = new Tasks();


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')){
        * 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

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 = ''; 
        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 = ''; 
        insert c;
        // Create Opportunities
        list<Opportunity> l_Opps = new list<Opportunity>();
        Opportunity o = new Opportunity();
        	o.AccountId =;
        	o.Name = 'Test Opportunity';
        	o.CloseDate =;
        	o.StageName = 'Qualified';
        	o.Description = 'Test Opportunity Description';
        Opportunity o2 = new Opportunity();
        	o2.AccountId =;
        	o2.Name = 'Test Opportunity';
        	o2.CloseDate =;
        	o2.StageName = 'Qualified';
        	o2.Description = 'Test Opportunity Description';
        Opportunity o3 = new Opportunity();
        	o3.AccountId =;
        	o3.Name = 'Test Opportunity';
        	o3.CloseDate =;
        	o3.StageName = 'Closed Won';
        	o3.Description = 'Test Opportunity Description';
        insert l_Opps;
        // Create Opportunity Contact Roles
        list<OpportunityContactRole> l_Ocr = new list<OpportunityContactRole>();
    	OpportunityContactRole ocr1 = new OpportunityContactRole();
    		ocr1.ContactId =;
    		ocr1.OpportunityId =;
			ocr1.IsPrimary = true;
			ocr1.Role = 'Decision Maker';
		OpportunityContactRole ocr2 = new OpportunityContactRole();
    		ocr2.ContactId =;
    		ocr2.OpportunityId =;
			ocr2.IsPrimary = true;
			ocr2.Role = 'Decision Maker';
		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 =;
        	t1.ActivityDate =;
    	// Task with wrong subject
    	Task t2 = new Task();
        	t2.Subject = 'something';
        	t2.Status = 'Completed';
        	t2.WhoId =;
        	t2.ActivityDate =;
    	// Task with no WhoId
    	Task t3 = new Task();
        	t3.Subject = 'something';
        	t3.Status = 'Completed';
        	t3.ActivityDate =;
    	// Task with a What ID already
    	Task t4 = new Task();
        	t4.Subject = 'something';
        	t4.Status = 'Completed';
        	t4.WhoId =;
        	t4.WhatId =;
        	t4.ActivityDate =;
    	// Task that should get triggered fully
    	Task t5 = new Task();
        	t5.Subject = 'Email: something';
        	t5.Status = 'Completed';
        	t5.WhoId =;
        	t5.ActivityDate =;
    	insert l_Tasks;
 		/* Asserts */
 		// Task 1 should not have a What ID populated
 		Task t = [select Id, WhoId, WhatId from Task where 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 = 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 = 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 = limit 1];
 		// Task 5 is the one that should've had the Opportunity ID auto populated
 		t = [select Id, WhoId, WhatId from Task where Id = limit 1];

Let me know if you have any suggestions.


  1. 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.


  2. 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.


  3. 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.

  4. 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.

RSS feed for comments on this post