One of the coolest things about Salesforce is that you can send emails programmatically right from your Apex code. This is super useful for things like alerts, notifications, or even follow-ups.
In this post, we’ll walk through a simple method that accepts an email address as input and sends a predefined email to that address. Then we’ll take it further: calling it from a trigger without hitting limits, sending to users and contacts, using an email template, checking whether the send worked, and testing it.
If you’re new to Apex or just starting out with Salesforce automation, don’t worry. We’ll break everything down so it’s easy to follow.
What Are We Trying to Do?
Imagine a scenario where your Salesforce org needs to send a quick alert to a user or customer. Maybe someone just submitted a form, or their case was updated.
Instead of manually writing and sending emails, we can create an Apex method that:
- Accepts an email address as input
- Builds an email message
- Sends it automatically through Salesforce
That’s exactly what the method below does.
Code Explanation
public with sharing class ApexUseCaseThree {
public static void sendAlertToCustomer(String emailAddress){
We start by declaring a class named ApexUseCaseThree and inside it, we define a static method called sendAlertToCustomer. This method takes one input: the recipient’s email address.
if(!String.isBlank(emailAddress)){
Before proceeding, we check that the email address is not blank. We don’t want to attempt sending an email without an address, because it would cause an error.
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
Here, we create a new email object using Salesforce’s built-in Messaging.SingleEmailMessage class. This object holds all the details about the email we’re about to send.
List<String> toAddressList = new List<String>{emailAddress};
We prepare a list containing the recipient’s email. Salesforce expects email addresses in a list, even if you’re only sending to one person.
mail.setSubject('URGENT - Take a look!');
mail.setPlainTextBody('This is a notification triggered by Salesforce via Apex for you!');
mail.setToAddresses(toAddressList);
We set the subject line, the body text and the recipient list. These are the core parts of a basic email.
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
Finally, we call Messaging.sendEmail() and pass in our email object inside a list. This sends the email to the recipient.
Final Code Snippet
public with sharing class ApexUseCaseThree {
public static void sendAlertToCustomer(String emailAddress){
if(!String.isBlank(emailAddress)){
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
List<String> toAddressList = new List<String>{emailAddress};
mail.setSubject('URGENT - Take a look!');
mail.setPlainTextBody('This is a notification triggered by Salesforce via Apex for you!');
mail.setToAddresses(toAddressList);
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
}
}
Why This Is Useful
This method is great for sending alerts from Apex, like notifying users when a record is updated or when an error occurs. It’s reusable and lightweight. The sections below show how to plug it into a trigger and how to call it from a Flow.
Send Email from an Apex Trigger
The method above sends one email per call. That is fine for a button click. It is a problem inside a trigger, because Salesforce allows only 10 sendEmail calls per transaction. Update 11 records in one go and the trigger fails with System.LimitException: Too many Email Invocations: 11.
The fix is the same as for SOQL and DML: don’t do it inside the loop. Build all the messages first, then send them with one call.
trigger CaseTrigger on Case (after update) {
List<Messaging.SingleEmailMessage> mails = new List<Messaging.SingleEmailMessage>();
for (Case c : Trigger.new) {
Case oldCase = Trigger.oldMap.get(c.Id);
Boolean justClosed = c.IsClosed && !oldCase.IsClosed;
if (justClosed && String.isNotBlank(c.SuppliedEmail)) {
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setToAddresses(new List<String>{ c.SuppliedEmail });
mail.setSubject('Your case ' + c.CaseNumber + ' is closed');
mail.setPlainTextBody('We have closed your case. Reply to this email if you still need help.');
mails.add(mail);
}
}
if (!mails.isEmpty()) {
Messaging.sendEmail(mails); // one call, however many cases
}
}
Three things to notice:
- The email goes out only when the case changes to closed. Comparing
Trigger.newwithTrigger.oldMapstops a second email every time someone edits a closed case. - All messages go in one list and one
sendEmailcall, so 200 cases cost one call, not 200. - Salesforce sends the emails only if the transaction commits. If a later validation rule or trigger fails and the save rolls back, no email leaves.
In a real org, keep the trigger thin and move this logic into a handler class. See Apex best practices for the pattern.
Send to a User, Contact or Lead with setTargetObjectId
If the recipient is a record in Salesforce, pass the record ID instead of typing the address. Salesforce reads the email address from the record.
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setTargetObjectId(UserInfo.getUserId()); // a User, Contact or Lead Id
mail.setSaveAsActivity(false); // must be false when the target is a User
mail.setSubject('Nightly job finished');
mail.setPlainTextBody('The account clean-up job has finished.');
Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{ mail });
Emails sent to internal users this way don’t count against the daily email limit, which makes it the right choice for admin and job notifications.
Use an Email Template
Hardcoding the subject and body in Apex means a developer has to change code every time marketing wants a new sentence. With a template, an admin edits the wording in Setup.
EmailTemplate template = [
SELECT Id FROM EmailTemplate WHERE DeveloperName = 'Case_Closed' LIMIT 1
];
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setTemplateId(template.Id);
mail.setTargetObjectId(contactId); // required with a template: Contact, Lead or User
mail.setWhatId(caseId); // the record the merge fields read from
Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{ mail });
setWhatId is for when the target is a Contact. If you send to a User, leave it out, or Salesforce rejects the email.
HTML Body, CC, Reply-To and Sender
mail.setHtmlBody('<p>Hi ' + firstName + ',</p><p>Your case is closed.</p>');
mail.setCcAddresses(new List<String>{ 'manager@example.com' });
mail.setReplyTo('support@example.com');
mail.setSenderDisplayName('Support Team');
To send from a shared address such as support@yourcompany.com, set it up under Setup as an Organization-Wide Address and reference it. Use this instead of setSenderDisplayName, not together with it.
OrgWideEmailAddress owa = [
SELECT Id FROM OrgWideEmailAddress WHERE Address = 'support@example.com' LIMIT 1
];
mail.setOrgWideEmailAddressId(owa.Id);
Attach a File
Messaging.EmailFileAttachment attachment = new Messaging.EmailFileAttachment();
attachment.setFileName('summary.pdf');
attachment.setBody(pdfBlob); // a Blob, for example from a ContentVersion
mail.setFileAttachments(new List<Messaging.EmailFileAttachment>{ attachment });
Check Whether the Email Was Accepted
By default, one bad address makes the whole sendEmail call throw an exception. Pass false as the second argument and Salesforce sends what it can and tells you what failed.
List<Messaging.SendEmailResult> results = Messaging.sendEmail(mails, false);
for (Messaging.SendEmailResult result : results) {
if (!result.isSuccess()) {
System.debug(LoggingLevel.ERROR, result.getErrors()[0].getMessage());
}
}
Success here means Salesforce accepted the message for delivery. It does not mean the email reached the inbox.
Email Limits in Apex
| Limit | Value |
|---|---|
sendEmail calls per transaction | 10 |
| Single emails to external addresses | 5,000 per day for the whole org, counted on GMT time |
Emails to internal users through setTargetObjectId | Not counted against the daily limit |
| Developer Edition and trial orgs | Much lower daily limits |
Two methods help you stay inside them. Limits.getEmailInvocations() tells you how many calls the transaction has used. Messaging.reserveSingleEmailCapacity(count) throws an exception straight away if sending that many emails would cross the daily limit, so you find out before doing the rest of the work.
Common Errors and Fixes
| Error | Why it happens | Fix |
|---|---|---|
NO_MASS_MAIL_PERMISSION, Single email is not enabled for your organization or profile | Email deliverability is switched off. Sandboxes start on System email only. | Setup, Email, Deliverability: set Access level to All email. |
Too many Email Invocations: 11 | sendEmail is called inside a loop. | Collect the messages in a list and send once. |
INVALID_EMAIL_ADDRESS | The address is malformed. | Validate the address, and use sendEmail(mails, false) so one bad address does not block the rest. |
SINGLE_EMAIL_LIMIT_EXCEEDED | The org has used its daily allowance. | Send to internal users by ID, reduce volume, or move bulk sends to a marketing tool. |
saveAsActivity must be false when sending mail to users | The target is a User and activity logging is on. | Call setSaveAsActivity(false). |
How to Test It
Apex tests never send real email, so you can’t check an inbox. Check that the code made the call you expected.
@isTest
private class ApexUseCaseThreeTest {
@isTest
static void sendsOneEmail() {
Test.startTest();
ApexUseCaseThree.sendAlertToCustomer('test@example.com');
Integer invocations = Limits.getEmailInvocations();
Test.stopTest();
Assert.areEqual(1, invocations, 'Expected one sendEmail call');
}
@isTest
static void skipsBlankAddress() {
Test.startTest();
ApexUseCaseThree.sendAlertToCustomer('');
Integer invocations = Limits.getEmailInvocations();
Test.stopTest();
Assert.areEqual(0, invocations, 'No email expected for a blank address');
}
}
If this test fails in a sandbox with the NO_MASS_MAIL_PERMISSION error, the deliverability setting above is the cause, not your code.
Call It from a Flow
Flow can only call Apex methods marked @InvocableMethod, and those methods must accept a list. That suits us, because a list is what bulk-safe email code needs anyway.
public with sharing class SendAlertAction {
@InvocableMethod(label='Send Alert Email' description='Sends a plain text alert to each address')
public static void sendAlerts(List<String> emailAddresses) {
List<Messaging.SingleEmailMessage> mails = new List<Messaging.SingleEmailMessage>();
for (String address : emailAddresses) {
if (String.isBlank(address)) {
continue;
}
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setToAddresses(new List<String>{ address });
mail.setSubject('URGENT - Take a look!');
mail.setPlainTextBody('This is a notification triggered by Salesforce via Apex for you!');
mails.add(mail);
}
if (!mails.isEmpty()) {
Messaging.sendEmail(mails);
}
}
}
FAQs
Can an Apex trigger send an email?
Yes. Build a list of Messaging.SingleEmailMessage objects in the trigger or its handler and call Messaging.sendEmail once, after the loop.
Is the email sent if the transaction fails?
No. Salesforce holds the email until the transaction commits. If the save rolls back, the email is dropped.
Should I use Apex or Flow to send email?
If a Flow Send Email action or an email alert covers it, use that. Reach for Apex when recipients or content are built dynamically, when you need attachments, or when the email is part of logic that already lives in Apex.
How many emails can Apex send per day?
5,000 single emails to external addresses per org per day. Emails to internal users sent by user ID are not counted.
Why does my email work in production but not in the sandbox?
New and refreshed sandboxes have deliverability set to System email only. Change it to All email in Setup.
Watch It in Action!
Prefer learning by watching? I’ve got you covered! Here’s a quick video walkthrough showing how this method works in a real Salesforce org.
Want to go further with this?
Learn it step by step in a course, or talk it through on a 1:1 call.
