Files
sendico/api/notification/internal/server/notificationimp/mail/internal/mailimp.go
Stephan D 717dafc673
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
better message formatting
2025-11-19 13:54:25 +01:00

175 lines
6.1 KiB
Go
Executable File

package mailimp
import (
"crypto/tls"
"time"
"github.com/tech/sendico/notification/interface/api/localizer"
mb "github.com/tech/sendico/notification/internal/server/notificationimp/mail/internal/builder"
"github.com/tech/sendico/notification/internal/server/notificationimp/mail/internal/mailkey"
mmail "github.com/tech/sendico/notification/internal/server/notificationimp/mail/messagebuilder"
"github.com/tech/sendico/pkg/domainprovider"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
mutil "github.com/tech/sendico/pkg/mutil/config"
mduration "github.com/tech/sendico/pkg/mutil/duration"
mail "github.com/xhit/go-simple-mail/v2"
"go.uber.org/zap"
)
// Client implements a mail client
type Client struct {
logger mlogger.Logger
server *mail.SMTPServer
client *mail.SMTPClient
from string
l localizer.Localizer
dp domainprovider.DomainProvider
}
// Config represents the mail configuration
type GSMConfig struct {
Username *string `mapstructure:"username,omitempty" yaml:"username,omitempty"`
UsernameEnv *string `mapstructure:"username_env,omitempty" yaml:"username_env,omitempty"`
Password *string `mapstructure:"password" yaml:"password"`
PasswordEnv *string `mapstructure:"password_env" yaml:"password_env"`
Host string `mapstructure:"host" yaml:"host"`
Port int `mapstructure:"port" yaml:"port"`
From string `mapstructure:"from" yaml:"from"`
TimeOut int `mapstructure:"network_timeout" yaml:"network_timeout"`
}
func (c *Client) sendImp(m mmail.Message, msg *mail.Email) error {
err := msg.Send(c.client)
if err != nil {
c.logger.Warn("Error sending email", zap.Error(err), zap.String("template_id", m.TemplateID()), zap.Strings("recipients", msg.GetRecipients()))
} else {
c.logger.Info("Email sent", zap.Strings("recipients", msg.GetRecipients()), zap.String("template_id", m.TemplateID()))
}
// TODO: add amplitude notification
return err
}
// Send sends an email message to the provided address and with the provided subject
func (c *Client) Send(r mmail.MailBuilder) error {
// New email simple html with inline and CC
r.AddData("ServiceName", c.l.ServiceName()).AddData("SupportMail", c.l.SupportMail())
m, err := r.Build()
if err != nil {
c.logger.Warn("Failed to build message", zap.Error(err))
return err
}
body, err := m.Body(c.l, c.dp)
if err != nil {
c.logger.Warn("Failed to build message body", zap.Error(err))
return err
}
if (len(body) == 0) || (len(m.Recipients()) == 0) {
c.logger.Warn("Malformed messge", zap.String("template_id", m.TemplateID()),
zap.String("locale", m.Locale()), zap.Strings("recipients", m.Recipients()),
zap.Int("body_size", len(body)))
return merrors.InvalidArgument("malformed message", "message.body", "message.recipients")
}
subj, err := mailkey.Subject(c.l, m.Parameters(), m.TemplateID(), m.Locale())
if err != nil {
c.logger.Warn("Failed to localize subject", zap.Error(err), zap.String("template_id", m.TemplateID()),
zap.String("locale", m.Locale()), zap.Strings("recipients", m.Recipients()),
zap.Int("body_size", len(body)))
return err
}
msg := mail.NewMSG()
msg.SetFrom(c.from).
AddTo(m.Recipients()...).
SetSubject(subj).
SetBody(mail.TextHTML, body)
// Call Send and pass the client
if err = c.sendImp(m, msg); err != nil {
c.logger.Info("Failed to send an email, attempting to reconnect...",
zap.Error(err),
zap.String("template", m.TemplateID()), zap.Strings("addresses", m.Recipients()), zap.String("locale", m.Locale()))
c.client = nil
c.client, err = c.server.Connect()
if err != nil {
c.logger.Warn("Failed to reconnect mail client",
zap.Error(err),
zap.String("template", m.TemplateID()), zap.Strings("addresses", m.Recipients()), zap.String("locale", m.Locale()))
return err
}
c.logger.Info("Connection has been successfully restored",
zap.Error(err),
zap.String("template", m.TemplateID()), zap.Strings("addresses", m.Recipients()), zap.String("locale", m.Locale()))
err = c.sendImp(m, msg)
if err != nil {
c.logger.Warn("Failed to send message after mail client recreation",
zap.Error(err),
zap.String("template", m.TemplateID()), zap.Strings("addresses", m.Recipients()), zap.String("locale", m.Locale()))
return err
}
}
return err
}
func (c *Client) MailBuilder() mmail.MailBuilder {
return mb.NewMessageBuilder()
}
// NewClient return a new mail
func NewClient(logger mlogger.Logger, l localizer.Localizer, dp domainprovider.DomainProvider, config *GSMConfig) *Client {
smtpServer := mail.NewSMTPClient()
// SMTP Server
smtpServer.Host = config.Host
if config.Port < 1 {
logger.Warn("Invalid mail client port configuration, defaulting to 465", zap.Int("port", config.Port))
config.Port = 465
}
smtpServer.Port = config.Port
smtpServer.Username = mutil.GetConfigValue(logger, "username", "username_env", config.Username, config.UsernameEnv)
smtpServer.Password = mutil.GetConfigValue(logger, "password", "password_env", config.Password, config.PasswordEnv)
smtpServer.Encryption = mail.EncryptionSSL
// Since v2.3.0 you can specified authentication type:
// - PLAIN (default)
// - LOGIN
// - CRAM-MD5
// server.Authentication = mail.AuthPlain
// Variable to keep alive connection
smtpServer.KeepAlive = true
// Timeout for connect to SMTP Server
smtpServer.ConnectTimeout = mduration.Param2Duration(config.TimeOut, time.Second)
// Timeout for send the data and wait respond
smtpServer.SendTimeout = mduration.Param2Duration(config.TimeOut, time.Second)
// Set TLSConfig to provide custom TLS configuration. For example,
// to skip TLS verification (useful for testing):
smtpServer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
// SMTP client
lg := logger.Named("client")
smtpClient, err := smtpServer.Connect()
if err != nil {
lg.Warn("Failed to connect", zap.Error(err))
} else {
lg.Info("Connected successfully", zap.String("username", smtpServer.Username), zap.String("host", config.Host))
}
from := config.From + " <" + smtpServer.Username + ">"
return &Client{
logger: lg,
server: smtpServer,
client: smtpClient,
from: from,
l: l,
dp: dp,
}
}