refac: removed 7 direct and 12 indirect go modules for custom code instead

This commit is contained in:
Florian Sylvain committed 2026-10-21 23:13:46 +02:00
1 parent cfcf108488
commit c68e9a7858
73 files changed
+2455 -428

No files matched your search

+47
View File
@@ -0,0 +1,47 @@
package mailer
import (
"RenewCMS/internal/infrastructure/config"
"errors"
"net/smtp"
"strings"
)
type loginAuth struct {
username string
password string
}
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
if !server.TLS && !isLoopback(server.Name) {
return "", nil, errors.New("refusing to send credentials over an unencrypted connection")
}
return "LOGIN", nil, nil
}
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if !more {
return nil, nil
}
switch strings.ToLower(strings.TrimSpace(string(fromServer))) {
case "username:":
return []byte(a.username), nil
case "password:":
return []byte(a.password), nil
}
return nil, errors.New("unexpected server challenge")
}
func isLoopback(host string) bool {
return host == "localhost" || host == "127.0.0.1" || host == "::1"
}
func selectAuth(mechanisms string, settings config.SmtpSettings) smtp.Auth {
advertised := strings.Fields(strings.ToUpper(mechanisms))
for _, mechanism := range advertised {
if mechanism == "PLAIN" {
return smtp.PlainAuth("", settings.Login, settings.Password, settings.Host)
}
}
return &loginAuth{username: settings.Login, password: settings.Password}
}
@@ -4,22 +4,47 @@ import (
domain "RenewCMS/internal/domain/mail"
"RenewCMS/internal/infrastructure/config"
"bytes"
"crypto/tls"
"embed"
"html/template"
"log"
"gopkg.in/gomail.v2"
"time"
)
type MailRepository struct {
smtp config.SmtpSettings
transport transport
sender string
now func() time.Time
}
type Option func(*options)
type options struct {
tlsConfig *tls.Config
now func() time.Time
}
func WithTLSConfig(tlsConfig *tls.Config) Option {
return func(o *options) { o.tlsConfig = tlsConfig }
}
func WithClock(now func() time.Time) Option {
return func(o *options) { o.now = now }
}
//go:embed templates/*
var mailTemplateFiles embed.FS
func NewMailRepository(smtp config.SmtpSettings) *MailRepository {
return &MailRepository{smtp: smtp}
func NewMailRepository(smtp config.SmtpSettings, opts ...Option) *MailRepository {
configured := options{now: time.Now}
for _, opt := range opts {
opt(&configured)
}
return &MailRepository{
transport: newTransport(smtp, configured.tlsConfig),
sender: smtp.Sender,
now: configured.now,
}
}
const defaultSubject = "RenewCMS | Action required"
@@ -60,28 +85,23 @@ func (m MailRepository) Render(templateName string, data any) (string, string, e
}
func (m MailRepository) Send(receiverAddress string, templateName string, data any) error {
login := m.smtp.Login
sender := m.smtp.Sender
password := m.smtp.Password
smtpHost := m.smtp.Host
smtpPort := m.smtp.Port
d := gomail.NewDialer(smtpHost, smtpPort, login, password)
subject, body, err := m.Render(templateName, data)
if err != nil {
return err
}
msg := gomail.NewMessage()
msg.SetHeaders(map[string][]string{
"From": {"RenewCMS <" + sender + ">"},
"To": {receiverAddress},
"Subject": {subject},
})
msg.SetBody("text/html", body)
msg, err := newMessage(m.sender, receiverAddress, subject, body, m.now())
if err != nil {
log.Println("error when building email:", err)
return err
}
payload, err := msg.bytes()
if err != nil {
log.Println("error when encoding email:", err)
return err
}
if err := d.DialAndSend(msg); err != nil {
if err := m.transport.deliver(msg.from.Address, msg.to.Address, payload); err != nil {
log.Println("error when sending email:", err)
return err
}
+90
View File
@@ -0,0 +1,90 @@
package mailer
import (
"bytes"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"mime"
"mime/quotedprintable"
"net/mail"
"strings"
"time"
)
const (
senderDisplayName = "RenewCMS"
messageIDBytes = 16
lineBreak = "\r\n"
)
type message struct {
from mail.Address
to mail.Address
subject string
body string
sentAt time.Time
}
func newMessage(sender string, receiver string, subject string, body string, sentAt time.Time) (message, error) {
from, err := mail.ParseAddress(sender)
if err != nil {
return message{}, fmt.Errorf("invalid sender address: %w", err)
}
to, err := mail.ParseAddress(receiver)
if err != nil {
return message{}, fmt.Errorf("invalid receiver address: %w", err)
}
if strings.ContainsAny(subject, "\r\n") {
return message{}, errors.New("subject contains a line break")
}
return message{
from: mail.Address{Name: senderDisplayName, Address: from.Address},
to: *to,
subject: subject,
body: body,
sentAt: sentAt,
}, nil
}
func (m message) messageID() (string, error) {
_, domain, _ := strings.Cut(m.from.Address, "@")
random := make([]byte, messageIDBytes)
if _, err := rand.Read(random); err != nil {
return "", err
}
return "<" + hex.EncodeToString(random) + "@" + domain + ">", nil
}
func (m message) bytes() ([]byte, error) {
id, err := m.messageID()
if err != nil {
return nil, err
}
var out bytes.Buffer
headers := []struct{ name, value string }{
{"From", m.from.String()},
{"To", m.to.String()},
{"Subject", mime.QEncoding.Encode("utf-8", m.subject)},
{"Date", m.sentAt.Format(time.RFC1123Z)},
{"Message-ID", id},
{"MIME-Version", "1.0"},
{"Content-Type", "text/html; charset=UTF-8"},
{"Content-Transfer-Encoding", "quoted-printable"},
}
for _, h := range headers {
out.WriteString(h.name + ": " + h.value + lineBreak)
}
out.WriteString(lineBreak)
encoder := quotedprintable.NewWriter(&out)
if _, err := encoder.Write([]byte(m.body)); err != nil {
return nil, err
}
if err := encoder.Close(); err != nil {
return nil, err
}
return out.Bytes(), nil
}
+116
View File
@@ -0,0 +1,116 @@
package mailer
import (
"RenewCMS/internal/infrastructure/config"
"crypto/tls"
"errors"
"net"
"net/smtp"
"strconv"
"time"
)
const (
implicitTLSPort = 465
dialTimeout = 10 * time.Second
sessionTimeout = 30 * time.Second
)
type transport struct {
settings config.SmtpSettings
tlsConfig *tls.Config
}
func newTransport(settings config.SmtpSettings, tlsConfig *tls.Config) transport {
if tlsConfig == nil {
tlsConfig = &tls.Config{}
}
tlsConfig = tlsConfig.Clone()
if tlsConfig.ServerName == "" {
tlsConfig.ServerName = settings.Host
}
if tlsConfig.MinVersion == 0 {
tlsConfig.MinVersion = tls.VersionTLS12
}
return transport{settings: settings, tlsConfig: tlsConfig}
}
func (t transport) deliver(sender string, receiver string, payload []byte) error {
client, err := t.connect()
if err != nil {
return err
}
defer func() { _ = client.Close() }()
if err := t.negotiate(client); err != nil {
return err
}
if err := client.Mail(sender); err != nil {
return err
}
if err := client.Rcpt(receiver); err != nil {
return err
}
if err := writeData(client, payload); err != nil {
return err
}
return client.Quit()
}
func (t transport) connect() (*smtp.Client, error) {
address := net.JoinHostPort(t.settings.Host, strconv.Itoa(t.settings.Port))
dialer := &net.Dialer{Timeout: dialTimeout}
connection, err := dialer.Dial("tcp", address)
if err != nil {
return nil, err
}
if err := connection.SetDeadline(time.Now().Add(sessionTimeout)); err != nil {
_ = connection.Close()
return nil, err
}
if t.settings.Port == implicitTLSPort {
connection = tls.Client(connection, t.tlsConfig)
}
client, err := smtp.NewClient(connection, t.settings.Host)
if err != nil {
_ = connection.Close()
return nil, err
}
return client, nil
}
func (t transport) negotiate(client *smtp.Client) error {
if t.settings.Port != implicitTLSPort {
if supported, _ := client.Extension("STARTTLS"); supported {
if err := client.StartTLS(t.tlsConfig); err != nil {
return err
}
}
}
return t.authenticate(client)
}
func (t transport) authenticate(client *smtp.Client) error {
if t.settings.Login == "" {
return nil
}
advertised, mechanisms := client.Extension("AUTH")
if !advertised {
return errors.New("smtp server does not support authentication")
}
return client.Auth(selectAuth(mechanisms, t.settings))
}
func writeData(client *smtp.Client, payload []byte) error {
writer, err := client.Data()
if err != nil {
return err
}
if _, err := writer.Write(payload); err != nil {
_ = writer.Close()
return err
}
return writer.Close()
}