Expanded mail client for easier email operations.
This commit is contained in:
parent
b269e7d264
commit
cb43e08183
37
README.md
37
README.md
@ -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
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
145
services/mail.go
145
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user