mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-14 05:39:05 +02:00
Remove the AppConfigMiddleware that stored an app-config resolver in the request context and the FromCtx helper that read it back downstream. Handlers now load the app config from AppConfigService and pass it as an explicit argument to the service methods that need it, which then forward it further down the call chain. The webauthn and usersignup modules gain an AppConfigResolver dependency so their handlers can load the config the same way. The email SendEmail and LDAP SyncAll helpers drop their context-based variants in favor of the explicit-config versions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ6SoJpmnLggZqactXxrak
176 lines
4.8 KiB
Go
176 lines
4.8 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
htemplate "html/template"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
ttemplate "text/template"
|
|
|
|
"github.com/italypaleale/go-kit/emailer"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
|
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
|
|
)
|
|
|
|
type EmailService struct {
|
|
db *gorm.DB
|
|
htmlTemplates map[string]*htemplate.Template
|
|
textTemplates map[string]*ttemplate.Template
|
|
}
|
|
|
|
func NewEmailService(db *gorm.DB) (*EmailService, error) {
|
|
htmlTemplates, err := email.PrepareHTMLTemplates(emailTemplatesPaths)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("prepare html templates: %w", err)
|
|
}
|
|
|
|
textTemplates, err := email.PrepareTextTemplates(emailTemplatesPaths)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("prepare html templates: %w", err)
|
|
}
|
|
|
|
return &EmailService{
|
|
db: db,
|
|
htmlTemplates: htmlTemplates,
|
|
textTemplates: textTemplates,
|
|
}, nil
|
|
}
|
|
|
|
func (srv *EmailService) SendTestEmail(ctx context.Context, dbConfig *appconfig.AppConfigModel, recipientUserId string) error {
|
|
var user model.User
|
|
err := srv.db.
|
|
WithContext(ctx).
|
|
First(&user, "id = ?", recipientUserId).
|
|
Error
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if user.Email == nil {
|
|
return &common.UserEmailNotSetError{}
|
|
}
|
|
|
|
return SendEmail(ctx, srv, dbConfig,
|
|
email.Address{
|
|
Email: *user.Email,
|
|
Name: user.FullName(),
|
|
}, TestTemplate, nil)
|
|
}
|
|
|
|
// SendEmail sends an email using the provided application configuration
|
|
func SendEmail[V any](ctx context.Context, srv *EmailService, dbConfig *appconfig.AppConfigModel, toEmail email.Address, template email.Template[V], tData *V) error {
|
|
|
|
data := &email.TemplateData[V]{
|
|
AppName: dbConfig.AppName.String(),
|
|
LogoURL: common.EnvConfig.AppURL + "/api/application-images/email",
|
|
Data: tData,
|
|
}
|
|
|
|
// Render the text and HTML bodies
|
|
text, html, err := renderBody(srv, template, data)
|
|
if err != nil {
|
|
return fmt.Errorf("prepare email body for '%s': %w", template.Path, err)
|
|
}
|
|
|
|
// Configure the emailer from the current app config
|
|
e, err := srv.getEmailer(ctx, dbConfig)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to configure emailer: %w", err)
|
|
}
|
|
|
|
// Send the email
|
|
err = e.SendEmail(ctx, emailer.EmailAddress{
|
|
Name: toEmail.Name,
|
|
Address: toEmail.Email,
|
|
}, template.Title(data), emailer.SendEmailMessage{
|
|
Text: text,
|
|
HTML: html,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send email: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getEmailer builds an emailer.Emailer from the current app config.
|
|
func (srv *EmailService) getEmailer(ctx context.Context, dbConfig *appconfig.AppConfigModel) (emailer.Emailer, error) {
|
|
// We support SMTP only (for now)
|
|
connString, err := smtpConnString(dbConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return emailer.NewEmailer(ctx, emailer.NewEmailerOpts{
|
|
ConnString: connString,
|
|
})
|
|
}
|
|
|
|
// smtpConnString builds the SMTP connection string that go-kit's emailer expects:
|
|
// smtp://<username>:<password>@<host>:<port>?fromAddress=<address>&fromName=<name>&tls=<none|starttls|tls>&insecureSkipVerify=<true|false>
|
|
func smtpConnString(dbConfig *appconfig.AppConfigModel) (string, error) {
|
|
host := dbConfig.SmtpHost.String()
|
|
if host == "" {
|
|
return "", errors.New("SMTP host is not configured")
|
|
}
|
|
|
|
u := &url.URL{
|
|
Scheme: "smtp",
|
|
Host: host,
|
|
}
|
|
port := dbConfig.SmtpPort.String()
|
|
if port != "" {
|
|
u.Host = net.JoinHostPort(host, port)
|
|
}
|
|
|
|
// Include credentials when set
|
|
smtpUser := dbConfig.SmtpUser.String()
|
|
smtpPassword := dbConfig.SmtpPassword.String()
|
|
if smtpUser != "" || smtpPassword != "" {
|
|
u.User = url.UserPassword(smtpUser, smtpPassword)
|
|
}
|
|
|
|
// TLS values from config: none, starttls, tls
|
|
tlsMode := dbConfig.SmtpTls.String()
|
|
if tlsMode == "" {
|
|
tlsMode = "none"
|
|
}
|
|
|
|
// Build the query string args
|
|
q := url.Values{}
|
|
q.Set("fromAddress", dbConfig.SmtpFrom.String())
|
|
q.Set("fromName", dbConfig.AppName.String())
|
|
q.Set("tls", tlsMode)
|
|
if dbConfig.SmtpSkipCertVerify.IsTrue() {
|
|
q.Set("insecureSkipVerify", "true")
|
|
}
|
|
u.RawQuery = q.Encode()
|
|
|
|
// Return the connection string
|
|
return u.String(), nil
|
|
}
|
|
|
|
// renderBody renders the text and HTML templates for the message into strings
|
|
func renderBody[V any](srv *EmailService, template email.Template[V], data *email.TemplateData[V]) (text string, html string, err error) {
|
|
textBuilder := &strings.Builder{}
|
|
err = email.GetTemplate(srv.textTemplates, template).ExecuteTemplate(textBuilder, "root", data)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("execute text template: %w", err)
|
|
}
|
|
|
|
htmlBuilder := &strings.Builder{}
|
|
err = email.GetTemplate(srv.htmlTemplates, template).ExecuteTemplate(htmlBuilder, "root", data)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("execute html template: %w", err)
|
|
}
|
|
|
|
return textBuilder.String(), htmlBuilder.String(), nil
|
|
}
|