Expanded mail client for easier email operations.

This commit is contained in:
mikestefanello 2022-01-14 15:42:32 -05:00
parent b269e7d264
commit cb43e08183
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,17 +8,29 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
// MailClient provides a client for sending email type (
// This is purposely not completed because there are many different methods and services // MailClient provides a client for sending email
// for sending email, many of which are very different. Choose what works best for you // This is purposely not completed because there are many different methods and services
// and populate the methods below // for sending email, many of which are very different. Choose what works best for you
type MailClient struct { // and populate the methods below
// config stores application configuration MailClient struct {
config *config.Config // config stores application configuration
config *config.Config
// templates stores the template renderer // templates stores the template renderer
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) {
@ -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
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 // skipSend determines if mail sending should be skipped
func (c *MailClient) skipSend() bool { func (m *MailClient) skipSend() bool {
return c.config.App.Environment != config.EnvProduction 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
} }