Expanded mail client for easier email operations.

This commit is contained in:
mikestefanello 2022-01-14 15:42:32 -05:00
parent 3f053711ba
commit e8d73421aa
5 changed files with 160 additions and 59 deletions

View file

@ -662,7 +662,7 @@ if form := ctx.Get(context.FormKey); form != nil {
``` ```
And finally, your template: And finally, your template:
``` ```html
<input id="email" name="email" type="email" class="input" value="{{.Form.Email}}"> <input id="email" name="email" type="email" class="input" value="{{.Form.Email}}">
``` ```
@ -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. 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: First, include a status class on the element so it will highlight green or red based on the validation:
``` ```html
<input id="email" name="email" type="email" class="input {{.Form.Submission.GetFieldStatusClass "Email"}}" value="{{.Form.Email}}"> <input id="email" name="email" type="email" class="input {{.Form.Submission.GetFieldStatusClass "Email"}}" value="{{.Form.Email}}">
``` ```
@ -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. 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: For example, to render a file located in `static/picture.png`, you would use:
```go ```html
<img src="{{File "picture.png"}}"/> <img src="{{File "picture.png"}}"/>
``` ```
@ -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. 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 ## HTTPS

View file

@ -1,6 +1,8 @@
package routes package routes
import ( import (
"fmt"
"github.com/mikestefanello/pagoda/context" "github.com/mikestefanello/pagoda/context"
"github.com/mikestefanello/pagoda/controller" "github.com/mikestefanello/pagoda/controller"
@ -47,7 +49,14 @@ func (c *Contact) Post(ctx echo.Context) error {
} }
if !form.Submission.HasErrors() { 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") return c.Fail(ctx, err, "unable to send email")
} }
} }

View file

@ -84,10 +84,14 @@ func (c *ForgotPassword) Post(ctx echo.Context) error {
ctx.Logger().Infof("generated password reset token for user %d", u.ID) ctx.Logger().Infof("generated password reset token for user %d", u.ID)
// Email the user // Email the user
err = c.Container.Mail.Send(ctx, u.Email, fmt.Sprintf( url := ctx.Echo().Reverse("reset_password", u.ID, token)
"Go here to reset your password: %s", err = c.Container.Mail.
ctx.Echo().Reverse("reset_password", u.ID, token), Compose().
)) To(u.Email).
Subject("Reset your password").
Body(fmt.Sprintf("Go here to reset your password: %s", url)).
Send(ctx)
if err != nil { if err != nil {
return c.Fail(ctx, err, "error sending password reset email") return c.Fail(ctx, err, "error sending password reset email")
} }

View file

@ -105,12 +105,16 @@ func (c *Register) sendVerificationEmail(ctx echo.Context, usr *ent.User) {
} }
// Send the email // Send the email
err = c.Container.Mail.Send(ctx, usr.Email, fmt.Sprintf( url := ctx.Echo().Reverse("verify_email", token)
"Confirm your email address: %s", err = c.Container.Mail.
ctx.Echo().Reverse("verify_email", token), 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 { 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 return
} }

View file

@ -8,11 +8,12 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type (
// MailClient provides a client for sending email // MailClient provides a client for sending email
// This is purposely not completed because there are many different methods and services // 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 // for sending email, many of which are very different. Choose what works best for you
// and populate the methods below // and populate the methods below
type MailClient struct { MailClient struct {
// config stores application configuration // config stores application configuration
config *config.Config config *config.Config
@ -20,6 +21,17 @@ type MailClient struct {
templates *TemplateRenderer templates *TemplateRenderer
} }
mail struct {
client *MailClient
from string
to string
subject string
body string
template string
templateData interface{}
}
)
// NewMailClient creates a new MailClient // NewMailClient creates a new MailClient
func NewMailClient(cfg *config.Config, templates *TemplateRenderer) (*MailClient, error) { func NewMailClient(cfg *config.Config, templates *TemplateRenderer) (*MailClient, error) {
return &MailClient{ return &MailClient{
@ -28,43 +40,86 @@ func NewMailClient(cfg *config.Config, templates *TemplateRenderer) (*MailClient
}, nil }, nil
} }
// Send sends an email to a given email address with a given body // Compose creates a new email
func (c *MailClient) Send(ctx echo.Context, to, body string) error { func (m *MailClient) Compose() *mail {
if c.skipSend() { return &mail{
ctx.Logger().Debugf("skipping email sent to: %s", to) client: m,
from: m.config.Mail.FromAddress,
}
} }
// TODO: Finish based on your mail sender of choice // skipSend determines if mail sending should be skipped
return nil func (m *MailClient) skipSend() bool {
return m.config.App.Environment != config.EnvProduction
} }
// SendTemplate sends an email to a given email address using a template and data which is passed to the template // 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 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. // The template must reside within the emails sub-directory.
func (c *MailClient) SendTemplate(ctx echo.Context, to, template string, data interface{}) error { // The funcmap will be automatically added to the template.
if c.skipSend() { // Use TemplateData() to supply the data that will be passed in to the template.
ctx.Logger().Debugf("skipping template email sent to: %s", to) 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 // Parse and execute template
// Uncomment the first variable when ready to use buf, err := m.client.templates.ParseAndExecute(
_, err := c.templates.ParseAndExecute(
"mail", "mail",
template, m.template,
template, m.template,
[]string{fmt.Sprintf("emails/%s", template)}, []string{fmt.Sprintf("emails/%s", m.template)},
[]string{}, []string{},
data, m.templateData,
) )
if err != nil { if err != nil {
return err return err
} }
// TODO: Finish based on your mail sender of choice 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 return nil
} }
// skipSend determines if mail sending should be skipped // TODO: Finish based on your mail sender of choice!
func (c *MailClient) skipSend() bool { return nil
return c.config.App.Environment != config.EnvProduction
} }