refac: removed 7 direct and 12 indirect go modules for custom code instead
This commit is contained in:
1 parent
cfcf108488
commit
c68e9a7858
73 files changed
+2455
-428
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in new issue
Block a user