+ Start a Discussion
Greg RohmanGreg Rohman 

Apex EmailFileAttachment 3MB limit - Any way to provide links to attachments in email?

Hello.

I have just come across the somewhat ridiculous convert-3mb-attachments-to-HTML limitation in Salesforce, which has effectively crippled a new app that I just developed.

The app is an expense app that permits users to enter expense details into a custom object, attach an image of a receipt (either via the desktop or mobile app), and then submit an expense report electronically that bundles all of the expense details into an email message with all of the receipts attached to the email. With this limitation, the attachments are all being sent as HTML files with links to the attachments, which appear to be hosted on some Salesforce instance that doesn't require a valid login to view.

I don't love the linking aspect, but I could live with it if there was a way to have the body of the email message contain links to the attachments, instead of each attachment being "attached" as an HTML file with an embedded link.

Here's a code snippet, just containing the code to add the attachments to an EmailFileAttachment. Everything above and below this code is functional and irrelevant to this issue.
        List<ContentVersion> attachmentData = new List<ContentVersion>();
        attachmentData = [SELECT ContentDocumentId,VersionData,FileExtension,FileType,Id,Title,PathOnClient FROM ContentVersion where Id IN :ContentVersionDetailsMap.keySet()];
        for (ContentVersion a :attachmentData) {
            Messaging.Emailfileattachment efa = new Messaging.Emailfileattachment();
            efa.setContentType(extContentTypeMap.get(a.FileExtension)); // Content type of attachment
            efa.setFileName(a.Title + '.' + a.FileExtension); //Title of the PDF
            efa.setFileName(a.PathOnClient); //Title of the PDF
            efa.setBody(a.VersionData); //Body of the PDF,need to do transfer into blob
            fileAttachments.add(efa);
        }
        mail.setFileAttachments(fileAttachments);

Is there some way to include a list of the attachment links in the body of the message instead of actually attaching them as HTML files?
Thanks in advance.

-Greg
 
Greg RohmanGreg Rohman
Quick update.

As a possible workaround to the attachment size issue, I considered sending each attachment in its own email message. It's not ideal, but at least it would save the recipients some clicks. I did some testing and rewrote the email sending portion of the code, and that worked... except I've hit upon another governor limit in that only 10 email invocations is permitted.

Still open to suggestions.

-Greg
Alain CabonAlain Cabon
Hi,

This kind of governor limit (10 mails max.) is for an unique transaction (synchronous) but you can use asynchronous treatments (batches and queues) to exceed easily this limit. 

1) an anonymous code for a test with 15 mails sent with a unique caller but using 15 enqueued jobs:
String recipient = 'Greg.Rohman@test.com';
Map<String,String> map1 = new Map<String,String> ();
map1.put('06858000002o4ZQAAY','DOC1');
map1.put('06858000002o4ZLAAY','DOC2');
Map<String,String> map2 = new Map<String,String> ();
map2.put('pdf','application/pdf');
map2.put('xlsx','application/excel');
for (integer i=0;i<15;i++) {
    String subject = 'Test email' + i;
    AsyncSendMails my_async = new AsyncSendMails(recipient,subject,map1,map2);
    ID jobID = System.enqueueJob(my_async);
}

The idea is to pass all the needed parameters for each job in the constructor of my_async : AsyncSendMails(recipient,subject,map1,map2);
Then you just trigger the mailings with the System.enqueueJob(my_async

There is also a governor limit for the number of jobs in a queue but this limit is sufficient and you can also use FlexQueue (100 jobs).

2) The class AsyncSendMails must implement the interface Queueable and the method execute(QueueableContext context)

https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_queueing_jobs.htm
public class AsyncSendMails implements Queueable {
    private Map<String,String> contentVersionIds;
    private Map<String,String> extContentTypeMap;
    private String toAddress;
    private String subject;
    public AsyncSendMails(String recipient,String mysubject,Map<String,String> map1,Map<String,String> map2)  {
        contentVersionIds = map1;  
        extContentTypeMap = map2;
        toAddress = recipient; 
        subject = mysubject;
    }
    public void execute(QueueableContext context) {      
        List<ContentVersion> attachmentData = new List<ContentVersion>();
        attachmentData = [SELECT ContentDocumentId,VersionData,FileExtension,FileType,Id,Title,PathOnClient
                          FROM ContentVersion where Id IN :contentVersionIds.keySet() LIMIT 10];
        List<Messaging.EmailFileAttachment> fileAttachments=new List<Messaging.EmailFileAttachment>();
        Integer total_size = 0;
        for (ContentVersion a :attachmentData) {
            Messaging.Emailfileattachment efa = new Messaging.Emailfileattachment();
            efa.setContentType(extContentTypeMap.get(a.FileExtension)); // Content type of attachment
            efa.setFileName(a.Title + '.' + a.FileExtension); //Title of the PDF
            // efa.setFileName(a.PathOnClient); //Title of the PDF
            efa.setBody(a.VersionData); //Body of the PDF,need to do transfer into blob
            integer bsize = efa.getBody().size();
            total_size += bsize;
            if (total_size < 3000000) {
                fileAttachments.add(efa);
            } else {
                send_mail(fileAttachments);
                total_size = 0;
            }   
        }
        if (total_size > 0) {
             send_mail(fileAttachments);
        }
    }
    void send_mail(List<Messaging.EmailFileAttachment> fileAttachments) {
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        mail.setFileAttachments(fileAttachments); 
        mail.setSubject(subject);
        mail.setToAddresses(new List<String> {toAddress});
        mail.setHtmlBody('<h1>TEST</h1>');
        boolean opt_allOrNone = true;
        Messaging.sendEmail(new Messaging.Email[] { mail } , opt_allOrNone);
    }
}

That is not exactly the code that you will use but that is the principle.

The only problem with the asynchronous treatments is the monitoring of the results (job well executed finally or not?).
That is also possible but that is not writen in the code above.
 
Alain CabonAlain Cabon
fileAttachments=newList<Messaging.EmailFileAttachment>(); was missing above if you exceed the 3,000,000 size limit.

But you will not use the code anyway without other changes.
 
for (ContentVersion a :attachmentData) {
            Messaging.Emailfileattachment efa = new Messaging.Emailfileattachment();
            efa.setContentType(extContentTypeMap.get(a.FileExtension)); // Content type of attachment
            efa.setFileName(a.Title + '.' + a.FileExtension); //Title of the PDF
            // efa.setFileName(a.PathOnClient); //Title of the PDF
            efa.setBody(a.VersionData); //Body of the PDF,need to do transfer into blob
            integer bsize = efa.getBody().size();
            total_size += bsize;
            if (total_size < 3000000) {
                fileAttachments.add(efa);
            } else {
                send_mail(fileAttachments);
                total_size = bsize;
                fileAttachments=newList<Messaging.EmailFileAttachment>();
                fileAttachments.add(efa);
            }   
}

 
Avishek Nanda 14Avishek Nanda 14
Hi Greg,

You can use salesforce file here. What would be the ideal solution you can twick you logic to take the attachment and save it in salesforce file. And you can create a external sharing link and include that in your HTML template. So the receipent receives the template with  the link. When he clicks on the link the file gets downloaded. Preety much work like When we put any file in Google drive and send the link to others to download. 

The attachment still resides in salesforce and allows users to download the file using an external link. I believe you wouldn't also face Email attachment governer limit. 

Let me know if this works.

Regards,
Avishek Nanda
Greg RohmanGreg Rohman
Thank you for the responses.

Alain, I like the approach in your second suggestion, where you add attachments up to the max of 3MB, then send the message and create a new message for additional attachments (at least I think that's what you are suggesting). I think the only thing missing would be a test to determine if a single attachment is over 3MB, in which case it would need to send the attachment as an HTML link. Make sense?

-Greg
Alain CabonAlain Cabon
Hi Greg,

The batches and queues are quite heavy solutions and the sending of just the links of the files (already stored in files perhaps in your org) as Avishek Nanda suggests could be a more suitable way and much more lighter but you need the code example. I will try to code an example out of curiosity or Avishek Nanda has perhaps already an example.
 
Greg RohmanGreg Rohman
Hi Alian.

I agree. But due to the temporary nature of the attachments, creating files and providing links to documents that only need to exist temporarily seems equally heavy. The password-free temporary CDN that Salesforce seems to employ when automatically converting the attachments to HTML links would be a perfect fit... and that goes back to my original question. Is there some way to obtain a list of those temporary links instead of them being attached as individual HTML files?

-Greg
Alain CabonAlain Cabon
Ok, the temporary nature of the attachments is important here indeed and you could test the sending of the emails with an optimized algorithm calculating the most of the attachments for each email up to 3 mega using queues for just the extra mail that exceeds the limit of 10 emails for one transaction. My sample code is theorical and you don't need to use a queue for each mail. That is needed for just the extra email (more than 10 mails).

You have already solved half of the problem and for the extra mails exceeding ten just try a solution with queues.

Alain