function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Stefanie BayerStefanie Bayer 

Attach email attachment to other records via trigger

Hi experts,
I have the following challenge I'm struggling with. The requested functionality is when saving an email with attachment from (e.g.) Salesforce App for Outlook to Salesforce and attaching the email to the opportunity, the attachment itself (not only the email) should additionally be attached to the opportunity.
So far, I figured out that 
  • (of course) the email is saved as a task
  • the attachment is saved in the ContentDocument table
  • the link between the email and the attachment is saved in the ContentDocumentLink table (plus the permissions that are applied)
  • additionally the link between the attachment and the opportunity's account (in the task's WhatId) is also saved as an additional entry in the ContentDocumentLink table
What I wanted to achieve with a trigger on the task object is to create a thrid entry in the ContentDocumentLink table between the opportunity itself an the email attachment. (This entry is created when I manually post the file (from Salesforce) to the opportunity feed.)

The problem is - at least that's what I'm thinking - that the entry in the ContentDocumentLink table is created in a separate transaction on the DB. The (after insert) trigger on the task cannot find any entries in the ContentDocumentLink table. If I check via workbench the entry is there.

Here is the trigger (probably not the best code since I'm not a developer - apologies for that :-)):
 
trigger addEmailAttachToWhatId on Task (after insert) {

    ContentDocumentLink doclink= new ContentDocumentLink ();   
    ContentDocumentLink doclinkold= new ContentDocumentLink ();   
    List<Id> tskL = new List<Id>();
    List<ContentDocumentLink> doclinkL = new List<ContentDocumentLink>();
    List<ContentDocumentLink> doclinks = new List<ContentDocumentLink>();

    try{
        for(Task t : Trigger.new){
            tskL.add(t.Id); 
        }    

        doclinks= [SELECT ContentDocumentId, LinkedEntityId FROM ContentDocumentLink WHERE LinkedEntityId in :tskL];
    }catch(Exception e){
    }
            
    for(Task task : Trigger.new){
        
        if(task.whatId != null){
                
            for (ContentDocumentLink dlink : doclinks){
                if (dlink.LinkedEntityId == task.Id){
                    doclink.ContentDocumentId= dlink.ContentDocumentId;
                    doclink.LinkedEntityId = task.WhatId;
                    doclink.ShareType = 'V';
                    doclink.Visibility = 'InternalUsers';
                    doclinkL.add(doclink);
                 }
            }
        }
    }       
    insert doclinkL;       
}

The message in the Debug log is as follows:
 
36.0 APEX_CODE,DEBUG;APEX_PROFILING,DEBUG;CALLOUT,INFO;DB,DEBUG;SYSTEM,DEBUG;VALIDATION,INFO;VISUALFORCE,DEBUG;WORKFLOW,DEBUG
12:26:48.0 (585253)|EXECUTION_STARTED
12:26:48.0 (615652)|CODE_UNIT_STARTED|[EXTERNAL]|01q24000000TjWc|addEmailAttachToWhatId on Task trigger event AfterInsert for [00T2400000cMmED]
12:26:48.0 (15796812)|SOQL_EXECUTE_BEGIN|[28]|Aggregations:0|SELECT ContentDocumentId, LinkedEntityId FROM ContentDocumentLink WHERE LinkedEntityId IN :tmpVar1
12:26:48.0 (43505087)|SOQL_EXECUTE_END|[28]|Rows:0
12:26:48.44 (44045805)|CUMULATIVE_LIMIT_USAGE
12:26:48.44 (44045805)|LIMIT_USAGE_FOR_NS|(default)|
  Number of SOQL queries: 1 out of 100
  Number of query rows: 0 out of 50000
  Number of SOSL queries: 0 out of 20
  Number of DML statements: 0 out of 150
  Number of DML rows: 0 out of 10000
  Maximum CPU time: 0 out of 10000
  Maximum heap size: 0 out of 6000000
  Number of callouts: 0 out of 100
  Number of Email Invocations: 0 out of 10
  Number of future calls: 0 out of 50
  Number of queueable jobs added to the queue: 0 out of 50
  Number of Mobile Apex push calls: 0 out of 10

12:26:48.44 (44045805)|CUMULATIVE_LIMIT_USAGE_END

12:26:48.0 (45844070)|CODE_UNIT_FINISHED|addEmailAttachToWhatId on Task trigger event AfterInsert for [00T2400000cMmED]
12:26:48.0 (47962053)|EXECUTION_FINISHED
12:26:48.53 (53178488)|EXECUTION_STARTED
12:26:48.53 (53192920)|CODE_UNIT_STARTED|[EXTERNAL]|Workflow:Task
12:26:48.53 (68766665)|WF_RULE_EVAL_BEGIN|Workflow
12:26:48.53 (68822118)|WF_CRITERIA_BEGIN|[Aufgabe:  00T2400000cMmED]|Email in Subject Line|01Q24000000Upow|ON_CREATE_OR_TRIGGERING_UPDATE|0
12:26:48.53 (73895645)|WF_RULE_FILTER|[Aufgabe : Thema beginnt mit Email]
12:26:48.53 (73924226)|WF_RULE_EVAL_VALUE|E-Mail:Test Email mit Attachment
12:26:48.53 (73931095)|WF_CRITERIA_END|false
12:26:48.53 (73962473)|WF_SPOOL_ACTION_BEGIN|Workflow
12:26:48.53 (73971008)|WF_ACTION| None
12:26:48.53 (73974685)|WF_RULE_EVAL_END
12:26:48.53 (74027751)|WF_ACTIONS_END| None
12:26:48.53 (74035366)|CODE_UNIT_FINISHED|Workflow:Task
12:26:48.53 (75452920)|EXECUTION_FINISHED


 
Pankaj_GanwaniPankaj_Ganwani
Hi,

It seems it takes some time for creation of the ContentDocumentLink records. You will have to use future method to achieve this. Just create a class and place future method in it. Call this future method from the trigger. I have shared the modified code below:
 
trigger addEmailAttachToWhatId on Task (after insert) {
	CreateContentDocomentLink.createContetnDocumentLink(Trigger.newMap.keyset());
}


Apex Class
public class CreateContentDocomentLink
{
	@future
	public static void createContetnDocumentLink(Set<Id> setTaskIds)
	{
	    List<ContentDocumentLink> doclinkL = new List<ContentDocumentLink>();
        List<ContentDocumentLink> doclinks = new List<ContentDocumentLink>();
		doclinks= [SELECT ContentDocumentId, LinkedEntityId FROM ContentDocumentLink WHERE LinkedEntityId in :setTaskIds];
	
		for(Task task : [select Id, WhatId from Task where Id IN : setTaskIds]){
			
			if(task.whatId != null){					
				for (ContentDocumentLink dlink : doclinks){
					if (dlink.LinkedEntityId == task.Id){
						doclink.ContentDocumentId= dlink.ContentDocumentId;
						doclink.LinkedEntityId = task.WhatId;
						doclink.ShareType = 'V';
						doclink.Visibility = 'InternalUsers';
						doclinkL.add(doclink);
					 }
				}
			}
		}  
		insert doclinkL; 	
	}          
}

 
Stefanie BayerStefanie Bayer
Thank you very much - exactly what I needed! - I just had to add the definition of doclink to your code:
 
public class CreateContentDocomentLink
{
    @future
    public static void createContetnDocumentLink(Set<Id> setTaskIds)
    {
        List<ContentDocumentLink> doclinkL = new List<ContentDocumentLink>();
        List<ContentDocumentLink> doclinks = new List<ContentDocumentLink>();
        doclinks= [SELECT ContentDocumentId, LinkedEntityId FROM ContentDocumentLink WHERE LinkedEntityId in :setTaskIds];
        ContentDocumentLink doclink = new ContentDocumentLink();
    
        for(Task task : [select Id, WhatId from Task where Id IN : setTaskIds]){
            
            if(task.whatId != null){                    
                for (ContentDocumentLink dlink : doclinks){
                    if (dlink.LinkedEntityId == task.Id){
                        doclink.ContentDocumentId= dlink.ContentDocumentId;
                        doclink.LinkedEntityId = task.WhatId;
                        doclink.ShareType = 'V';
                        doclink.Visibility = 'InternalUsers';
                        doclinkL.add(doclink);
                     }
                }
            }
        }  
        insert doclinkL;    
    }          
}

 
Pankaj_GanwaniPankaj_Ganwani
Just a suggestion, you can add element in one statement in the list. Like:

doclinkL.add(new ContentDocumentLink(ContentDocumentId = dlink.ContentDocumentId, LinkedEntityId = task.WhatId, ShareType = 'V', Visibility = 'InternalUsers'));
Stefanie BayerStefanie Bayer
Ok, great. On additional problem - sometime the future handler finds the record, sometimes it doesn't... - is there a way to delay the handler further? (sorry about the stupid question - I've never used future handlers before...)

Working Log:
36.0 APEX_CODE,DEBUG;APEX_PROFILING,DEBUG;CALLOUT,INFO;DB,DEBUG;SYSTEM,DEBUG;VALIDATION,INFO;VISUALFORCE,DEBUG;WORKFLOW,DEBUG
14:27:36.1 (1776641)|EXECUTION_STARTED
14:27:36.1 (1812444)|CODE_UNIT_STARTED|[EXTERNAL]|FutureHandler - state load
14:27:36.1 (6864321)|CODE_UNIT_FINISHED|FutureHandler - state load
14:27:36.1 (8653339)|EXECUTION_FINISHED
14:27:36.32 (32224039)|EXECUTION_STARTED
14:27:36.32 (32236478)|CODE_UNIT_STARTED|[EXTERNAL]|01p2400000DHGOT|CreateContentDocomentLink.createContetnDocumentLink
14:27:36.32 (54084215)|SOQL_EXECUTE_BEGIN|[8]|Aggregations:0|SELECT ContentDocumentId, LinkedEntityId FROM ContentDocumentLink WHERE LinkedEntityId = :tmpVar1
14:27:36.32 (102974061)|SOQL_EXECUTE_END|[8]|Rows:1
14:27:36.32 (105864160)|SOQL_EXECUTE_BEGIN|[11]|Aggregations:0|SELECT Id, WhatId FROM Task 
14:27:36.32 (129594162)|SOQL_EXECUTE_END|[11]|Rows:1
14:27:36.32 (130605066)|DML_BEGIN|[25]|Op:Insert|Type:ContentDocumentLink|Rows:1
14:27:36.32 (309898226)|DML_END|[25]
14:27:36.310 (310052027)|CUMULATIVE_LIMIT_USAGE
14:27:36.310 (310052027)|LIMIT_USAGE_FOR_NS|(default)|
  Number of SOQL queries: 2 out of 200
  Number of query rows: 2 out of 50000
  Number of SOSL queries: 0 out of 20
  Number of DML statements: 1 out of 150
  Number of DML rows: 1 out of 10000
  Maximum CPU time: 0 out of 60000
  Maximum heap size: 0 out of 12000000
  Number of callouts: 0 out of 100
  Number of Email Invocations: 0 out of 10
  Number of future calls: 0 out of 50
  Number of queueable jobs added to the queue: 0 out of 1
  Number of Mobile Apex push calls: 0 out of 10

14:27:36.310 (310052027)|CUMULATIVE_LIMIT_USAGE_END

14:27:36.32 (310113437)|CODE_UNIT_FINISHED|CreateContentDocomentLink.createContetnDocumentLink
14:27:36.32 (311663942)|EXECUTION_FINISHED


Not working log:
 
36.0 APEX_CODE,DEBUG;APEX_PROFILING,DEBUG;CALLOUT,INFO;DB,DEBUG;SYSTEM,DEBUG;VALIDATION,INFO;VISUALFORCE,DEBUG;WORKFLOW,DEBUG
14:54:30.0 (118624)|EXECUTION_STARTED
14:54:30.0 (168800)|CODE_UNIT_STARTED|[EXTERNAL]|FutureHandler - state load
14:54:30.0 (3965008)|CODE_UNIT_FINISHED|FutureHandler - state load
14:54:30.0 (5563708)|EXECUTION_FINISHED
14:54:30.19 (19007354)|EXECUTION_STARTED
14:54:30.19 (19022470)|CODE_UNIT_STARTED|[EXTERNAL]|01p2400000DHGOT|CreateContentDocomentLink.createContetnDocumentLink
14:54:30.19 (34036328)|SOQL_EXECUTE_BEGIN|[8]|Aggregations:0|SELECT ContentDocumentId, LinkedEntityId FROM ContentDocumentLink WHERE LinkedEntityId = :tmpVar1
14:54:30.19 (43408470)|SOQL_EXECUTE_END|[8]|Rows:0
14:54:30.19 (44523829)|SOQL_EXECUTE_BEGIN|[11]|Aggregations:0|SELECT Id, WhatId FROM Task 
14:54:30.19 (49484898)|SOQL_EXECUTE_END|[11]|Rows:1
14:54:30.50 (50470541)|CUMULATIVE_LIMIT_USAGE
14:54:30.50 (50470541)|LIMIT_USAGE_FOR_NS|(default)|
  Number of SOQL queries: 2 out of 200
  Number of query rows: 1 out of 50000
  Number of SOSL queries: 0 out of 20
  Number of DML statements: 0 out of 150
  Number of DML rows: 0 out of 10000
  Maximum CPU time: 0 out of 60000
  Maximum heap size: 0 out of 12000000
  Number of callouts: 0 out of 100
  Number of Email Invocations: 0 out of 10
  Number of future calls: 0 out of 50
  Number of queueable jobs added to the queue: 0 out of 1
  Number of Mobile Apex push calls: 0 out of 10

14:54:30.50 (50470541)|CUMULATIVE_LIMIT_USAGE_END

14:54:30.19 (50531426)|CODE_UNIT_FINISHED|CreateContentDocomentLink.createContetnDocumentLink
14:54:30.19 (52384109)|EXECUTION_FINISHED


Any ideas?
Stefanie BayerStefanie Bayer
Checking if the records have been created and if not, calling the method again didn't help either "Future method cannot be called from a future or batch method"... Do you have any ideas left? 
Pankaj_GanwaniPankaj_Ganwani
Hi,

You cannot call Future method from inside another future method or batch process. We will have to go with some different approach. Will explain you in some time.

Thanks,
Pankaj
Pankaj_GanwaniPankaj_Ganwani
Hi Stefanie,

The only solution which I can think of to go with the batch approach which will run nightly on daily basis. Just create a custom Boolean field on Activity(Task) named as isProcessed. The Batch will fetch all the records where isProcessed = false and process them i.e. ContentDocumentLink records will be created in the same manner as you are doing in the trigger.

Please let me know if this makes sense to you. I will share the Batch code once I get consent from your end.

Thanks,
Pankaj
Stefanie BayerStefanie Bayer
Hi Pankaj, well - I think we could live with a couple of seconds delay, but an overnight batch approach is not the right solution for the users I'm afraid. But if we cannot figure out a solution, we just have to live with the status quo. 
Thanks,
Stefanie
Randall ToepferRandall Toepfer
Hi Stefanie,

One of my clients uses Outlook to attach emails to a custom object.  We want the attachment to relate to the custom object as well (a very similiar use case to your problem).

Here's the code to do just that.

http://zydecodigital.com/wordpress/2019/05/24/salesforce-auto-relate-email-message-attachment/ (http://zydecodigital.com/wordpress/2019/05/24/salesforce-auto-relate-email-message-attachment)

You were on the right direction but had the trigger on the task instead of the ContentDocumentLink.

Randall