diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..7512d1a
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,323 @@
+# Contributing
+
+Thanks so much for wanting to help! We really appreciate it.
+
+* Have an idea for a new feature?
+* Want to add a new built-in theme?
+
+Excellent! You've come to the right place.
+
+1. If you find a bug or wish to suggest a new feature, please create an issue first
+2. Make sure your code & comment conventions are in-line with the project's style (execute gometalinter as in [.travis.yml](.travis.yml) file)
+3. Make your commits and PRs as tiny as possible - one feature or bugfix at a time
+4. Write detailed commit messages, in-line with the project's commit naming conventions
+
+## Theming Instructions
+
+This file contains instructions on adding themes to Mailgen:
+
+* [Using a Custom Theme](#using-a-custom-theme)
+* [Creating a Built-In Theme](#creating-a-built-in-theme)
+
+> We use Golang templates under the hood to inject the e-mail body into themes.
+
+### Using a Custom Theme
+
+If you want to supply your own **custom theme** for Hermes to use (but don't want it included with Mailgen):
+
+1. Create a new struct implementing `Theme` interface ([hermes.go](hermes.go)). An real-life example is in [default.go](default.go)
+2. Supply your new theme at hermes creation
+
+```go
+
+type MyCustomTheme struct{}
+
+func (dt *MyCustomTheme) Name() string {
+ return "mycustomthem"
+}
+
+func (dt *MyCustomTheme) HTMLTemplate() string {
+ // Get the template from a file (if you want to be able to change the template live without retstarting your application)
+ // Or write the template by returning pure string here (if you want embbeded template and do not bother with external dependencies)
+ return ""
+}
+
+func (dt *Default) PlainTextTemplate() string {
+ // Get the template from a file (if you want to be able to change the template live without retstarting your application)
+ // Or write the template by returning pure string here (if you want embbeded template and do not bother with external dependencies)
+ return ""
+}
+
+h := hermes.Hermes{
+ Theme: new(MyCustomTheme) // Set your fresh new theme here
+ Product: hermes.Product{
+ Name: "Hermes",
+ Link: "https://example-hermes.com/",
+ },
+}
+
+// ...
+// Continue with the rest as usual, create your email and generate the content.
+// ...
+```
+
+3. That's it.
+
+### Creating a Built-In Theme
+
+If you want to create a new **built-in** Mailgen theme:
+
+1. Fork the repository to your GitHub account and clone it to your computer
+2. Create a new Go file named after your new theme
+3. Copy content of [default.go](default.go) file in new file and make any necessary changes
+6. Scroll down to the [injection snippets](#injection-snippets) and copy and paste each code snippet into the relevant area of your template markup
+7. Test the theme by running the `examples/*.js` scripts (insert your theme name in each script) and observing the template output in `preview.html`
+8. Take a screenshot of your theme portraying each example and place it in `screenshots/{theme}/{example}.png`
+9. Add the theme name, credit, and screenshots to the `README.md` file's [Supported Themes](README.md#supported-themes) section (copy one of the existing themes' markup and modify it accordingly)
+7. Submit a pull request with your changes and we'll let you know if anything's missing!
+
+Thanks again for your contribution!
+
+# Injection Snippets
+
+## Product Branding Injection
+
+The following will inject either the product logo or name into the template.
+
+```html
+
+ <% if (locals.product.logo) { %>
+
+ <% } else { %>
+ <%- product.name %>
+ <% } %>
+
+```
+
+It's a good idea to add the following CSS declaration to set `max-height: 50px` for the logo:
+
+```css
+.email-logo {
+ max-height: 50px;
+}
+```
+
+## Title Injection
+
+The following will inject the e-mail title (Hi John Appleseed,) or a custom title provided by the user:
+
+```html
+<%- title %>
+```
+
+## Intro Injection
+
+The following will inject the intro text (string or array) into the e-mail:
+
+```html
+<% if (locals.intro) { %>
+ <% intro.forEach(function (introItem) { -%>
+
<%- introItem %>
+ <% }) -%>
+<% } %>
+```
+
+## Dictionary Injection
+
+The following will inject a `` of key-value pairs into the e-mail:
+
+```html
+
+<% if (locals.dictionary) { %>
+
+ <% for (item in dictionary) { -%>
+ - <%- item.charAt(0).toUpperCase() + item.slice(1) %>:
+ - <%- dictionary[item] %>
+ <% } -%>
+
+<% } %>
+```
+
+It's a good idea to add this to the top of the template to improve the styling of the dictionary:
+
+```css
+/* Dictionary */
+.dictionary {
+ width: 100%;
+ overflow: hidden;
+ margin: 0 auto;
+ padding: 0;
+}
+.dictionary dt {
+ clear: both;
+ color: #000;
+ font-weight: bold;
+ margin-right: 4px;
+}
+.dictionary dd {
+ margin: 0 0 10px 0;
+}
+```
+
+## Table Injection
+
+The following will inject the table into the e-mail:
+
+```html
+
+<% if (locals.table) { %>
+
+
+ <% for (var column in table.data[0]) {%>
+
+ width="<%= table.columns.customWidth[column] %>"
+ <% } %>
+ <% if(locals.table.columns && locals.table.columns.customAlignment && locals.table.columns.customAlignment[column]) { %>
+ style="text-align:<%= table.columns.customAlignment[column] %>"
+ <% } %>
+ >
+ <%- column.charAt(0).toUpperCase() + column.slice(1) %>
+ |
+ <% } %>
+
+ <% for (var i in table.data) {%>
+
+ <% for (var column in table.data[i]) {%>
+
+ style="text-align:<%= table.columns.customAlignment[column] %>"
+ <% } %>
+ >
+ <%- table.data[i][column] %>
+ |
+ <% } %>
+
+ <% } %>
+
+<% } %>
+```
+
+It's a good idea to add this to the top of the template to improve the styling of the table:
+
+```css
+/* Table */
+.data-wrapper {
+ width: 100%;
+ margin: 0;
+ padding: 35px 0;
+}
+.data-table {
+ width: 100%;
+ margin: 0;
+}
+.data-table th {
+ text-align: left;
+ padding: 0px 5px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid #DEDEDE;
+}
+.data-table th p {
+ margin: 0;
+ font-size: 12px;
+}
+.data-table td {
+ text-align: left;
+ padding: 10px 5px;
+ font-size: 15px;
+ line-height: 18px;
+}
+```
+
+## Action Injection
+
+The following will inject the action link (or button) into the e-mail:
+
+```html
+
+<% if (locals.action) { %>
+ <% action.forEach(function (actionItem) { -%>
+ <%- actionItem.instructions %>
+
+ <%- actionItem.button.text %>
+
+ <% }) -%>
+<% } %>
+```
+
+It's a good idea to add this to the top of the template to specify a fallback color for the action buttons in case it wasn't provided by the user:
+
+```html
+<%
+if (locals.action) {
+ // Make it possible to override action button color (specify fallback color if no color specified)
+ locals.action.forEach(function(actionItem) {
+ if (!actionItem.button.color) {
+ actionItem.button.color = '#48CFAD';
+ }
+ });
+}
+%>
+```
+
+## Outro Injection
+
+The following will inject the outro text (string or array) into the e-mail:
+
+```html
+<% if (locals.outro) { %>
+ <% outro.forEach(function (outroItem) { -%>
+ <%- outroItem %>
+ <% }) -%>
+<% } %>
+```
+
+## Signature Injection
+
+The following will inject the signature phrase (e.g. Yours truly) along with the product name into the e-mail:
+
+```html
+<%- signature %>,
+
+<%- product.name %>
+```
+
+## Copyright Injection
+
+The following will inject the copyright notice into the e-mail:
+
+```html
+<%- product.copyright %>
+```
+
+## Go-To Action Injection
+
+In order to support Gmail's [Go-To Actions](https://developers.google.com/gmail/markup/reference/go-to-action), add the following anywhere within the template:
+
+```html
+
+<% if (locals.goToAction) { %>
+
+<% } %>
+```
+
+## Text Direction Injection
+
+In order to support generating RTL e-mails, inject the `textDirection` variable into the `` tag:
+
+```html
+
+```
+
diff --git a/README.md b/README.md
index 8e686b1..9645cab 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
[![Build Status](https://travis-ci.org/matcornic/hermes.svg?branch=master)](https://travis-ci.org/matcornic/hermes)
[![Go Report Card](https://goreportcard.com/badge/github.com/matcornic/hermes)](https://goreportcard.com/report/github.com/matcornic/hermes)
+[![Go Coverage](http://gocover.io/_badge/github.com/matcornic/hermes/)](http://gocover.io/github.com/matcornic/hermes/)
+[![Godoc](https://godoc.org/github.com/matcornic/hermes?status.svg)](https://godoc.org/github.com/matcornic/hermes)
Hermes is the Go port of the great [mailgen](https://github.com/eladnava/mailgen) engine for Node.js. Check their work, it's awesome !
It's a package that generates clean, responsive HTML e-mails for sending transactional e-mails (welcome e-mail, reset password e-mails, receipt e-mails and so on).
@@ -257,17 +259,7 @@ email := hermes.Email{
## Contributing
-Thanks so much for wanting to help! We really appreciate it.
-
-* Have an idea for a new feature?
-* Want to add a new built-in theme?
-
-Excellent! You've come to the right place.
-
-1. If you find a bug or wish to suggest a new feature, please create an issue first
-2. Make sure your code & comment conventions are in-line with the project's style
-3. Make your commits and PRs as tiny as possible - one feature or bugfix at a time
-4. Write detailed commit messages, in-line with the project's commit naming conventions
+See [CONTRIBUTING.md](CONTRIBUTING.md)
## License
diff --git a/default.go b/default.go
index 2c57670..60c9005 100644
--- a/default.go
+++ b/default.go
@@ -379,7 +379,7 @@ func (dt *Default) HTMLTemplate() string {
// PlainTextTemplate returns a Golang template that will generate an plain text email.
func (dt *Default) PlainTextTemplate() string {
- return `{{.Email.Body.Greeting}} {{.Email.Body.Name}},
+ return `{{if .Email.Body.Title }}{{ .Email.Body.Title }}{{ else }}{{ .Email.Body.Greeting }} {{ .Email.Body.Name }},{{ end }},
{{ with .Email.Body.Intros }}{{ range $line := . }}{{ $line }}{{ end }}{{ end }}
{{ with .Email.Body.Dictionary }}{{ range $entry := . }}
{{ $entry.Key }}: {{ $entry.Value }}{{ end }}{{ end }}
@@ -390,7 +390,7 @@ func (dt *Default) PlainTextTemplate() string {
{{ $line }}{{ end }}{{ end }}
{{.Email.Body.Signature}},
-{{.Hermes.Product.Name}}
+{{.Hermes.Product.Name}} - {{.Hermes.Product.Link}}
{{.Hermes.Product.Copyright}}
`
diff --git a/hermes_test.go b/hermes_test.go
index 30284e7..47bc131 100644
--- a/hermes_test.go
+++ b/hermes_test.go
@@ -5,13 +5,42 @@ import (
"testing"
)
-func TestHermes_ok(t *testing.T) {
+var testedThemes = []Theme{
+ // Insert your new theme here
+ new(Default),
+}
+/////////////////////////////////////////////////////
+// Every theme should display the same information //
+// Find below the tests to check that //
+/////////////////////////////////////////////////////
+
+// Implement this interface when creating a new example checking a common feature of all themes
+type Example interface {
+ // Create the hermes example with data
+ // Represents the "Given" step in Given/When/Then Workflow
+ getExample() (h Hermes, email Email)
+ // Checks the content of the generated HTML email by asserting content presence or not
+ assertHTMLContent(t *testing.T, s string)
+ // Checks the content of the generated Plaintext email by asserting content presence or not
+ assertPlainTextContent(t *testing.T, s string)
+}
+
+// Scenario
+type SimpleExample struct {
+ theme Theme
+}
+
+func (ed *SimpleExample) getExample() (Hermes, Email) {
h := Hermes{
+ Theme: ed.theme,
Product: Product{
- Name: "Hermes",
- Link: "http://hermes.com",
+ Name: "HermesName",
+ Link: "http://hermes-link.com",
+ Copyright: "Copyright © Hermes-Test",
+ Logo: "http://www.duchess-france.org/wp-content/uploads/2016/01/gopher.png",
},
+ TextDirection: TDLeftToRight,
}
email := Email{
@@ -25,6 +54,29 @@ func TestHermes_ok(t *testing.T) {
{"Lastname", "Snow"},
{"Birthday", "01/01/283"},
},
+ Table: Table{
+ Data: [][]Entry{
+ {
+ {Key: "Item", Value: "Golang"},
+ {Key: "Description", Value: "Open source programming language that makes it easy to build simple, reliable, and efficient software"},
+ {Key: "Price", Value: "$10.99"},
+ },
+ {
+ {Key: "Item", Value: "Hermes"},
+ {Key: "Description", Value: "Programmatically create beautiful e-mails using Golang."},
+ {Key: "Price", Value: "$1.99"},
+ },
+ },
+ Columns: Columns{
+ CustomWidth: map[string]string{
+ "Item": "20%",
+ "Price": "15%",
+ },
+ CustomAlignement: map[string]string{
+ "Price": "right",
+ },
+ },
+ },
Actions: []Action{
{
Instructions: "To get started with Hermes, please click here:",
@@ -40,21 +92,206 @@ func TestHermes_ok(t *testing.T) {
},
},
}
+ return h, email
+}
+func (ed *SimpleExample) assertHTMLContent(t *testing.T, r string) {
+
+ // Assert on product
+ assert.Contains(t, r, "HermesName", "Product: Should find the name of the product in email")
+ assert.Contains(t, r, "http://hermes-link.com", "Product: Should find the link of the product in email")
+ assert.Contains(t, r, "Copyright © Hermes-Test", "Product: Should find the Copyright of the product in email")
+ assert.Contains(t, r, "http://www.duchess-france.org/wp-content/uploads/2016/01/gopher.png", "Product: Should find the logo of the product in email")
+
+ // Assert on email body
+ assert.Contains(t, r, "Hi Jon Snow", "Name: Should find the name of the person")
+ assert.Contains(t, r, "Welcome to Hermes", "Intro: Should have intro")
+ assert.Contains(t, r, "Birthday", "Dictionary: Should have dictionary")
+ assert.Contains(t, r, "Open source programming language", "Table: Should have table with first row and first column")
+ assert.Contains(t, r, "Programmatically create beautiful e-mails using Golang", "Table: Should have table with second row and first column")
+ assert.Contains(t, r, "$10.99", "Table: Should have table with first row and second column")
+ assert.Contains(t, r, "$1.99", "Table: Should have table with second row and second column")
+ assert.Contains(t, r, "started with Hermes", "Action: Should have instruction")
+ assert.Contains(t, r, "Confirm your account", "Action: Should have button of action")
+ assert.Contains(t, r, "#22BC66", "Action: Button should have given color")
+ assert.Contains(t, r, "https://hermes-example.com/confirm?token=d9729feb74992cc3482b350163a1a010", "Action: Button should have link")
+ assert.Contains(t, r, "Need help, or have questions", "Outro: Should have outro")
+}
+
+func (ed *SimpleExample) assertPlainTextContent(t *testing.T, r string) {
+
+ // Assert on product
+ assert.Contains(t, r, "HermesName", "Product: Should find the name of the product in email")
+ assert.Contains(t, r, "http://hermes-link.com", "Product: Should find the link of the product in email")
+ assert.Contains(t, r, "Copyright © Hermes-Test", "Product: Should find the Copyright of the product in email")
+ assert.NotContains(t, r, "http://www.duchess-france.org/wp-content/uploads/2016/01/gopher.png", "Product: Should not find any logo in plain text")
+
+ // Assert on email body
+ assert.Contains(t, r, "Hi Jon Snow", "Name: Should find the name of the person")
+ assert.Contains(t, r, "Welcome to Hermes", "Intro: Should have intro")
+ assert.Contains(t, r, "Birthday", "Dictionary: Should have dictionary")
+ assert.NotContains(t, r, "Open source programming language", "Table: Not possible to have table in plain text")
+ assert.NotContains(t, r, "Programmatically create beautiful e-mails using Golang", "Table: Not possible to have table in plain text")
+ assert.NotContains(t, r, "$10.99", "Table: Not possible to have table in plain text")
+ assert.NotContains(t, r, "$1.99", "Table: Not possible to have table in plain text")
+ assert.Contains(t, r, "started with Hermes", "Action: Should have instruction")
+ assert.NotContains(t, r, "Confirm your account", "Action: Should not have button of action in plain text")
+ assert.NotContains(t, r, "#22BC66", "Action: Button should not have color in plain text")
+ assert.Contains(t, r, "https://hermes-example.com/confirm?token=d9729feb74992cc3482b350163a1a010", "Action: Even if button is not possible in plain text, it should have the link")
+ assert.Contains(t, r, "Need help, or have questions", "Outro: Should have outro")
+}
+
+type WithTitleInsteadOfNameExample struct {
+ theme Theme
+}
+
+func (ed *WithTitleInsteadOfNameExample) getExample() (Hermes, Email) {
+ h := Hermes{
+ Theme: ed.theme,
+ Product: Product{
+ Name: "Hermes",
+ Link: "http://hermes.com",
+ },
+ }
+
+ email := Email{
+ Body{
+ Name: "Jon Snow",
+ Title: "A new e-mail",
+ },
+ }
+ return h, email
+}
+
+func (ed *WithTitleInsteadOfNameExample) assertHTMLContent(t *testing.T, r string) {
+ assert.NotContains(t, r, "Hi Jon Snow", "Name: should not find greetings from Jon Snow because title should be used")
+ assert.Contains(t, r, "A new e-mail", "Title should be used instead of name")
+}
+
+func (ed *WithTitleInsteadOfNameExample) assertPlainTextContent(t *testing.T, r string) {
+ assert.NotContains(t, r, "Hi Jon Snow", "Name: should not find greetings from Jon Snow because title should be used")
+ assert.Contains(t, r, "A new e-mail", "Title shoud be used instead of name")
+}
+
+type WithGreetingDifferentThanDefault struct {
+ theme Theme
+}
+
+func (ed *WithGreetingDifferentThanDefault) getExample() (Hermes, Email) {
+ h := Hermes{
+ Theme: ed.theme,
+ Product: Product{
+ Name: "Hermes",
+ Link: "http://hermes.com",
+ },
+ }
+
+ email := Email{
+ Body{
+ Greeting: "Dear",
+ Name: "Jon Snow",
+ },
+ }
+ return h, email
+}
+
+func (ed *WithGreetingDifferentThanDefault) assertHTMLContent(t *testing.T, r string) {
+ assert.NotContains(t, r, "Hi Jon Snow", "Should not find greetings with 'Hi' which is default")
+ assert.Contains(t, r, "Dear Jon Snow", "Should have greeting with Dear")
+}
+
+func (ed *WithGreetingDifferentThanDefault) assertPlainTextContent(t *testing.T, r string) {
+ assert.NotContains(t, r, "Hi Jon Snow", "Should not find greetings with 'Hi' which is default")
+ assert.Contains(t, r, "Dear Jon Snow", "Should have greeting with Dear")
+}
+
+type WithSignatureDifferentThanDefault struct {
+ theme Theme
+}
+
+func (ed *WithSignatureDifferentThanDefault) getExample() (Hermes, Email) {
+ h := Hermes{
+ Theme: ed.theme,
+ Product: Product{
+ Name: "Hermes",
+ Link: "http://hermes.com",
+ },
+ }
+
+ email := Email{
+ Body{
+ Name: "Jon Snow",
+ Signature: "Best regards",
+ },
+ }
+ return h, email
+}
+
+func (ed *WithSignatureDifferentThanDefault) assertHTMLContent(t *testing.T, r string) {
+ assert.NotContains(t, r, "Yours truly", "Should not find signature with 'Yours truly' which is default")
+ assert.Contains(t, r, "Best regards", "Should have greeting with Dear")
+}
+
+func (ed *WithSignatureDifferentThanDefault) assertPlainTextContent(t *testing.T, r string) {
+ assert.NotContains(t, r, "Yours truly", "Should not find signature with 'Yours truly' which is default")
+ assert.Contains(t, r, "Best regards", "Should have greeting with Dear")
+}
+
+// Test all the themes for the features
+
+func TestThemeSimple(t *testing.T) {
+ for _, theme := range testedThemes {
+ checkExample(t, &SimpleExample{theme})
+ }
+}
+
+func TestThemeWithTitleInsteadOfName(t *testing.T) {
+ for _, theme := range testedThemes {
+ checkExample(t, &WithTitleInsteadOfNameExample{theme})
+ }
+}
+
+func TestThemeWithGreetingDifferentThanDefault(t *testing.T) {
+ for _, theme := range testedThemes {
+ checkExample(t, &WithGreetingDifferentThanDefault{theme})
+ }
+}
+
+func TestThemeWithGreetingDiffrentThanDefault(t *testing.T) {
+ for _, theme := range testedThemes {
+ checkExample(t, &WithSignatureDifferentThanDefault{theme})
+ }
+}
+
+func checkExample(t *testing.T, ex Example) {
+ // Given an example
+ h, email := ex.getExample()
+
+ // When generating HTML template
r, err := h.GenerateHTML(email)
t.Log(r)
assert.Nil(t, err)
assert.NotEmpty(t, r)
+ // Then asserting HTML is OK
+ ex.assertHTMLContent(t, r)
+
+ // When generating plain text template
r, err = h.GeneratePlainText(email)
t.Log(r)
assert.Nil(t, err)
assert.NotEmpty(t, r)
- assert.Equal(t, h.Theme.Name(), "default")
+ // Then asserting plain text is OK
+ ex.assertPlainTextContent(t, r)
}
-func TestHermes_defaultTextDirection(t *testing.T) {
+////////////////////////////////////////////
+// Tests on default values for all themes //
+// It does not check email content //
+////////////////////////////////////////////
+
+func TestHermes_TextDirectionAsDefault(t *testing.T) {
h := Hermes{
Product: Product{
Name: "Hermes",
@@ -88,4 +325,29 @@ func TestHermes_defaultTextDirection(t *testing.T) {
_, err := h.GenerateHTML(email)
assert.Nil(t, err)
assert.Equal(t, h.TextDirection, TDLeftToRight)
+ assert.Equal(t, h.Theme.Name(), "default")
+}
+
+func TestHermes_Default(t *testing.T) {
+ h := Hermes{}
+ setDefaultHermesValues(&h)
+ email := Email{}
+ setDefaultEmailValues(&email)
+
+ assert.Equal(t, h.TextDirection, TDLeftToRight)
+ assert.Equal(t, h.Theme, new(Default))
+ assert.Equal(t, h.Product.Name, "Hermes")
+ assert.Equal(t, h.Product.Copyright, "Copyright © 2017 Hermes. All rights reserved.")
+
+ assert.Empty(t, email.Body.Actions)
+ assert.Empty(t, email.Body.Dictionary)
+ assert.Empty(t, email.Body.Intros)
+ assert.Empty(t, email.Body.Outros)
+ assert.Empty(t, email.Body.Table.Data)
+ assert.Empty(t, email.Body.Table.Columns.CustomWidth)
+ assert.Empty(t, email.Body.Table.Columns.CustomAlignement)
+
+ assert.Equal(t, email.Body.Greeting, "Hi")
+ assert.Equal(t, email.Body.Signature, "Yours truly")
+ assert.Empty(t, email.Body.Title)
}