Unsubscribe Apex Email Service
Apex email services. The short story here is that you can send email to your Salesforce Org which is then processed by some Apex logic in order to support the specific needs of your business.
One of our clients uses an Apex email service to automate Case creation for customer support issues. We have another client that has an email service for parsing CSV attachments in order to update their Org exchange rates. Just two ideas for how real-world Salesforce customers use Apex email services for automation.
Anyway, maybe you've seen that Apex code sample that uses an Apex email service for handling email opt outs? Here's a reimagined take on that logic.
/*
Created by: Greg Hacic
Last Update: 25 October 2019 by Greg Hacic
Questions?: greg@interactiveties.com
Notes:
- InboundEmailHandler interface to set hasOptedOutOfEmail to TRUE for Contact and Lead records
- tests located at unsubscribeTest.cls
*/
public class unsubscribe implements Messaging.InboundEmailHandler {
//access an InboundEmail object to retrieve the contents, headers and attachments of inbound emails
public Messaging.InboundEmailResult handleInboundEmail(Messaging.InboundEmail email, Messaging.InboundEnvelope envelope) {
Messaging.InboundEmailResult result = new Messaging.InboundEmailResult(); //InboundEmailResult object for returning the result of the Apex Email Service
if (String.isNotBlank(email.subject) && email.subject.containsIgnoreCase('unsubscribe')) { //if the subject is not null, blank or empaty '' AND the subject contains the string 'unsubscribe' (regardless of case)
if (!System.isFuture() && !System.isBatch()) { //if not currently in future or batch context
handleUnsubscribeFuture(envelope.fromAddress); //process the email address asynchronously
} else { //otherwise
handleUnsubscribe(envelope.fromAddress); //process the email address synchronously
}
}
result.success = true; //set the InboundEmailResult.Success boolean to true
return result; //return the result
}
//updates the hasOptedOutOfEmail value to false for a passed email address and object name
public static void hasOptedOutOfEmail(String emailAddress, String objectName) {
List<SObject> updatedRecords = new List<SObject>(); //list of SObjects
String soql = 'SELECT Id FROM '+objectName+' WHERE Email = \''+emailAddress+'\' AND hasOptedOutOfEmail = FALSE'; //construct the query statement
List<SObject> objectList = Database.query(soql); //execute the query > populate the list
Schema.SObjectType t = Schema.getGlobalDescribe().get(objectName); //grab the sObject token for the object name passed to the method
for (SObject r : objectList) { //for each object in the list
SObject o = t.newSObject(r.Id); //construct a new sObject of this type, with the specified Id
o.put('hasOptedOutOfEmail', TRUE); //set the hasOptedOutOfEmail value to TRUE
updatedRecords.add(o); //add the SObject to the list
}
if (!updatedRecords.isEmpty()) { //if the list is not empty
List<Database.SaveResult> updateResults = Database.update(updatedRecords, false); //update SObject records allow for partial fail
}
}
//execute the handleUnsubscribe method asynchronously
public static void handleUnsubscribe(String s) {
hasOptedOutOfEmail(s, 'Lead'); //handle Lead Opt Outs
hasOptedOutOfEmail(s, 'Contact'); //handle Contact Opt Outs
}
//execute the handleUnsubscribe method asynchronously
@future //future annotation indicates asynchronous execution
public static void handleUnsubscribeFuture(String s) {
handleUnsubscribe(s); //pass the string to the handleUnsubscribe method
}
}
Removing the tests from the Apex class itself is a best practice. That test coverage is done in the class below:
/*
Created by: Greg Hacic
Last Update: 25 October 2019 by Greg Hacic
Questions?: greg@interactiveties.com
Notes:
- tests unsubscribe.cls (95.83% coverage)
*/
@isTest
private class unsubscribeTest {
//tests unsubscribe
@isTest //defines method for use during testing only
static void validSubject() {
//BEGIN: Some Setup Items...
//insert an account
List<Account> accounts = new List<Account>();
accounts.add(new Account(BillingCity = 'Denver', BillingPostalCode = '80202', BillingStreet = '201 W Colfax Ave, First Floor', BillingState = 'CO', BillingCountry = 'US', Name = 'Tess Trucking Co.', ShippingCity = 'Denver', ShippingPostalCode = '80202', ShippingStreet = '201 W Colfax Ave, First Floor', ShippingState = 'CO', ShippingCountry = 'US'));
accounts.add(new Account(BillingCity = 'Buffalo', BillingPostalCode = '14202', BillingStreet = '65 Niagara Square', BillingState = 'NY', BillingCountry = 'US', Name = 'Tess Financial Corp', ShippingCity = 'Buffalo', ShippingPostalCode = '14202', ShippingStreet = '65 Niagara Square', ShippingState = 'NY', ShippingCountry = 'US'));
accounts.add(new Account(BillingCity = 'San Francisco', BillingPostalCode = '94110', BillingStreet = '3180 18th St', BillingState = 'CA', BillingCountry = 'US', Name = 'Tess Health LLC'));
insert accounts;
//insert Contacts
List<Contact> contacts = new List<Contact>();
contacts.add(new Contact(AccountId = accounts[0].Id, Email = 'tess@example.com', FirstName = 'Tess', LastName = 'Dachshund'));
contacts.add(new Contact(AccountId = accounts[0].Id, Email = 'fake.name@example.com', FirstName = 'Grachus', LastName = 'Dachshund'));
contacts.add(new Contact(AccountId = accounts[1].Id, Email = 'fake.name@example.com', FirstName = 'Priscus', LastName = 'Dachshund'));
contacts.add(new Contact(AccountId = accounts[1].Id, Email = 'spartacus@example.com', FirstName = 'Spartacus', LastName = 'Dachshund'));
contacts.add(new Contact(AccountId = accounts[2].Id, Email = 'crixus@example.com', FirstName = 'Crixus', LastName = 'Dachshund'));
contacts.add(new Contact(AccountId = accounts[2].Id, Email = 'fake.name@example.com', FirstName = 'Oenomaus', LastName = 'Dachshund'));
contacts.add(new Contact(AccountId = accounts[2].Id, Email = 'gannicus@example.com', FirstName = 'Gannicus', LastName = 'Dachshund'));
insert contacts;
//insert Leads
List<Lead> leads = new List<Lead>();
for (Integer i = 0; i < 100; i++) {
leads.add(new Lead(Company = 'Example Corp', Email = 'fake.name@example.com', FirstName = 'Fake', LastName = 'Person'));
}
insert leads;
//END: Some Setup Items...
Test.startTest(); //denote testing context
//email
Messaging.InboundEmail email = new Messaging.InboundEmail(); //construct a new inbound email object
email.fromAddress = 'fake.name@example.com';
email.fromName = 'Fake Name';
email.htmlBody = '<p>I would really like to be removed from your lists.</p>';
email.plainTextBody = 'I would really like to be removed from your lists.';
email.subject = 'Remove Me From Your List Unsubscribe';
//envelope
Messaging.InboundEnvelope envelope = new Messaging.InboundEnvelope(); //construct a new inbound envelope object that stores the envelope information associated with the inbound email
envelope.fromAddress = 'fake.name@example.com';
unsubscribe handler = new unsubscribe();
handler.handleInboundEmail(email, envelope);
Test.stopTest(); //revert from testing context
//validate the logic
for (Lead l : [SELECT Email, hasOptedOutOfEmail, Id FROM Lead]) { //for all leads
System.assertEquals(true, l.hasOptedOutOfEmail); //hasOptedOutOfEmail = true
}
for (Contact c : [SELECT Email, hasOptedOutOfEmail, Id FROM Contact]) { //for all contacts
if (c.Email == 'fake.name@example.com') {
System.assertEquals(true, c.hasOptedOutOfEmail); //hasOptedOutOfEmail = true
} else {
System.assertEquals(false, c.hasOptedOutOfEmail); //hasOptedOutOfEmail = false
}
}
}
//tests unsubscribe
@isTest //defines method for use during testing only
static void invalidSubject() {
//BEGIN: Some Setup Items...
//insert an account
List<Account> accounts = new List<Account>();
accounts.add(new Account(BillingCity = 'Denver', BillingPostalCode = '80202', BillingStreet = '201 W Colfax Ave', BillingState = 'CO', BillingCountry = 'US', Name = 'Tess Corp'));
insert accounts;
//insert Contacts
List<Contact> contacts = new List<Contact>();
for (Integer i = 0; i < 100; i++) {
contacts.add(new Contact(AccountId = accounts[0].Id, Email = 'fake.name@example.com', FirstName = 'Tess', LastName = 'Dachshund'+i));
}
insert contacts;
//insert Leads
List<Lead> leads = new List<Lead>();
for (Integer i = 0; i < 100; i++) {
leads.add(new Lead(Company = 'Example Corp', Email = 'fake.name@example.com', FirstName = 'Fake', LastName = 'Person'));
}
insert leads;
//END: Some Setup Items...
Test.startTest(); //denote testing context
//email
Messaging.InboundEmail email = new Messaging.InboundEmail(); //construct a new inbound email object
email.fromAddress = 'fake.name@example.com';
email.fromName = 'Fake Name';
email.htmlBody = '<p>I would really like to be removed from your lists.</p>';
email.plainTextBody = 'I would really like to be removed from your lists.';
email.subject = 'Remove Me From Your List';
//envelope
Messaging.InboundEnvelope envelope = new Messaging.InboundEnvelope(); //construct a new inbound envelope object that stores the envelope information associated with the inbound email
envelope.fromAddress = 'fake.name@example.com';
unsubscribe handler = new unsubscribe();
handler.handleInboundEmail(email, envelope);
Test.stopTest(); //revert from testing context
//validate the logic
for (Lead l : [SELECT Email, hasOptedOutOfEmail, Id FROM Lead]) { //for all leads
System.assertEquals(false, l.hasOptedOutOfEmail); //hasOptedOutOfEmail = false
}
for (Contact c : [SELECT Email, hasOptedOutOfEmail, Id FROM Contact]) { //for all contacts
System.assertEquals(false, c.hasOptedOutOfEmail); //hasOptedOutOfEmail = false
}
}
}