Mail Attachments in B2C Commerce Cloud

A drawing show multiple ways of digital communication (email, phone, desktop)

Within Salesforce B2C Commerce Cloud, you have the option to send transactional e-mails with ISML templates. You can easily style them to your needs. But what about adding attachments? Adding attachments is something not often needed and not documented anywhere.

But not to worry! It is possible with a bit of coding magic.

Note: This solution can be used in conjunction with jsPDF.

TLDR; Solution

For the people who want a quick solution to their attachment problem without much reading work, here you go!

Controller

				
					'use strict';

var server = require('server');

/**
 * Encodes a string into a base64 string with an email-safe line width
 *
 * @param {string} str String the string to encode
 * @param {string} characterEncoding String the character encoding (i.e. 'ISO-8859-1')
 *
 * @return {string} The encoded string
 */
function encodeBase64ForEmail(str, characterEncoding) {
    var StringUtils = require('dw/util/StringUtils');
    var StringWriter = require('dw/io/StringWriter');
    var strBase64 = StringUtils.encodeBase64(str, characterEncoding);
    var strBase64LB = '';
    var stringWriter = new StringWriter();

    var offset = 0;
    var length = 76;

    while (offset < strBase64.length) {
        var maxOffset = offset + length;

        if (strBase64.length >= maxOffset) {
            stringWriter.write(strBase64, offset, length);
            stringWriter.write('\n');
        } else {
            stringWriter.write(strBase64, offset, length - (maxOffset - strBase64.length));
        }
        offset += length;
    }

    stringWriter.flush();
    strBase64LB = stringWriter.toString();
    stringWriter.close();

    return strBase64LB;
}

/**
 * Read a file to a String (encoded in IS0-8859-1)
 *
 * @param {string} filePath - The file path to read
 *
 * @return {string} - The file content
 */
function readPDFFile(filePath) {
    var File = require('dw/io/File');
    var FileReader = require('dw/io/FileReader');

    var testPDF = new File(filePath);
    var pdfReader = new FileReader(testPDF, 'ISO-8859-1');

    var pdfContent = '';
    var line = '';

    /**
     * Warning: You can reach the maximum string length with this code!
     */
    do {
        line = pdfReader.readN(1000);
        pdfContent += line;
    } while (line != null);

    return pdfContent;
}


/**
 * Add files to the attributes to render the mail template.
 *
 * @param {dw.util.Map} mailAttributes - The mail attributes
 */
function addFilesToMailAttributes(mailAttributes) {
    var Map = require('dw/util/HashMap');
    var pdfContent = readPDFFile('IMPEX/jspdf/test_0.pdf');
    var files = new Map();

    files.put('test.pdf', encodeBase64ForEmail(pdfContent, 'ISO-8859-1'));

    mailAttributes.put('Base64FileMap', files);
}

/**
 * Just an example controller to test sending a mail with attachments
 */
server.get('Test', function (req, res, next) {
    var Map = require('dw/util/HashMap');
    var Template = require('dw/util/Template');
    var Mail = require('dw/net/Mail');

    // Create the template that we will use to send the email.
    var template = new Template('mail/testMail.isml');

    // Work with a HashMap to pass the data to the template.
    var mailAttributes = new Map();
    mailAttributes.put('EmailMessage', 'Test Message');
    addFilesToMailAttributes(mailAttributes);

    var mail = new Mail();
    // Render the template with the data in the Hash
    var content = template.render(mailAttributes);

    mail.addTo('thomas.theunen@gmail.com');
    mail.setFrom('info@forward.eu');
    mail.setSubject('Example Email');
    mail.setContent(content);

    res.json({
        success: mail.send().message,
        content: content.getText()
    });

    next();
});

module.exports = server.exports();

				
			

Template

				
					<iscontent type="multipart/mixed; boundary=001a113414f6401b9c04f1451632" compact="false" charset="ISO-8859-1">--001a113414f6401b9c04f1451632
Content-Type: multipart/alternative; boundary=001a113414f6401b8604f1451630

--001a113414f6401b8604f1451630
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable

<isif condition="${!empty(pdict.EmailMessage)}"><isprint value="${pdict.EmailMessage}" /></isif>

--001a113414f6401b8604f1451630
Content-Type: text/html; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable

<isif condition="${!empty(pdict.EmailMessage)}"><isprint value="${pdict.EmailMessage}" /></isif>
<isif condition="${!empty(pdict.EmailTemplate)}"><isinclude template="${pdict.EmailTemplate}" /></isif>

<isif condition="${ !empty(pdict.Base64FileMap) }"><isloop items="${ pdict.Base64FileMap.keySet() }" var="key"><isset name="fileContent" value="${ pdict.Base64FileMap.get(key) }" scope="page"/>
--001a113414f6401b9c04f1451632
Content-Type: application/pdf; name="${key}";
Content-Description: ${key}
Content-Disposition: attachment; filename="${key}"; size=${fileContent.length}; creation-date="${(new Date()).toISOString()}"; modification-date="${(new Date()).toISOString()}"
Content-Transfer-Encoding: base64

${fileContent}</isloop>
</isif>--001a113414f6401b9c04f1451632--

				
			

Breakdown of the solution

“Give a man a fish, and he’ll eat for a day. Teach a man to fish, and he’ll eat for a lifetime.” We will be taking this approach to the code above.

You can easily copy-paste the code from above and get it to work with your project, but it is also essential to understand each piece of the puzzle. If something goes wrong or an unexpected change is needed, you will know where to look.

The controller

Within the controller, we have multiple functions to help us get all the data to send that e-mail with an attachment. 

Note: It would be best to move these to a helper file for re-use!

base64

To work with files (and emails), base64 encoding is the way to go. We will be working with a multipart message to get this to work.

We have the following function in the controller to help us get the required string to use in the mail.

				
					/**
 * Encodes a string into a base64 string with an email-safe line width
 *
 * @param {string} str String the string to encode
 * @param {string} characterEncoding String the character encoding (i.e. 'ISO-8859-1')
 *
 * @return {string} The encoded string
 */
function encodeBase64ForEmail(str, characterEncoding) {
    var StringUtils = require('dw/util/StringUtils');
    var StringWriter = require('dw/io/StringWriter');
    var strBase64 = StringUtils.encodeBase64(str, characterEncoding);
    var strBase64LB = '';
    var stringWriter = new StringWriter();

    var offset = 0;
    var length = 76;

    while (offset < strBase64.length) {
        var maxOffset = offset + length;

        if (strBase64.length >= maxOffset) {
            stringWriter.write(strBase64, offset, length);
            stringWriter.write('\n');
        } else {
            stringWriter.write(strBase64, offset, length - (maxOffset - strBase64.length));
        }
        offset += length;
    }

    stringWriter.flush();
    strBase64LB = stringWriter.toString();
    stringWriter.close();

    return strBase64LB;
}
				
			

And once we have that base64 encoded string, we can use it in our mail template. And inside that template, we are adding some metadata to give information about the file we are trying to send:

  • Content-Type: Here, we will mark which file type and what name the file has.
  • Content-Description: The description of the file
  • Content-Disposition: Here, we provide more information about the file like its filename, the size of the PDF, …
  • Content-Transfer-Encoding: Here, we tell the mail client that the attachment is encoded using base64
				
					--001a113414f6401b9c04f1451632
Content-Type: application/pdf; name="${key}";
Content-Description: ${key}
Content-Disposition: attachment; filename="${key}"; size=${fileContent.length}; creation-date="${(new Date()).toISOString()}"; modification-date="${(new Date()).toISOString()}"
Content-Transfer-Encoding: base64

${fileContent}
				
			

As you can see, base64 poses no real challenge for Salesforce Commerce Cloud, and we will be able to send attachments quite easily using it.

ISO-8859-1

Within the controller, we have multiple options to work with:

In this example, we will be using the second option.

				
					/**
 * Read a file to a String (encoded in IS0-8859-1)
 *
 * @param {string} filePath - The file path to read
 *
 * @return {string} - The file content
 */
function readPDFFile(filePath) {
    var File = require('dw/io/File');
    var FileReader = require('dw/io/FileReader');

    var testPDF = new File(filePath);
    var pdfReader = new FileReader(testPDF, 'ISO-8859-1');

    var pdfContent = '';
    var line = '';

    /**
     * Warning: You can reach the maximum string length with this code!
     */
    do {
        line = pdfReader.readN(1000);
        pdfContent += line;
    } while (line != null);

    return pdfContent;
}
				
			

It is essential (and you will see this a few times in the example) to use the ISO-8859-1 encoding while reading the file.

Some files might work if you do not use this encoding, but it will not work as expected once you add images and more complex configurations.

Quota Limits

jsStringLength

When working with files (especially in the storefront), you have to keep watch of the Quota Limits – every developer’s friend in SFCC.

In my example, one is especially one to keep an eye on.

A screenshot of the quota limit surround string length in Salesforce Commerce CLoud.

There are multiple ways to work around this limit, but we will not be digging into that in this post.

Template Size

A second limitation to keep in mind is an “undocumented quota limit.” A template response needs to be 10 MB at max, and if it exceeds this size, a server error is thrown (and Salesforce will not send the mail).

So your attachments combined with the text of your mail can not exceed this amount!

The template

Within the template, we will be using a few tricks to get our solution to work.

Content-Type: multipart/alternative

For mails to work with multiple files and a text or HTML option, we must work with the multipart content type and boundaries.

But what does this mean? In layman’s terms, we split our messages into multiple pieces separating them by a predetermined key.

At the top of the file, we tell the system which key it is within the Content-Type (both to Commerce Cloud using the <iscontent> tag and the HTML itself for the e-mail reader).

				
					<iscontent type="multipart/mixed; boundary=001a113414f6401b9c04f1451632" compact="false" charset="ISO-8859-1">--001a113414f6401b9c04f1451632
Content-Type: multipart/alternative; boundary=001a113414f6401b8604f1451630
				
			

Once the key has been set, it can “split up” the mail into different parts. A good example is a separate part for the plain-text and HTML emails.

Note: Do not forget the ‘- -‘ in front of the key as you see them in the examples.

				
					--001a113414f6401b8604f1451630
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable

<isif condition="${!empty(pdict.EmailMessage)}"><isprint value="${pdict.EmailMessage}" /></isif>

--001a113414f6401b8604f1451630
Content-Type: text/html; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable

<isif condition="${!empty(pdict.EmailMessage)}"><isprint value="${pdict.EmailMessage}" /></isif>
<isif condition="${!empty(pdict.EmailTemplate)}"><isinclu
--001a113414f6401b9c04f1451632--
				
			

The same methodology is used for the files. Each attachment gets its own “section” separated by that same key.

				
					<isif condition="${ !empty(pdict.Base64FileMap) }">
<isloop items="${ pdict.Base64FileMap.keySet() }" var="key">
<isset name="fileContent" value="${ pdict.Base64FileMap.get(key) }" scope="page"/>
--001a113414f6401b9c04f1451632
Content-Type: application/pdf; name="${key}";
Content-Description: ${key}
Content-Disposition: attachment; filename="${key}"; size=${fileContent.length}; creation-date="${(new Date()).toISOString()}"; modification-date="${(new Date()).toISOString()}"
Content-Transfer-Encoding: base64

${fileContent}
</isloop>
				
			

Watch out for spaces and new lines

You will have undoubtedly noticed that the code within the template is quite compressed and not “pretty printed.”

Multipart is extremely sensitive to empty lines, tabs, and spaces. So keep that in mind when making modifications to the template.

Not just PDF

We have been working with PDF in this example, but you can also use this solution for other file types! You could send CSV reports, as an example, using this method.

Sources

It wouldn’t be fair to the authors if I did not provide links to the sources I used to get a working example.

Table of Contents

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

More From My Blog