Skip to content

Sending Emails

BrunoRosendo edited this page Jul 27, 2023 · 2 revisions

Sending email

Sending email is really simple with the use of the EmailService. This class has a single method, send, which receives a single argument, an instance of EmailBuilder.

Contents

EmailBuilder subclasses

BaseEmailBuilder

All EmailBuilder implementations are a subclass of BaseEmailBuilder, which has the ability to set the sender and receiver(s) of an email, through the from, to, cc, and bcc methods.

SimpleEmailBuilder

The SimpleEmailBuilder lets you send emails easily without requiring a template to be setup. Although it's less work upfront and more flexible in some regards, it's error prone and not a recommended way to send emails.

Nonetheless, html and text contents are supported, as are attachments and inline files.

Here's an example, it sends an email with different html and plain text versions, an inline image and a pdf attachment:

emailService.send(
    SimpleEmailService()
        .to("[email protected]")
        .subject("Example email")
        .text("Example text content")
        .html("<em>Example html content</em>\n<img src=\"cid:image\">")
        .inline("image", "classpath:image.png")
        .attachment("example.pdf", "classpath:document.pdf")
)

TemplateEmailBuilder

The TemplateEmailBuilder (or any of its subclasses), generates emails following a given template from data given to it through the data method. This is much easier than using SimpleEmailBuilder, but requires more effort upfront to create the template.

Here's an example:

emailService.send(
    ExampleTemplateEmailBuilder() // A subclass of TemplateEmailBuilder
        .to("[email protected]")
        .data(ExampleData("test"))
)

Creating a template

Let's create a simple email with a user's list of tasks as an example.

TemplateEmailBuilder subclass

Firstly we must create a TemplateEmailBuilder subclass. This class will usually live in the pt.up.fe.ni.website.backend.email package, but it could be anywhere.

class TaskListEmailTemplateBuilder : TemplateEmailBuilder()

This is a good place to start but there are two things missing, the path to our mustache template and the type of data we're expecting.

Let's use Any as the type for now and templates/email/task-list.mustache as the template path.

class TaskListEmailTemplateBuilder : TemplateEmailBuilder<Any>("task-list")

Note: We pass "task-list" as the argument because, by default, the builder will add templates/email as a prefix and .mustache as a suffix.

Mustache template

Let's now create the actual template itself. We use mustache as the templating language and markdown as the markup language. I recommend looking at their documentation (mustache, commonmark) if you're not familiar with these languages, but you can get the gist of it from this document alone.

Here's what a simple email template could look like:

---
subject: Task list: {{title}}
---

# {{title}}

{{#tasks}}
- {{.}}
{{/tasks}}

Let's break this example down:

  • The first part (between the ---s) is called YAML frontmatter, it's a way to add metadata that will not be included in the final markup. Here we set the email subject line.
  • The line starting with # is parsed as a heading and will be converted to an h1 html element.
  • The line starting with - is parsed as a list item and will be converted to an html list item inside an unordered list.
  • Words inside double braces (or mustaches) are replaced with their value in the provided data.
  • The double braces followed by # or / are special notation for a section, these will be explained later but you can think of them as a for loop for now.

Template data type safety

You might find a problem with our example, the template has no guarantees that the data it receives actually includes a title and tasks! This can be fixed by providing a custom type for our template data:

data class TaskList(
    val title: String,
    val tasks: List<String>
)

class TaskListEmailTemplateBuilder : TemplateEmailBuilder<TaskList>("task-list")

Now our template has a guarantee that the data it receives conforms to the shape it's expecting.

Advanced features

More mustache features

We touched on sections a bit, but let's look at them more in depth:

{{#context}}
  ...
{{/context}}

This a simple section, it has a few different effects depending on the type of context:

  • If it's a false-ish value (false, null, empty list), the section will not be rendered;
  • If it's a list, the section will behave as a for-loop, rendering once for each element in the list, and changing its context to that element;
  • Otherwise, the section will render once with its context set to that value.

A section's context (or the whole data outside a section) can be accessed with {{.}}.

We also have access to inverted sections:

{{^context}}
  ...
{{/context}}

These behave like you would expect, only rendering for false-ish values.

In addition to sections, we have partials, these simply include another template inside our own:

{{> path/to/another/template}}

Markdown extensions

Commonmark, the markdown library we use, has support for extensions which add more features. The only one we're using right now adds support for the YAML frontmatter, but feel free to add more if the need arises.

For our task list example, the tasklist extension might (unsurprisingly) be useful:

---
subject: Task list: {{title}}
---

# {{title}}

{{#tasks}}
- [{{#done}}x{{/done}}{{^done}} {{/done}}] {{name}}
{{/tasks}}

These extensions can be added at the start of the TemplateEmailBuilder class, where the commonmark parser is created:

abstract class TemplateEmailBuilder<T>(
    private val template: String
) : BaseEmailBuilder() {
    private companion object {
        val commonmarkParser: Parser = Parser.builder().extensions(
            listOf(
                YamlFrontMatterExtension.create(),
                TaskListItemsExtension.create(), // Add more extensions here
            )
        ).build()
        val commonmarkHtmlRenderer: HtmlRenderer = HtmlRenderer.builder().build()
        val commonmarkTextRenderer: TextContentRenderer = TextContentRenderer.builder().build()
    }

    ...
}

Images and other inline files

Like normal markdown, images can be included with the ![<Alt>](<URL>) syntax, but they need to be hosted somewhere, or, more conveniently, sent with our email. This can be achieved with the use of inline files:

---
subject: {{title}}
inline:
    - image :: classpath:image.png
---

# {{title}}

![Image description](cid:image)

The inline option in the template frontmatter should be a list of files to be sent with the email, in the format <name> :: <path>, these can then be accessed inside the email body with the url cid:<name>. If the name is omitted, the path will be used.

Attachments

Attachments work exactly like inline files, the differences being they can't be accessed inside the email body, and they appear as downloadable to the receiver.

---
subject: {{title}}
attachments:
    - image.png :: classpath:image-with-different-name.png
---

# {{title}}

Styles

Styles can be included by path in the YAML frontmatter. By default, they'll be added inside a <style> tag in the head of the email html content.

You can also disable the default stylesheet.

---
subject: {{title}}
styles:
    - classpath:style.css
no_default_style: true
---

# {{title}}

Note: make sure any CSS features you use are supported by the email clients.

HTML layout

The layout used for the email HTML content can be changed in the YAML frontmatter:

---
subject: {{title}}
layout: my-layout.html
---

# {{title}}

This will make the builder look for a template in templates/email/my-layout.html.mustache. This template will receive the rendered markdown as a string in the content variable, the email subject line in the subject variable, and the CSS styles as a list of strings (the css files contents) in the styles variable.

Note: make sure any HTML features you use are supported by the email clients.