Sending e-mails from Salesforce using Amazon SES

SFDC has increased from 1000 to 5000 the number of e-mails that can be sent to external e-mail addresses within Winter ’17 release. This is really good news but still, in some cases, can be insufficient.

I want to show you how you can send e-mails, in both HTML and text plain versions and with attachments using Amazon SES.

However, as of now Amazon SES doesn’t support TLS v1.1, only supports v1.0. SFDC is going to disable all the communications made with TLS v1.0 pretty soon so we need to use something in the middle between SFDC and Amazon SES. We will use Amazon API Gateway that supports TLS v1.1. We don’t know when Amazon SES will support the new protocol so we need this workaround for the time being.

Let’s take a look to the following code:

public class AWS_SES {
public static String key = '----------';
public static String secret = '----------';
public static String endpoint = 'https://xxxxxxx.execute-api.us-west-2.amazonaws.com/prod/mail';
@future(callout=true)
public static void sendEmailAsync(String mJson) {
Email m = (Email)Json.deserialize(mJson, Email.class);
String boundary = 'gc0p4Jq0M2Yt08jU534c0p';
String boundary2 = 'Yt08jU534c0pgc0p4Jq0M2';

HttpRequest httpReq = new HttpRequest();
httpReq.setMethod('POST');
httpReq.setEndpoint(endpoint);

httpReq.setHeader('myHost','email.us-west-2.amazonaws.com');
httpReq.setHeader('myContent-Type','application/x-www-form-urlencoded');
String awsFormattedNow = awsFormattedDate(Datetime.now());
httpReq.setHeader('Date',awsFormattedNow);
httpReq.setHeader('X-Amzn-Authorization', headerForAmazonAuthorization(key,signature(awsFormattedNow,secret)));

String msg = '';
// header
msg += 'From:' + m.fromAddress + '\n';
msg += 'Subject:'+ m.subject + '\n';
msg += 'MIME-Version:1.0\n';
msg += 'Content-type:multipart/mixed; boundary="' + boundary + '"\n\n';

// html and text
msg += '--' + boundary + '\n';

msg += 'Content-type:multipart/alternative; boundary="' + boundary2 + '"\n\n';

// text
msg += '--' + boundary2 + '\n';
msg += 'Content-type: text/plain;charset=utf-8\n';
msg += 'Content-Transfer-Encoding: quoted-printable\n\n';
msg += m.bodyText;
msg += '\n\n';

// html
msg += '--' + boundary2 + '\n';
msg += 'Content-Type: text/html; charset=utf-8\n';
//msg += 'Content-Transfer-Encoding: 7bit\n\n';
msg += 'Content-Transfer-Encoding: quoted-printable\n\n';
msg += m.bodyHtml;
msg += '\n\n';

// attachment
if(m.attachment != null) {
msg += '--' + boundary + '\n';
msg += 'Content-Transfer-Encoding: base64\n';
msg += 'Content-Type: '+ m.attachment.contentType +'; name=' + m.attachment.name + ';\n';
msg += 'Content-Disposition: attachment; filename=' + m.attachment.name + ';\n\n';
msg += EncodingUtil.base64Encode(m.attachment.body);

}

// close
msg += '\n\n--' + boundary + '--\n';

System.debug('##msg: ' + msg);
String encodedMessage = EncodingUtil.base64Encode(Blob.valueOf(msg));
String requestBody = 'Action=SendRawEmail&Destinations.member.1=' + EncodingUtil.urlEncode(m.toAddress,'UTF-8') + '&RawMessage.Data=' + EncodingUtil.urlEncode(encodedMessage, 'UTF-8');
System.debug('##requestBody:' + requestBody);

httpReq.setHeader('Content-Length', String.valueOf(requestBody.length()));

httpReq.setBody(requestBody);
Http http = new Http();
HttpResponse response = http.send(httpReq);

System.debug('##response: ' + response);
System.debug('##response body: ' + response.getBody());

}
public class MailAttachment {
public String name;
public Blob body;
public String contentType;
}

public class Email {
public String displayName;
public String fromAddress;
public String toAddress;
public String subject;
public String bodyText;
public String bodyHtml;
public MailAttachment attachment;
}
private static string awsFormattedDate(Datetime now)
{
return now.formatGmt('EEE, d MMM yyyy HH:mm:ss Z');
}

private static string headerForAmazonAuthorization(String accessKey, String signature)
{
return 'AWS3-HTTPS AWSAccessKeyId='+accessKey+', Algorithm=HmacSHA256, Signature='+signature;
}

private static string signature(String awsNow, String s) {
system.assert( secret != null ,' missing S3.secret key');
Blob bsig = Crypto.generateMac('HmacSHA256', Blob.valueOf(awsNow), Blob.valueOf(s));
return EncodingUtil.base64Encode(bsig);
}
}

Obviously the key and the secret should be stored properly and the endpoint shouldn’t be hardcoded. For clarity reasons lets keep it like that.
We would like to send the e-mails asynchronously, that’s why it is inside a @future method. In order to pass the parameters to this method we serialize the object in Json and it is deserialized in the body of the method.
If you need to send e-mails to more than one recipient you need to work it out. You have to look into the “Destinations.member.x” parameter.

If Amazon SES starts supporting TLS v1.1 we could get rid of the API Gateway and we would need to change a few things:

Endpoint. In my case would be https://email.us-west-2.amazonaws.com.
Headers: myHost and myContent-Type should be Host and Content-Type respectively. This is a trick in order the API Gateway to accept the request.
An example to invoke this method can be the following:

Attachment attach = [SELECT name, body, contentType FROM Attachment LIMIT 1];
AWS_SES.Email newEmail = new AWS_SES.Email();
if (attach.body != null) {
AWS_SES.MailAttachment newEmailAttachment = new AWS_SES.MailAttachment();
newEmailAttachment.name = attach.name;
newEmailAttachment.body = attach.body;
newEmailAttachment.ContentType = attach.ContentType;
newEmail.attachment = newEmailAttachment;
}
newEmail.displayName = 'Marc Benioff';
newEmail.fromAddress = '[email protected]';
newEmail.toAddress = '[email protected]';
newEmail.subject = 'Golf';
newEmail.bodyText = 'Hi buddy let's play golf together on Sunday';
newEmail.bodyHtml = '<h1>Golf</h1><p>Hi buddy let's play golf together on Sunday</p>' ;
AWS_SES.sendEmailAsync(Json.serialize(newEmail));

The from address has to be validated on AWS, on the Amazon SES settings.

The configuration for the API Gateway is straight forward. It’s a simple HTTP integration with only one tricky part. On the Integration Request mapping you have to map myHost with Host and myContent-Type with Content-Type headers. Otherwise the request to the API Gateway will fail.

And the Integration request:

Leave a Reply

avatar
  Subscribe  
Notify of