-
Notifications
You must be signed in to change notification settings - Fork 0
Sending Emails
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
.
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.
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("example@example.com")
.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")
)
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("example@example.com")
.data(ExampleData("test"))
)
Let's create a simple email with a user's list of tasks as an example.
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.
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 anh1
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.
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.
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}}
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()
}
...
}
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 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 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.
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.
Getting Started
Architecture Details
Implementation Details
Testing
Documentation
Deployment