Why am I getting this Output {__type":"com.amazon.coral.service#SerializationException} when calling to an AWS SES API?

0

I have been trying to integrate the Amazon SES API into my Salesforce project so I can setup a scheduled batch job to send bulk emails.

I've created this class to create a signed request to the SES service.

public class AwsSignatureV4 {
    public static String signSesRequest(String accessKey, String secretKey, String region, String method, String path, String payload, Map<String, String> headers) {
        String amzDate = Datetime.now().formatGMT('yyyyMMdd\'T\'HHmmss\'Z\'');
        String dateStamp = Datetime.now().formatGMT('yyyyMMdd');
        String canonicalRequest = createSesCanonicalRequest(method, path, payload, headers);
        String stringToSign = createStringToSign(region, dateStamp, amzDate, canonicalRequest);
        String signature = calculateSignature(secretKey, region, dateStamp, stringToSign);
        headers.put('Authorization', buildAuthorizationHeader(accessKey, region, 'ses', amzDate, signature));

        // Construct the signed URL
        System.debug('here');
        return constructSignedUrl(method, path, headers, region);
    }

    private static String createSesCanonicalRequest(String method, String path, String payload, Map<String, String> headers) {
        List<String> canonicalHeaders = new List<String>();
        for (String key : headers.keySet()) {
            canonicalHeaders.add(key.toLowerCase() + ':' + headers.get(key).trim());
        }
        canonicalHeaders.sort();
        String canonicalHeadersStr = String.join(canonicalHeaders, '\n');

        String canonicalRequest = method + '\n' + path + '\n\n' + canonicalHeadersStr + '\n\n' + EncodingUtil.base64Encode(Blob.valueOf(payload));
    
        return canonicalRequest;
    }

    private static String createStringToSign(String region, String dateStamp, String amzDate, String canonicalRequest) {
        String stringToSign = 'AWS4-HMAC-SHA256\n' + amzDate + '\n' + dateStamp + '/' + region + '/ses/aws4_request\n' + EncodingUtil.convertToHex(Crypto.generateDigest('SHA-256', Blob.valueOf(canonicalRequest)));
        return stringToSign;
    }

    private static String calculateSignature(String secretKey, String region, String dateStamp, String stringToSign) {
        Blob kDate = Crypto.generateMac('HMACSHA256', Blob.valueOf('AWS4' + secretKey), Blob.valueOf(dateStamp));
        Blob kRegion = Crypto.generateMac('HMACSHA256', kDate, Blob.valueOf(region));
        Blob kService = Crypto.generateMac('HMACSHA256', kRegion, Blob.valueOf('ses'));
        Blob kSigning = Crypto.generateMac('HMACSHA256', kService, Blob.valueOf('aws4_request'));
        Blob signature = Crypto.generateMac('HMACSHA256', kSigning, Blob.valueOf(stringToSign));
        return EncodingUtil.convertToHex(signature);
    }

    private static String buildAuthorizationHeader(String accessKey, String region, String service, String amzDate, String signature) {
        return 'AWS4-HMAC-SHA256 Credential=' + accessKey + '/' + Datetime.now().formatGMT('yyyyMMdd') + '/' + region + '/' + service + '/aws4_request, SignedHeaders=content-type;host;x-amz-target;x-amz-date, Signature=' + signature;
    }

    private static String constructSignedUrl(String method, String path, Map<String, String> headers, String region) {
        // Include only required headers for signing
        String[] signedHeaders = new String[]{'content-type', 'host', 'x-amz-target', 'x-amz-date'};

        String queryString = '';
        for (String signedHeader : signedHeaders) {
            if (headers.containsKey(signedHeader)) {
                queryString += signedHeader + '=' + EncodingUtil.urlEncode(headers.get(signedHeader), 'UTF-8') + '&';
            }
        }
        queryString = queryString.removeEnd('&');
        return path + '?' + queryString;
    }
}

I then use this method to create/call the Http request like this

public static void sesSender(){
    String payload = '{"BulkEmailEntries":[{"Destination":{"ToAddresses":["<redacted email address>"]},"ReplacementTags":[{"Name":"unsubscribe","Value":"<redacted email address>"}]}],"DefaultContent":{"Template":{"TemplateName":"Welcome_Email"}},"DefaultEmailTags":[{"Name":"user","Value":"test"}],"FromEmailAddress":"<redacted email address>","ReplyToAddresses":["<redacted email address>"]}';
    String accessKey = ACCESS_KEY;
    String secretKey = SECRET_KEY;
    String awsRegion = REGION;
    String method = 'POST';
    Map<String, String> headers = new Map<String, String>{
        'content-type' => 'application/json',
        'x-amz-target' => 'com.amazonaws.services.simpleemailv2.AmazonSimpleEmailServiceV2.SendBulkEmail',
        'x-amz-date' => Datetime.now().formatGMT('yyyyMMdd\'T\'HHmmss\'Z\''),
        'host' => 'email.ap-southeast-2.amazonaws.com'
    };
    
    // Base64 encode the payload
    Blob payloadBlob = Blob.valueOf(payload);
    String base64Payload = EncodingUtil.base64Encode(payloadBlob);

    String endpoint = 'https://email.ap-southeast-2.amazonaws.com';

    // Sign the request
    String signedRequest;
    try{

        signedRequest = AwsSignatureV4.signSesRequest(accessKey, secretKey, awsRegion, method, endpoint, base64Payload, headers);
        System.debug(JSON.serialize(signedRequest));
    }catch(Exception e){
        System.debug(e);
    }
    
    HttpRequest req = new HttpRequest();
    req.setEndpoint(signedRequest);
    req.setMethod(method);
    req.setBody(base64Payload); // Use the Base64 encoded payload
    
    // Set headers in the HttpRequest
    for (String key : headers.keySet()) {
        req.setHeader(key, headers.get(key));
    }
    
    Http http = new Http();
    HttpResponse res = http.send(req);
    
    String responseBody = res.getBody();
    System.debug(res);
    System.debug(responseBody);
    System.debug(res.getHeaderKeys());
    for(String key : res.getHeaderKeys()){
        System.debug(res.getHeader(key));
    }
}

when I call this method, the HttpResponse does return a 200 status however the body is returning {"Output":{"__type":"com.amazon.coral.service#SerializationException"},"Version":"1.0"}

I've tried looking for information about this but have had little luck. From the little information I can find though (Stack Overflow question), my understanding is there is something wrong with the payload but I'm not even sure if that's true or what is wrong if it is. This is what I used to format my payload the way I did Amazon SES API v2 SendBulkEmail

Any help or insight would be greatly appreciated.

Jake
asked 5 months ago107 views
No Answers

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.

Guidelines for Answering Questions