feat: initial mailer module with password reset and email verification templates

This commit is contained in:
Derrick Hammer 2024-02-26 07:30:53 -05:00
parent 39f8152e09
commit 81e540c2ce
Signed by: pcfreak30
GPG Key ID: C997C339BE476FF2
13 changed files with 282 additions and 0 deletions

View File

@ -32,6 +32,9 @@ func initCheckRequiredConfig(logger *zap.Logger, config *config.Manager) error {
"core.storage.s3.region",
"core.storage.s3.access_key",
"core.storage.s3.secret_key",
"core.mail.host",
"core.mail.username",
"core.mail.password",
}
for _, key := range required {

View File

@ -4,6 +4,8 @@ import (
"flag"
"net/http"
"git.lumeweb.com/LumeWeb/portal/mailer"
"git.lumeweb.com/LumeWeb/portal/config"
"git.lumeweb.com/LumeWeb/portal/account"
@ -60,6 +62,7 @@ func main() {
cron.Module,
account.Module,
metadata.Module,
mailer.Module,
protocols.BuildProtocols(cfg),
api.BuildApis(cfg),
fx.Provide(api.NewCasbin),

View File

@ -11,4 +11,5 @@ type CoreConfig struct {
Sia SiaConfig `mapstructure:"sia"`
Storage StorageConfig `mapstructure:"storage"`
Protocols []string `mapstructure:"protocols"`
Mail MailConfig `mapstructure:"mail"`
}

10
config/mail.go Normal file
View File

@ -0,0 +1,10 @@
package config
type MailConfig struct {
Host string
Port int
SSL bool
AuthType string
Username string
Password string
}

1
go.mod
View File

@ -29,6 +29,7 @@ require (
github.com/spf13/viper v1.18.2
github.com/tus/tusd/v2 v2.2.3-0.20240125123123-9080d351525d
github.com/vmihailenco/msgpack/v5 v5.4.1
github.com/wneessen/go-mail v0.4.1
go.etcd.io/bbolt v1.3.8
go.sia.tech/core v0.1.12
go.sia.tech/jape v0.11.1

2
go.sum
View File

@ -370,6 +370,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wneessen/go-mail v0.4.1 h1:m2rSg/sc8FZQCdtrV5M8ymHYOFrC6KJAQAIcgrXvqoo=
github.com/wneessen/go-mail v0.4.1/go.mod h1:zxOlafWCP/r6FEhAaRgH4IC1vg2YXxO0Nar9u0IScZ8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=

69
mailer/email.go Normal file
View File

@ -0,0 +1,69 @@
package mailer
import "github.com/wneessen/go-mail"
type Email struct {
to string
from string
subject string
body string
}
func (e *Email) To() string {
return e.to
}
func (e *Email) SetTo(to string) {
e.to = to
}
func (e *Email) From() string {
return e.from
}
func (e *Email) SetFrom(from string) {
e.from = from
}
func (e *Email) Subject() string {
return e.subject
}
func (e *Email) SetSubject(subject string) {
e.subject = subject
}
func (e *Email) Body() string {
return e.body
}
func (e *Email) SetBody(body string) {
e.body = body
}
func (e *Email) ToMessage() (*mail.Msg, error) {
msg :=
mail.NewMsg()
err := msg.From(e.from)
if err != nil {
return nil, err
}
err = msg.To(e.to)
if err != nil {
return nil, err
}
msg.SetBodyString("text/plain", e.body)
return msg, nil
}
func NewEmail(subject, body string) *Email {
return &Email{
subject: subject,
body: body,
}
}

70
mailer/mailer.go Normal file
View File

@ -0,0 +1,70 @@
package mailer
import (
"git.lumeweb.com/LumeWeb/portal/config"
"github.com/wneessen/go-mail"
"go.uber.org/fx"
"go.uber.org/zap"
)
type TemplateData = map[string]interface{}
var Module = fx.Module("mailer",
fx.Options(
fx.Provide(NewMailer),
fx.Provide(NewTemplateRegistry),
fx.Invoke(func(registry *TemplateRegistry) error {
return registry.loadTemplates()
}),
),
)
type Mailer struct {
config *config.Manager
logger *zap.Logger
client *mail.Client
templateRegistry *TemplateRegistry
}
func (m *Mailer) TemplateSend(template string, subjectVars TemplateData, bodyVars TemplateData, to string) error {
email, err := m.templateRegistry.RenderTemplate(template, subjectVars, bodyVars)
if err != nil {
return err
}
msg, err := email.ToMessage()
if err != nil {
return err
}
return m.client.DialAndSend(msg)
}
func NewMailer(config *config.Manager, templateRegistry *TemplateRegistry, logger *zap.Logger) (*Mailer, error) {
var options []mail.Option
if config.Config().Core.Mail.Port != 0 {
options = append(options, mail.WithPort(config.Config().Core.Mail.Port))
}
if config.Config().Core.Mail.AuthType != "" {
options = append(options, mail.WithSMTPAuth(mail.SMTPAuthType(config.Config().Core.Mail.AuthType)))
}
if config.Config().Core.Mail.SSL {
options = append(options, mail.WithSSLPort(true))
}
options = append(options, mail.WithUsername(config.Config().Core.Mail.Username))
options = append(options, mail.WithPassword(config.Config().Core.Mail.Password))
client, err := mail.NewClient(config.Config().Core.Mail.Host, options...)
if err != nil {
return nil, err
}
m := &Mailer{config: config, logger: logger, client: client, templateRegistry: templateRegistry}
return m, nil
}

95
mailer/templates.go Normal file
View File

@ -0,0 +1,95 @@
package mailer
import (
"embed"
"errors"
"io/fs"
"strings"
"text/template"
)
const EMAIL_FS_PREFIX = "templates"
const TEMPLATE_PASSWORD_RESET = "password_reset.tpl"
const VERIFY_EMAIL = "verify_email.tpl"
type EmailTemplate struct {
Subject *template.Template
Body *template.Template
}
//go:embed templates/*
var templateFS embed.FS
var ErrTemplateNotFound = errors.New("template not found")
type TemplateRegistry struct {
templates map[string]EmailTemplate
}
func NewTemplateRegistry() *TemplateRegistry {
return &TemplateRegistry{
templates: make(map[string]EmailTemplate),
}
}
func (tr *TemplateRegistry) loadTemplates() error {
subjectTemplates, err := fs.Glob(templateFS, EMAIL_FS_PREFIX+"*_subject*")
if err != nil {
return err
}
for _, subjectTemplate := range subjectTemplates {
templateName := strings.TrimSuffix(strings.TrimPrefix(subjectTemplate, EMAIL_FS_PREFIX), "_subject.tpl")
bodyTemplate := strings.TrimSuffix(subjectTemplate, "_subject.tpl") + "_body.tpl"
subjectContent, err := fs.ReadFile(templateFS, templateName+"_subject.tpl")
if err != nil {
return err
}
subjectTmpl, err := template.New(templateName).Parse(string(subjectContent))
if err != nil {
return err
}
bodyContent, err := fs.ReadFile(templateFS, bodyTemplate)
if err != nil {
return err
}
bodyTmpl, err := template.New(templateName).Parse(string(bodyContent))
if err != nil {
return err
}
tr.templates[templateName] = EmailTemplate{
Subject: subjectTmpl,
Body: bodyTmpl,
}
}
return nil
}
func (tr *TemplateRegistry) RenderTemplate(templateName string, subjectVars TemplateData, bodyVars TemplateData) (*Email, error) {
tmpl, ok := tr.templates[templateName]
if !ok {
return nil, ErrTemplateNotFound
}
var subjectBuilder strings.Builder
err := tmpl.Subject.Execute(&subjectBuilder, subjectVars)
if err != nil {
return nil, err
}
var bodyBuilder strings.Builder
err = tmpl.Body.Execute(&bodyBuilder, bodyVars)
if err != nil {
return nil, err
}
return NewEmail(subjectBuilder.String(), bodyBuilder.String()), nil
}

View File

@ -0,0 +1,16 @@
Dear {{if .FirstName}}{{.FirstName}}{{else}}{{.Email}}{{end}},
You are receiving this email because we received a password reset request for your account. If you did not request a password reset, please ignore this email.
To reset your password, please click the link below:
{{.ResetURL}}
This link will expire in {{.ExpireTime}} hours. If you did not request a password reset, no further action is required.
If you're having trouble clicking the reset link, copy and paste the URL below into your web browser:
{{.ResetURL}}
Thank you for using {{.PortalName}}.
Best regards,
The {{.PortalName}} Team

View File

@ -0,0 +1 @@
Reset Your Password for {{.PortalName}}

View File

@ -0,0 +1,10 @@
Dear {{if .FirstName}}{{.FirstName}}{{else}}{{.Email}}{{end}},
Thank you for registering with {{.PortalName}}. To complete your registration and verify your email address, please enter the following verification code in the provided field on our website:
Verification Code: {{.VerificationCode}}
Please note, this code will expire in {{.ExpireTime}} minutes. If you did not initiate this request, please ignore this email or contact our support team for assistance.
Best regards,
The {{.PortalName}} Team

View File

@ -0,0 +1 @@
Verify Your Email for {{.PortalName}}