In this article, I will show you the process of creating an Apex class to send an email with attachments callable in a Salesforce flow. Here I will create an Apex class to send a single email. Sending email in flow exists as a standard feature, but it is not possible to send an email with attachments.
This Apex class, for example, invoke a flow by clicking a button on the Opportunity record page, creating an invoice (call OFC), and sending an email with an invoice (call Apex). Please refer to the following article for sending invoice attachment emails with Flow + OFC + Apex.
Send an email with an invoice in Salesforce Flow – Office File Creator Advanced –
Key Points of Apex Class
- Methods that can be called in a flow are given the InvocableMethod annotation.
- Variables used for input and output in the flow are given the InvocableVariable annotation.
- The Email class and the extension class SingleEmailMessage class are used for sending emails.
When using InvocableMethod, the following points should be noted.
- The invocable method must be static and public or global, and its class must be an outer class.
- Only one method in a class can have the InvocableMethod annotation.
- Other annotations can’t be used with the InvocableMethod annotation.
For more information, please refer to the Apex Developer's Guide below.
When sending an email, some methods to set some parameters are described here as a sample. Please refer to the Apex Developer's Guide for all methods.
Email Class (Base Email Methods)
Sample of Sending Email Apex Class
SendEmailWithAttachment.cls
/**
* Send Email with attachments
*/
public class SendEmailWithAttachment{
/**
* Request
*/
public class Request {
@InvocableVariable(label='To Addresses')
public String[] toAddresses;
@InvocableVariable(label='Cc Addresses')
public String[] ccAddresses;
@InvocableVariable(label='Bcc Addresses')
public String[] bccAddresses;
@InvocableVariable(label='Set email sender to Bcc *Default is false')
public Boolean bccSender;
@InvocableVariable(label='Organization-Wide Email Address Id')
public String orgWideEmailAddressId;
@InvocableVariable(label='Sender Name *Cannot be set if the orgWideEmailAddressId is set')
public String senderDisplayName;
@InvocableVariable(label='Template Id')
public String templateId;
@InvocableVariable(label='Subject')
public String subject;
@InvocableVariable(label='Text Body')
public String plainTextBody;
@InvocableVariable(label='Html Body')
public String htmlBody;
@InvocableVariable(label='Use the sender email signature *Default is true')
public Boolean useSignature;
@InvocableVariable(label='File Ids *ContentVersionId, AttchmentId or DocumentId')
public String[] fileIds;
@InvocableVariable(label='Related Record Id')
public String whatId;
@InvocableVariable(label='Target Object Id(Contact, Lead, User) *Required if template Id is set')
public String targetObjectId;
@InvocableVariable(label='Set targetObjectId as recipient *Default is true')
public Boolean treatTargetObjectAsRecipient;
@InvocableVariable(label='Save As Activity *Save to targetObjectId and whatId records, default is true')
public Boolean saveAsActivity;
}
/**
* Result
*/
public class Result {
@InvocableVariable(label='Send result *true=success、false=failure')
public Boolean isSuccess;
@InvocableVariable(label='Error message')
public String errorMessage;
}
/**
* Send Email
* @description Send single email with attachments
* @param requests Email parameters
* @return Result of sending email
*/
@InvocableMethod(label='Send Email (Send single email with attachments)')
public static List<Result> sendEmail(List<Request> requests) {
Request req = requests[0];
Result result = new Result();
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
//Recipient
mail.setToAddresses(req.toAddresses);
mail.setCcAddresses(req.ccAddresses);
mail.setBccAddresses(req.bccAddresses);
if(req.bccSender != null){
mail.setBccSender(req.bccSender);
}
//Sender
if(!String.isBlank(req.orgWideEmailAddressId)){
mail.setOrgWideEmailAddressId(req.orgWideEmailAddressId);
}
else{
mail.setSenderDisplayName(req.senderDisplayName);
}
//Subject and body
if(!String.isBlank(req.templateId)){
mail.setTemplateId(req.templateId);
}
else{
mail.setSubject(req.subject);
mail.setHtmlBody(req.htmlBody);
mail.setPlainTextBody(req.plainTextBody);
}
//Use the sender email signature
if(req.useSignature != null){
mail.setUseSignature(req.useSignature);
}
//Attachment
mail.setEntityAttachments(req.fileIds);
//Related record Id
mail.setWhatId(req.whatId);
//Target object record Id (Contact, Lead, User)
mail.setTargetObjectId(req.targetObjectId);
//Set the target object Id (Contact, Lead, User) as the recipient
if(req.treatTargetObjectAsRecipient != null){
mail.setTreatTargetObjectAsRecipient(req.treatTargetObjectAsRecipient);
}
//Save email to activity
if(req.saveAsActivity != null){
mail.setSaveAsActivity(req.saveAsActivity);
}
//Send email
Messaging.SendEmailResult[] sendResults =
Messaging.sendEmail(new Messaging.SingleEmailMessage[] {mail}, false);
result.isSuccess = sendResults[0].success;
if(!result.isSuccess) {
result.errorMessage = '';
for(Messaging.SendEmailError error : sendResults[0].getErrors()){
result.errorMessage += error.getMessage() + ';';
}
}
return new List<Result> {result};
}
}
Request class
Excerpts
/**
* Request
*/
public class Request {
@InvocableVariable(label='To Addresses')
public String[] toAddresses;
@InvocableVariable(label='Cc Addresses')
public String[] ccAddresses;
@InvocableVariable(label='Bcc Addresses')
public String[] bccAddresses;
@InvocableVariable(label='Set email sender to Bcc *Default is false')
public Boolean bccSender;
Create variables in the Request class for input when invoked in a flow. To make the variable callable in the flow, use @InvocableVariable for the variable.
label: The display label for the variable. Default is the variable name. It is displayed on the flow screen.
description: The description of the variable. It is not displayed on the flow screen.
required: Specify whether the variable is required or not. The default is false.
In the flow, only the label is displayed and not the description when Apex is called. For example, only the label will be displayed in the flow, even if the description is included as follows.
@InvocableVariable(label='Set email sender to Bcc description='Default is false')
Setup screen in the flow
So, if you need a description of the variable, include it in the label so that the description is also displayed, and do not use the description option.
@InvocableVariable(label='Set email sender to Bcc *Default is false')
Setup screen in the flow
Since this will be a generic email sending Apex class, any variables will default to required=false and the required option will be omitted.
Result class
/**
* Result
*/
public class Result {
@InvocableVariable(label='Send result *true=success、false=failure')
public Boolean isSuccess;
@InvocableVariable(label='Error message')
public String errorMessage;
}
Creates the results in the Result class. Returns the Send result (isSuccess) and Error message (errorMessage).
sendEmail method
/**
* Send Email
* @description Send single email with attachments
* @param requests Email parameters
* @return Result of sending email
*/
@InvocableMethod(label='Send Email (Send single email with attachments)')
public static List<Result> sendEmail(List<Request> requests) {
Request req = requests[0];
Result result = new Result();
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
Use @InvocableMethod for methods to make them callable in the flow.
label: Display label for the method. Default is the method name. It is displayed on the flow screen.
Other supported modifiers include description, callout, category, configurationEditor, and iconName. For more information, see the Apex Developer's Guide.
The description appears in the Apex Developer's Guide as "The description for the method, which appears as the action description in Flow Builder", but it is not actually displayed anywhere in the flow. Include it in the label so that a description appears.
@InvocableMethod(label='Send Email (Send single email with attachments)')
InvocableMethod requires both input and output to be the list. Since each is a single piece of data, albeit a list, the input parameter Requests is received in requests[0].
Recipient
//Recipient
mail.setToAddresses(req.toAddresses);
mail.setCcAddresses(req.ccAddresses);
mail.setBccAddresses(req.bccAddresses);
if(req.bccSender != null){
mail.setBccSender(req.bccSender);
}
Multiple addresses can be set for toAddresses, ccAddresses, and bccAddresses, respectively. Collection variables of text type must be created to be passed as a parameter in the flow.
bccSender sets the email sender to Bcc if bccSender is true. The default is false.
Sender
//Sender
if(!String.isBlank(req.orgWideEmailAddressId)){
mail.setOrgWideEmailAddressId(req.orgWideEmailAddressId);
}
else{
mail.setSenderDisplayName(req.senderDisplayName);
}
If the Organization-Wide Email Address Id (orgWideEmailAddressId) is set, the Sender Name (senderDisplayName) cannot be set. If both are set, an error will occur. Here, because of the conditional branching, it is not an error even if both are set, but the Sender Name is ignored.
Subject and body
//Subject and body
if(!String.isBlank(req.templateId)){
mail.setTemplateId(req.templateId);
}
else{
mail.setSubject(req.subject);
mail.setHtmlBody(req.htmlBody);
mail.setPlainTextBody(req.plainTextBody);
}
If the templateId is set, the content of the template is used for the subject line and body; if not, the content of subject, htmlBody, and plainTextBody is used for the subject line and body. If the templateId is set and the subject, htmlBody, and plainTextBody are set, the content of the subject and body will be overwritten by the content of the subject, htmlBody, and plainTextBody, although there is no error if all of them are set at the same time. The contents of the subject, htmlBody, and plainTextBody will be overwritten.
Use the sender email signature
//Use the sender email signature
if(req.useSignature != null){
mail.setUseSignature(req.useSignature);
}
When the useSignatrure is set to true, the sender's signature is added to the end of the body of the email. The user's signature is set from the own icon in the upper right corner of the screen > Settings > Email > My Email Settings. The default is true.
To avoid duplicate signatures, you must be aware of whether to use the signature in the body of the email or the user's signature. For example, if a signature is included in the body of the email and the user's own signature is set, the default is true, so the user's signature will be displayed below the signature in the body of the template, resulting in a duplicate signature.
Also, be careful not to set useSignatrure=false when testing, as the user's signature is not set and will not be doubled without setting useSignatrure=false, but another user who has set their signature will have their signature doubled when sending the message.
If you want to use the signature in the body of the email and not use the user's signature, explicitly set useSignatrure=false in the caller. Conversely, if the user's signature is to be used, the signature should not be included in the body of the mail.
Own icon in the upper right corner of the screen > Settings > Email > My Email Settings
Attachment
//Attachment
mail.setEntityAttachments(req.fileIds);
The fileIds can be set to multiple file Ids. Collection variables of text type must be created to be passed as a parameter in the flow.
Set ContentVersionId, AttachmentId, and DocumentId.
Related record Id
//Related record Id
mail.setWhatId(req.whatId);
When using a template, set the target record Id to whatId if the merged fields are used in the email template. Even if the template is not used, setting whatId will save the activity in the whatId record after the email is sent when Save Email to Activity (saveAsActivity) is true.
Target object record Id (Contact, Lead, User)
//Target object record Id (Contact, Lead, User)
mail.setTargetObjectId(req.targetObjectId);
The targetObjectId is set to the Id of the Contact, Lead, or User. The targetObjectId is required if the templateId is set. If the templateId is set and the targetObjectId is not set, an error will occur. When sending a single email as in this case, only a single Id can be set for the targetObjectId.
When using a template, the object fields of targetObjectId can be used as merged fields.
When Set the target object Id (Contact, Lead, User) as the recipient (targetTargetObjectAsRecipient) is true, the email addresses of the Contact, Lead, and User will be set to To recipient.
Also, when Save email to activity (saveAsActivity) is true, the activity is saved in the targetObjectId record after the email is sent.
Set the target object Id (Contact, Lead, User) as the recipient
//Set the target object Id (Contact, Lead, User) as the recipient
if(req.treatTargetObjectAsRecipient != null){
mail.setTreatTargetObjectAsRecipient(req.treatTargetObjectAsRecipient);
}
When the targetTargetObjectAsRecipient is true, the email address of the Target object record Id (Contact, Lead, User) (targetObjectId) is set to To recipient. The default is true.
Save email to activity
//Save email to activity
if(req.saveAsActivity != null){
mail.setSaveAsActivity(req.saveAsActivity);
}
When the saveAsActivity is true, the activity is saved in the Target object record Id (Contact, Lead, User) (targetObjectId) and the Related record Id (whatId) record after the email is sent. The default is true.
Send email
//Send email
Messaging.SendEmailResult[] sendResults =
Messaging.sendEmail(new Messaging.SingleEmailMessage[] {mail}, false);
result.isSuccess = sendResults[0].success;
if(!result.isSuccess) {
result.errorMessage = '';
for(Messaging.SendEmailError error : sendResults[0].getErrors()){
result.errorMessage += error.getMessage() + ';';
}
}
return new List<Result> {result};
Performs a single email send and returns the result. Specify the emails and allOrNothing options as arguments to Messaging.sendEmail.
emails: Multiple email messages can be set. Here, only one email is sent and "mail" is set.
allOrNothing: The optional opt_allOrNone parameter specifies whether sendEmail prevents delivery of all other messages when any of the messages fail due to an error (true), or whether it allows delivery of the messages that don't have errors (false). The default is true. The default is true. Here it is set to false.
For example, to send 3 emails, look like this:
allOrNothing=true: If 3 emails are sent and the third email has an error, all emails will not be sent.
allOrNothing=false: If 3 emails are sent and the third email has an error, the first and second emails will be sent.
Since I am sending a single email here, sending or not sending other emails is an irrelevant option. However, if allOrNothing is true and some error occurs at runtime, processing stops when the Messaging.sendEmail method is executed.
However, if allOrNothing is set to true and some error occurs during execution, processing will stop when the Messaging.sendEmail method is executed. If allOrNothing is set to false, processing does not stop if there is an error, and the result is returned with an error. Set false here.
Test class
SendEmailWithAttachmentTest.cls
/**
* SendEmailWithAttachment Test class
*/
@isTest
private class SendEmailWithAttachmentTest {
/**
* Normal
*/
@isTest
private static void test_normal01(){
SendEmailWithAttachment.Request req = new SendEmailWithAttachment.Request();
req.toAddresses = new String[]{'test01@testsfdc.com'};
req.ccAddresses = new String[]{'test02@testsfdc.com'};
req.subject = 'Invoice';
req.plainTextBody = 'Please find the attached invoice.';
req.bccSender = true;
req.useSignature = true;
req.saveAsActivity = true;
Test.startTest();
List<SendEmailWithAttachment.Result> results =
SendEmailWithAttachment.sendEmail(new List<SendEmailWithAttachment.Request>{req});
Test.stopTest();
System.assertEquals(true, results[0].isSuccess);
}
/**
* Normal
* Using Email template
*/
@isTest
private static void test_normal02(){
SendEmailWithAttachment.Request req = new SendEmailWithAttachment.Request();
req.toAddresses = new String[]{'test01@testsfdc.com'};
req.ccAddresses = new String[]{'test02@testsfdc.com'};
req.templateId = createEmailTemplate().Id;
req.targetObjectId = UserInfo.getUserId();
req.saveAsActivity = false;
req.treatTargetObjectAsRecipient = true;
Test.startTest();
List<SendEmailWithAttachment.Result> results =
SendEmailWithAttachment.sendEmail(new List<SendEmailWithAttachment.Request>{req});
Test.stopTest();
System.assertEquals(true, results[0].isSuccess);
}
/**
* Normal
* Using OrgWideEmailAddress
*/
@isTest
private static void test_normal03(){
SendEmailWithAttachment.Request req = new SendEmailWithAttachment.Request();
req.toAddresses = new String[]{'test01@testsfdc.com'};
req.subject = 'Invoice';
req.plainTextBody = 'Please find the attached invoice.';
//get orgWideEmailAddress
Boolean isSuccess;
List<OrgWideEmailAddress> orgList = [SELECT Id FROM OrgWideEmailAddress LIMIT 1];
if(orgList.size() == 1){
req.orgWideEmailAddressId = orgList[0].Id;
isSuccess = true;
}
else{
req.orgWideEmailAddressId = '0D2000000000000000';
isSuccess = false;
}
Test.startTest();
List<SendEmailWithAttachment.Result> results =
SendEmailWithAttachment.sendEmail(new List<SendEmailWithAttachment.Request>{req});
Test.stopTest();
System.assertEquals(isSuccess, results[0].isSuccess);
}
/**
* Error
* Email address formatting error
*/
@isTest
private static void test_error01(){
SendEmailWithAttachment.Request req = new SendEmailWithAttachment.Request();
req.toAddresses = new String[]{'test01testsfdccom'};
req.subject = 'Invoice';
req.plainTextBody = 'Please find the attached invoice.';
Test.startTest();
List<SendEmailWithAttachment.Result> results =
SendEmailWithAttachment.sendEmail(new List<SendEmailWithAttachment.Request>{req});
Test.stopTest();
System.assertEquals(false, results[0].isSuccess);
}
/**
* Create EmailTemplate
*/
private static EmailTemplate createEmailTemplate(){
EmailTemplate e = new EmailTemplate(
Name = 'test',
DeveloperName = 'test',
FolderId = UserInfo.getUserId(),
TemplateType = 'Text',
Subject = 'Invoice',
Body = 'Please find the attached invoice.',
IsActive = true
);
insert e;
return e;
}
}
OrgWideEmailAddress
//get orgWideEmailAddress
Boolean isSuccess;
List<OrgWideEmailAddress> orgList = [SELECT Id FROM OrgWideEmailAddress LIMIT 1];
if(orgList.size() == 1){
req.orgWideEmailAddressId = orgList[0].Id;
isSuccess = true;
}
else{
req.orgWideEmailAddressId = '0D2000000000000000';
isSuccess = false;
}
The OrgWideEmailAddress records cannot be created in the test class. Therefore, if there is an organization address in the execution environment, it is obtained; if not, "0D20000000000000000000" is set. " 0D200000000000000000000000" is a non-existent record, so the expected result is an error.
Memo
The number of emails sent can be obtained with Limits.getEmailInvocations(). The result of the number of emails sent can be checked in the test class as follows. In this case, the result is 1 for success and 0 for failure due to a single email.
System.assertEquals(1, Limits.getEmailInvocations());
However, if the allOrNothing argument of Messaging.sendEmail is set to false when sending an email, the getEmailInvocations method will return the number of emails sent even in case of failure. The result of the result class is used to check for success or error.
Error examples
Examples of an error when executing Messaging.sendEmail are as follows.
Error list
Error Messages | Error Contents |
REQUIRED_FIELD_MISSING, Add a recipient to send an email.: [] | None of the recipients are set. |
INVALID_EMAIL_ADDRESS, Email address is invalid: | The email address format is incorrect. |
REQUIRED_FIELD_MISSING, Missing targetObjectId with template: [] | templateId is set but targetObjectId is not set. |
INVALID_SAVE_AS_ACTIVITY_FLAG, saveAsActivity must be false when sending mail to users.: [saveAsActivity, true] | UserId is set in targetObjectId and saveAsActivity is true. |
INVALID_ID_FIELD, SaveAsActivity is not allowed with whatId that is not supported as a task whatId. | saveAsActivity is true and "Allow Activities" for the whatId object is off. |
Single email is not enabled for your organization or profile. | From Setup > Email> Deliverability, Access level in Access to Send Email (All Email Services) is not set to "All Email". |
Memo
If the format of the email address is correct and the email address does not exist, there will be no error. The email transmission is considered successful upon completion, and the result of an undeliverable address is not verified.
To check for undeliverables, go to Setup > Email > Deliverability > Bounce Management (Emails from Salesforce or Email Relay Only) Settings.
Activate bounce management: When checked on, IsBounced in EmailMessage record is updated with true. When SaveAsActivity is true, the activity will also show "Bounced".
Return bounced emails to sender: When checked on, the sender will receive undeliverable emails from Salesforce.
During the non-delivery test, a non-delivery email was received from the following Salesforce address.
Sender: Mail Delivery System <mailer-daemon@salesforce.com>
The result of the undeliverable error takes immediately to several days after the email is sent. It took one day for a non-existent domain.