feat: initial mailer module with password reset and email verification templates
This commit is contained in:
parent
39f8152e09
commit
81e540c2ce
|
@ -32,6 +32,9 @@ func initCheckRequiredConfig(logger *zap.Logger, config *config.Manager) error {
|
||||||
"core.storage.s3.region",
|
"core.storage.s3.region",
|
||||||
"core.storage.s3.access_key",
|
"core.storage.s3.access_key",
|
||||||
"core.storage.s3.secret_key",
|
"core.storage.s3.secret_key",
|
||||||
|
"core.mail.host",
|
||||||
|
"core.mail.username",
|
||||||
|
"core.mail.password",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, key := range required {
|
for _, key := range required {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"flag"
|
"flag"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.lumeweb.com/LumeWeb/portal/mailer"
|
||||||
|
|
||||||
"git.lumeweb.com/LumeWeb/portal/config"
|
"git.lumeweb.com/LumeWeb/portal/config"
|
||||||
|
|
||||||
"git.lumeweb.com/LumeWeb/portal/account"
|
"git.lumeweb.com/LumeWeb/portal/account"
|
||||||
|
@ -60,6 +62,7 @@ func main() {
|
||||||
cron.Module,
|
cron.Module,
|
||||||
account.Module,
|
account.Module,
|
||||||
metadata.Module,
|
metadata.Module,
|
||||||
|
mailer.Module,
|
||||||
protocols.BuildProtocols(cfg),
|
protocols.BuildProtocols(cfg),
|
||||||
api.BuildApis(cfg),
|
api.BuildApis(cfg),
|
||||||
fx.Provide(api.NewCasbin),
|
fx.Provide(api.NewCasbin),
|
||||||
|
|
|
@ -11,4 +11,5 @@ type CoreConfig struct {
|
||||||
Sia SiaConfig `mapstructure:"sia"`
|
Sia SiaConfig `mapstructure:"sia"`
|
||||||
Storage StorageConfig `mapstructure:"storage"`
|
Storage StorageConfig `mapstructure:"storage"`
|
||||||
Protocols []string `mapstructure:"protocols"`
|
Protocols []string `mapstructure:"protocols"`
|
||||||
|
Mail MailConfig `mapstructure:"mail"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
type MailConfig struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
SSL bool
|
||||||
|
AuthType string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
1
go.mod
1
go.mod
|
@ -29,6 +29,7 @@ require (
|
||||||
github.com/spf13/viper v1.18.2
|
github.com/spf13/viper v1.18.2
|
||||||
github.com/tus/tusd/v2 v2.2.3-0.20240125123123-9080d351525d
|
github.com/tus/tusd/v2 v2.2.3-0.20240125123123-9080d351525d
|
||||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||||
|
github.com/wneessen/go-mail v0.4.1
|
||||||
go.etcd.io/bbolt v1.3.8
|
go.etcd.io/bbolt v1.3.8
|
||||||
go.sia.tech/core v0.1.12
|
go.sia.tech/core v0.1.12
|
||||||
go.sia.tech/jape v0.11.1
|
go.sia.tech/jape v0.11.1
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -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/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 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
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/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/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=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
Reset Your Password for {{.PortalName}}
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
Verify Your Email for {{.PortalName}}
|
Loading…
Reference in New Issue