Email templating made easy with Mustache and Java

ALex Antonica
5 min readJul 16, 2023

--

Overview

Oftentimes we want our emails to be dynamic, to include some data from the database, but we don’t want to create an entire pipeline of exporting this data, just to send an email 😕 moreover we also want to keep and manage the template as code and still be able to customise it so the look and feel of the emails attract the user.

Therefore in this article we’ll cover how to create / populate / style email templates using Java ( + SpringBoot 🍃) , Mustache 〰️ and SendGrid in order to take advantage of oop aspects.

🔧

feel free to jump to the next section if you already have the setup done

I’m gonna fast forward trough the setup as it’s quite easy

  • First we’ll need a SendGrid account and an API key which you can generate from Integration Guide
  • And a basic template -> copy its id, we’re gonna need it

Please note the triple {{{ }}} this will tell SendGrid to render that element as a HTML one

  • Then we’ll add the Mustache and SendGrid mvn dependencies to our spring boot project
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mustache</artifactId>
</dependency>
<dependency>
<artifactId>sendgrid-java</artifactId>
<groupId>com.sendgrid</groupId>
<version>4.8.1</version>
</dependency>
  • In the application.yaml (or .properties) you should add the SendGrid api key
spring:
sendgrid:
api-key: <redacted>

🙌 ⌨️

So far so good.

In order to use mustache template engine, let’s create a file under resources folder called test-email.mustache and populate it with this template.

After you open this file, if you use intellij, it might suggest to install the mustache plugin, which will really help you in autocompleting the template tags and compilation errors.

👋 {{receivingUser}},

<!--if sendingUser is exists in the context-->
{{#sendingUser}}
{{sendingUser}} sends their regards.
{{/sendingUser}}

<!--if sendingUser is doesn't exist in the context-->
{{^sendingUser}}
We hope this email find you well.
{{/sendingUser}}

{{#emailsExist}}
These are the latest emails from your inbox:

<!-- Loop through email -->
{{#emails}}
{{subject}}
{{body}}
{{/emails}}

{{/emailsExist}}

{{^emailsExist}}
These are the no emails in your inbox
{{/emailsExist}}

For the sake of this example, I’ve created a new class called CustomEmail and TestController that has a GET /test endpoint

@Data
@AllArgsConstructor
class CustomEmail {
private String subject;
private String body;
}

@RestController
@RequiredArgsConstructor
@Log4j2
public class TestController {

private final SendGrid sendGrid;

static final String SEND_GRID_TEMPLATE_ID = "<redacted>";
static final String TEST_EMAIL_TEMPLATE_PATH = "test-email.mustache";

@GetMapping("/test")
public String test() {

Compiler compiler = Mustache.compiler();
String contentsOfTestEmailMustacheFile = readPayloadFromClasspath(TEST_EMAIL_TEMPLATE_PATH);

Map<String, Object> context = new HashMap<>();

context.put("receivingUser", "John");
context.put("sendingUser", "ALex");
context.put("emailsExist", true);

List<CustomEmail> emails = new ArrayList<>();
emails.add(new CustomEmail("Test Email Subject 1", "Test Email Body 1"));
emails.add(new CustomEmail("Test Email Subject 2", "Test Email Body 2"));
emails.add(new CustomEmail("Test Email Subject 3", "Test Email Body 3"));

context.put("emails", emails);
String populatedTemplate = compiler.compile(contentsOfTestEmailMustacheFile).execute(context);

sendMail(populatedTemplate);

return populatedTemplate;
}

private void sendMail(String populatedTemplate) {
var mail = new Mail();
var to = new Email("<redacted>");
var from = new Email("<redacted>", "ALex Test Email");

mail.setFrom(from);
mail.setSubject("Test Email Subject");
mail.setTemplateId(SEND_GRID_TEMPLATE_ID);

// template data
Personalization personalization = new Personalization();
personalization.addTo(to);
personalization.addDynamicTemplateData("body", populatedTemplate);
mail.addPersonalization(personalization);

var req = new Request();
try {
req.setMethod(Method.POST);
req.setEndpoint("mail/send");
req.setBody(mail.build());
var res = this.sendGrid.api(req);

if (res != null) {
log.info("Status " + res.getStatusCode());
log.info("Status " + res.getBody());
}
} catch (IOException ex) {
log.error("Exception in sending email", ex);
}
log.info("Sent Sendgrid mail from thread {}", Thread.currentThread().getName());
}
}

Let’s take line by line of test() and see what’s going on.

  1. First we need an instance of mustache’s compiler
  2. Then we need to retrieve the contents of the template we just created
  3. As you might’ve guessed already, the compiler will need a context (/map) in order to figure out what the variables are. So we create and populate just that.
  4. Then the compiler to well…compiles the content of the file, and executes (replacing / applying logic etc) using the context we just created
  5. The result is a string without any variables inside it, and if we had conditions or loops, those were replaced too
  6. Afterwards we send the email and return the result of the executed template

If you run the server and call that endpoint you should get something like

~/Downloads/templates ❯ curl localhost:8888/test
👋 John,

<!--if sendingUser is exists in the context-->
ALex sends their regards.

<!--if sendingUser is doesn't exist in the context-->

These are the latest emails from your inbox:

<!-- Loop through email -->
Test Email Subject 1
Test Email Body 1
Test Email Subject 2
Test Email Body 2
Test Email Subject 3
Test Email Body 3
~/Downloads/templates ❯

We see that the email is quite ugly 😬 so let’s add some style and structure to it ( not that i’m any good with colors), something very cringy just to make a point.

<h1><b>👋{{receivingUser}},</b></h1>
<br>

<!--if sendingUser is exists in the context-->
{{#sendingUser}}
<i style="color: red">{{sendingUser}} sends their regards.</i>
<br>
{{/sendingUser}}

<!--if sendingUser is doesn't exist in the context-->
{{^sendingUser}}

<p style="text-color: red">We hope this email find you well.</p>
{{/sendingUser}}

{{#emailsExist}}
These are the latest emails from your inbox:

<ul>
<!-- Loop through email -->
{{#emails}}
<li>
<b>{{subject}}</b> - {{body}}
</li>
{{/emails}}
</ul>
{{/emailsExist}}

{{^emailsExist}}
<b>These are the no emails in your inbox</b>
{{/emailsExist}}

We simply wrapped the elements in some HTML tags with inline styling, which gave us this “beauty” but i’m gonna let you run wild with your imagination on how powerful this can become. 👍

Conclusion

As you can see it’s not rocket science, but it does come in handy if you want to aggregate some data in the backend then create an email and style some parts of it in code.

P.S. If, after the mustache’s execution, you get element’s like &lt; you need to unescape the result

--

--

ALex Antonica

Software engineer in my 5th year now, passionate and curious about new technologies, practices and ways of thinking. Never stop growing.