wg-portal/internal/app/mail/manager.go
2025-03-23 23:09:47 +01:00

191 lines
5.5 KiB
Go

package mail
import (
"context"
"fmt"
"io"
"log/slog"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
)
// region dependencies
type Mailer interface {
// Send sends an email with the given subject and body to the given recipients.
Send(ctx context.Context, subject, body string, to []string, options *domain.MailOptions) error
}
type ConfigFileManager interface {
// GetInterfaceConfig returns the configuration for the given interface.
GetInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) (io.Reader, error)
// GetPeerConfig returns the configuration for the given peer.
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
// GetPeerConfigQrCode returns the QR code for the given peer.
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
}
type UserDatabaseRepo interface {
// GetUser returns the user with the given identifier.
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
}
type WireguardDatabaseRepo interface {
// GetInterfaceAndPeers returns the interface and all peers for the given interface identifier.
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
// GetPeer returns the peer with the given identifier.
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
// GetInterface returns the interface with the given identifier.
GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error)
}
type TemplateRenderer interface {
// GetConfigMail returns the text and html template for the mail with a link.
GetConfigMail(user *domain.User, link string) (io.Reader, io.Reader, error)
// GetConfigMailWithAttachment returns the text and html template for the mail with an attachment.
GetConfigMailWithAttachment(user *domain.User, cfgName, qrName string) (
io.Reader,
io.Reader,
error,
)
}
// endregion dependencies
type Manager struct {
cfg *config.Config
tplHandler TemplateRenderer
mailer Mailer
configFiles ConfigFileManager
users UserDatabaseRepo
wg WireguardDatabaseRepo
}
// NewMailManager creates a new mail manager.
func NewMailManager(
cfg *config.Config,
mailer Mailer,
configFiles ConfigFileManager,
users UserDatabaseRepo,
wg WireguardDatabaseRepo,
) (*Manager, error) {
tplHandler, err := newTemplateHandler(cfg.Web.ExternalUrl)
if err != nil {
return nil, fmt.Errorf("failed to initialize template handler: %w", err)
}
m := &Manager{
cfg: cfg,
tplHandler: tplHandler,
mailer: mailer,
configFiles: configFiles,
users: users,
wg: wg,
}
return m, nil
}
// SendPeerEmail sends an email to the user linked to the given peers.
func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error {
for _, peerId := range peers {
peer, err := m.wg.GetPeer(ctx, peerId)
if err != nil {
return fmt.Errorf("failed to fetch peer %s: %w", peerId, err)
}
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
return err
}
if peer.UserIdentifier == "" {
slog.Debug("skipping peer email",
"peer", peerId,
"reason", "no user linked")
continue
}
user, err := m.users.GetUser(ctx, peer.UserIdentifier)
if err != nil {
slog.Debug("skipping peer email",
"peer", peerId,
"reason", "unable to fetch user",
"error", err)
continue
}
if user.Email == "" {
slog.Debug("skipping peer email",
"peer", peerId,
"reason", "user has no mail address")
continue
}
err = m.sendPeerEmail(ctx, linkOnly, user, peer)
if err != nil {
return fmt.Errorf("failed to send peer email for %s: %w", peerId, err)
}
}
return nil
}
func (m Manager) sendPeerEmail(ctx context.Context, linkOnly bool, user *domain.User, peer *domain.Peer) error {
qrName := "WireGuardQRCode.png"
configName := peer.GetConfigFileName()
var (
txtMail, htmlMail io.Reader
err error
mailOptions domain.MailOptions
)
if linkOnly {
txtMail, htmlMail, err = m.tplHandler.GetConfigMail(user, "deep link TBD")
if err != nil {
return fmt.Errorf("failed to get mail body: %w", err)
}
} else {
peerConfig, err := m.configFiles.GetPeerConfig(ctx, peer.Identifier)
if err != nil {
return fmt.Errorf("failed to fetch peer config for %s: %w", peer.Identifier, err)
}
peerConfigQr, err := m.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier)
if err != nil {
return fmt.Errorf("failed to fetch peer config QR code for %s: %w", peer.Identifier, err)
}
txtMail, htmlMail, err = m.tplHandler.GetConfigMailWithAttachment(user, configName, qrName)
if err != nil {
return fmt.Errorf("failed to get full mail body: %w", err)
}
mailOptions.Attachments = append(mailOptions.Attachments, domain.MailAttachment{
Name: configName,
ContentType: "text/plain",
Data: peerConfig,
Embedded: false,
})
mailOptions.Attachments = append(mailOptions.Attachments, domain.MailAttachment{
Name: qrName,
ContentType: "image/png",
Data: peerConfigQr,
Embedded: true,
})
}
txtMailStr, _ := io.ReadAll(txtMail)
htmlMailStr, _ := io.ReadAll(htmlMail)
mailOptions.HtmlBody = string(htmlMailStr)
err = m.mailer.Send(ctx, "WireGuard VPN Configuration", string(txtMailStr), []string{user.Email}, &mailOptions)
if err != nil {
return fmt.Errorf("failed to send mail: %w", err)
}
return nil
}