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:
```
```html
<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.
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}}">
```
@ -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
<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.
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

View File

@ -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")
}
}

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)
// 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")
}

View File

@ -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
}

View File

@ -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
}