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.access_key",
|
||||
"core.storage.s3.secret_key",
|
||||
"core.mail.host",
|
||||
"core.mail.username",
|
||||
"core.mail.password",
|
||||
}
|
||||
|
||||
for _, key := range required {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -11,4 +11,5 @@ type CoreConfig struct {
|
|||
Sia SiaConfig `mapstructure:"sia"`
|
||||
Storage StorageConfig `mapstructure:"storage"`
|
||||
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/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
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/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=
|
||||
|
|
|
@ -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