diff --git a/README.md b/README.md
index 1fcbf03..a5dc6e5 100644
--- a/README.md
+++ b/README.md
@@ -662,7 +662,7 @@ if form := ctx.Get(context.FormKey); form != nil {
```
And finally, your template:
-```
+```html
```
@@ -675,7 +675,7 @@ While [validator](https://github.com/go-playground/validator) is a great package
To provide the inline validation in your template, there are two things that need to be done.
First, include a status class on the element so it will highlight green or red based on the validation:
-```
+```html
```
@@ -964,7 +964,7 @@ The cache max-life is controlled by the configuration at `Config.Cache.Expiratio
While it's ideal to use cache control headers on your static files so browsers cache the files, you need a way to bust the cache in case the files are changed. In order to do this, a function is provided in the [funcmap](#funcmap) to generate a static file URL for a given file that appends a cache-buster query. This query string is randomly generated and persisted until the application restarts.
For example, to render a file located in `static/picture.png`, you would use:
-```go
+```html
```
@@ -979,7 +979,36 @@ Where `9fhe73kaf3` is the randomly-generated cache-buster.
An email client was added as a _Service_ to the `Container` but it is just a skeleton without any actual email-sending functionality. The reason is because there are a lot of ways to send email and most prefer using a SaaS solution for that. That makes it difficult to provide a generic solution that will work for most applications.
-Two starter methods were added to the `MailClient`, one to send an email via plain-text and one to send via a template by leveraging the [template renderer](#template-renderer). The standard library can be used if you wish to send email via SMTP and most SaaS providers have a Go package that can be used if you choose to go that direction.
+The structure in the client (`MailClient`) makes composing emails very easy and you have the option to construct the body using either a simple string or with a template by leveraging the [template renderer](#template-renderer). The standard library can be used if you wish to send email via SMTP and most SaaS providers have a Go package that can be used if you choose to go that direction. **You must** finish the implementation of `mail.Send`.
+
+The _from_ address will default to the configuration value at `Config.Mail.FromAddress`. This can be overridden per-email by calling `From()` on the email and passing in the desired address.
+
+See below for examples on how to use the client to compose emails.
+
+**Sending with a string body**:
+
+```go
+err = c.Mail.
+ Compose().
+ To("hello@example.com").
+ Subject("Welcome!").
+ Body("Thank you for registering.").
+ Send(ctx)
+```
+
+**Sending with a template body**:
+
+```go
+err = c.Mail.
+ Compose().
+ To("hello@example.com").
+ Subject("Welcome!").
+ Template("welcome").
+ TemplateData(templateData).
+ Send(ctx)
+```
+
+This will use the template located at `templates/emails/welcome.gohtml` and pass `templateData` to it.
## HTTPS
diff --git a/routes/contact.go b/routes/contact.go
index 4a08e65..0ae07d9 100644
--- a/routes/contact.go
+++ b/routes/contact.go
@@ -1,6 +1,8 @@
package routes
import (
+ "fmt"
+
"github.com/mikestefanello/pagoda/context"
"github.com/mikestefanello/pagoda/controller"
@@ -47,7 +49,14 @@ func (c *Contact) Post(ctx echo.Context) error {
}
if !form.Submission.HasErrors() {
- if err := c.Container.Mail.Send(ctx, form.Email, "Hello!"); err != nil {
+ err := c.Container.Mail.
+ Compose().
+ To(form.Email).
+ Subject("Contact form submitted").
+ Body(fmt.Sprintf("The message is: %s", form.Message)).
+ Send(ctx)
+
+ if err != nil {
return c.Fail(ctx, err, "unable to send email")
}
}
diff --git a/routes/forgot_password.go b/routes/forgot_password.go
index d1f860d..604c99b 100644
--- a/routes/forgot_password.go
+++ b/routes/forgot_password.go
@@ -84,10 +84,14 @@ func (c *ForgotPassword) Post(ctx echo.Context) error {
ctx.Logger().Infof("generated password reset token for user %d", u.ID)
// Email the user
- err = c.Container.Mail.Send(ctx, u.Email, fmt.Sprintf(
- "Go here to reset your password: %s",
- ctx.Echo().Reverse("reset_password", u.ID, token),
- ))
+ url := ctx.Echo().Reverse("reset_password", u.ID, token)
+ err = c.Container.Mail.
+ Compose().
+ To(u.Email).
+ Subject("Reset your password").
+ Body(fmt.Sprintf("Go here to reset your password: %s", url)).
+ Send(ctx)
+
if err != nil {
return c.Fail(ctx, err, "error sending password reset email")
}
diff --git a/routes/register.go b/routes/register.go
index 619c866..dde596d 100644
--- a/routes/register.go
+++ b/routes/register.go
@@ -105,12 +105,16 @@ func (c *Register) sendVerificationEmail(ctx echo.Context, usr *ent.User) {
}
// Send the email
- err = c.Container.Mail.Send(ctx, usr.Email, fmt.Sprintf(
- "Confirm your email address: %s",
- ctx.Echo().Reverse("verify_email", token),
- ))
+ url := ctx.Echo().Reverse("verify_email", token)
+ err = c.Container.Mail.
+ Compose().
+ To(usr.Email).
+ Subject("Confirm your email address").
+ Body(fmt.Sprintf("Click here to confirm your email address: %s", url)).
+ Send(ctx)
+
if err != nil {
- ctx.Logger().Errorf("unable to send email verification token: %v", err)
+ ctx.Logger().Errorf("unable to send email verification link: %v", err)
return
}
diff --git a/services/mail.go b/services/mail.go
index 81659a9..7f59b78 100644
--- a/services/mail.go
+++ b/services/mail.go
@@ -8,17 +8,29 @@ import (
"github.com/labstack/echo/v4"
)
-// MailClient provides a client for sending email
-// This is purposely not completed because there are many different methods and services
-// for sending email, many of which are very different. Choose what works best for you
-// and populate the methods below
-type MailClient struct {
- // config stores application configuration
- config *config.Config
+type (
+ // MailClient provides a client for sending email
+ // This is purposely not completed because there are many different methods and services
+ // for sending email, many of which are very different. Choose what works best for you
+ // and populate the methods below
+ MailClient struct {
+ // config stores application configuration
+ config *config.Config
- // templates stores the template renderer
- templates *TemplateRenderer
-}
+ // templates stores the template renderer
+ templates *TemplateRenderer
+ }
+
+ mail struct {
+ client *MailClient
+ from string
+ to string
+ subject string
+ body string
+ template string
+ templateData interface{}
+ }
+)
// NewMailClient creates a new MailClient
func NewMailClient(cfg *config.Config, templates *TemplateRenderer) (*MailClient, error) {
@@ -28,43 +40,86 @@ func NewMailClient(cfg *config.Config, templates *TemplateRenderer) (*MailClient
}, nil
}
-// Send sends an email to a given email address with a given body
-func (c *MailClient) Send(ctx echo.Context, to, body string) error {
- if c.skipSend() {
- ctx.Logger().Debugf("skipping email sent to: %s", to)
+// Compose creates a new email
+func (m *MailClient) Compose() *mail {
+ return &mail{
+ client: m,
+ from: m.config.Mail.FromAddress,
}
-
- // TODO: Finish based on your mail sender of choice
- return nil
-}
-
-// SendTemplate sends an email to a given email address using a template and data which is passed to the template
-// The template name should only include the filename without the extension or directory.
-// The funcmap will be automatically added to the template and the data will be passed in.
-func (c *MailClient) SendTemplate(ctx echo.Context, to, template string, data interface{}) error {
- if c.skipSend() {
- ctx.Logger().Debugf("skipping template email sent to: %s", to)
- }
-
- // Parse and execute template
- // Uncomment the first variable when ready to use
- _, err := c.templates.ParseAndExecute(
- "mail",
- template,
- template,
- []string{fmt.Sprintf("emails/%s", template)},
- []string{},
- data,
- )
- if err != nil {
- return err
- }
-
- // TODO: Finish based on your mail sender of choice
- return nil
}
// skipSend determines if mail sending should be skipped
-func (c *MailClient) skipSend() bool {
- return c.config.App.Environment != config.EnvProduction
+func (m *MailClient) skipSend() bool {
+ return m.config.App.Environment != config.EnvProduction
+}
+
+// From sets the email from address
+func (m *mail) From(from string) *mail {
+ m.from = from
+ return m
+}
+
+// To sets the email address this email will be sent to
+func (m *mail) To(to string) *mail {
+ m.to = to
+ return m
+}
+
+// Subject sets the subject line of the email
+func (m *mail) Subject(subject string) *mail {
+ m.subject = subject
+ return m
+}
+
+// Body sets the body of the email
+// This is not required and will be ignored if a template via Template()
+func (m *mail) Body(body string) *mail {
+ m.body = body
+ return m
+}
+
+// Template sets the template to be used to produce the body of the email
+// The template name should only include the filename without the extension or directory.
+// The template must reside within the emails sub-directory.
+// The funcmap will be automatically added to the template.
+// Use TemplateData() to supply the data that will be passed in to the template.
+func (m *mail) Template(template string) *mail {
+ m.template = template
+ return m
+}
+
+// TemplateData sets the data that will be passed to the template specified when calling Template()
+func (m *mail) TemplateData(data interface{}) *mail {
+ m.templateData = data
+ return m
+}
+
+// Send attempts to send the email
+func (m *mail) Send(ctx echo.Context) error {
+ // Check if a template was supplied
+ if m.template != "" {
+ // Parse and execute template
+ buf, err := m.client.templates.ParseAndExecute(
+ "mail",
+ m.template,
+ m.template,
+ []string{fmt.Sprintf("emails/%s", m.template)},
+ []string{},
+ m.templateData,
+ )
+ if err != nil {
+ return err
+ }
+
+ m.body = buf.String()
+ }
+
+ // Check if mail sending should be skipped
+ if m.client.skipSend() {
+ ctx.Logger().Debugf("skipping email sent to: %s", m.to)
+ return nil
+ }
+
+ // TODO: Finish based on your mail sender of choice!
+ return nil
}