Files
easywish/backend/internal/services/smtp.go
2025-07-10 01:41:54 +03:00

144 lines
3.6 KiB
Go

// Copyright (c) 2025 Nikolai Papin
//
// This file is part of Easywish
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
// the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package services
import (
"crypto/tls"
"easywish/config"
"fmt"
"net"
"net/smtp"
"strings"
"time"
"go.uber.org/zap"
errs "easywish/internal/errors"
)
type SmtpService interface {
SendEmail(to string, subject, body string) error
}
type smtpServiceImpl struct {
log *zap.Logger
}
func NewSmtpService(_log *zap.Logger) SmtpService {
return &smtpServiceImpl{log: _log}
}
func (s *smtpServiceImpl) SendEmail(to string, subject, body string) error {
cfg := config.GetConfig()
if !cfg.SmtpEnabled {
s.log.Error("Attempted to send an email with SMTP disabled in the config")
return errs.ErrSmtpDisabled
}
if cfg.SmtpServer == "" || cfg.SmtpPort == 0 || cfg.SmtpFrom == "" {
s.log.Error("SMTP service settings or the SMTP From paramater are not set")
return errs.ErrSmtpMissingConfiguration
}
toSlice := []string{to}
headers := map[string]string{
"From": cfg.SmtpFrom,
"To": strings.Join(toSlice, ", "),
"Subject": subject,
"MIME-Version": "1.0",
"Content-Type": "text/html; charset=UTF-8",
}
var sb strings.Builder
for k, v := range headers {
sb.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
}
sb.WriteString("\r\n" + body)
message := []byte(sb.String())
hostPort := fmt.Sprintf("%s:%d", cfg.SmtpServer, cfg.SmtpPort)
var conn net.Conn
var err error
if cfg.SmtpUseSSL {
tlsConfig := &tls.Config{ServerName: cfg.SmtpServer}
conn, err = tls.Dial("tcp", hostPort, tlsConfig)
} else {
timeout := time.Duration(cfg.SmtpTimeout) * time.Second
conn, err = net.DialTimeout("tcp", hostPort, timeout)
}
if err != nil {
s.log.Error("SMTP connection failure", zap.Error(err))
return err
}
defer conn.Close()
client, err := smtp.NewClient(conn, cfg.SmtpServer)
if err != nil {
s.log.Error("SMTP client creation failed", zap.Error(err))
return err
}
defer client.Close()
if !cfg.SmtpUseSSL && cfg.SmtpUseTLS {
tlsConfig := &tls.Config{ServerName: cfg.SmtpServer}
if err = client.StartTLS(tlsConfig); err != nil {
return err
}
}
// Authenticate if credentials exist
if cfg.SmtpUser != "" && cfg.SmtpPassword != "" {
auth := smtp.PlainAuth("", cfg.SmtpUser, cfg.SmtpPassword, cfg.SmtpServer)
if err = client.Auth(auth); err != nil {
s.log.Error("SMTP authentication failure", zap.Error(err))
return err
}
}
if err = client.Mail(cfg.SmtpFrom); err != nil {
s.log.Error("SMTP sender set failed", zap.Error(err))
return err
}
for _, recipient := range toSlice {
if err = client.Rcpt(recipient); err != nil {
s.log.Error("SMTP recipient set failed", zap.Error(err))
return err
}
}
// Send email body
w, err := client.Data()
if err != nil {
s.log.Error("SMTP data command failed", zap.Error(err))
return err
}
if _, err = w.Write(message); err != nil {
s.log.Error("SMTP message write failed", zap.Error(err))
return err
}
if err = w.Close(); err != nil {
s.log.Error("SMTP message close failed", zap.Error(err))
return err
}
return client.Quit()
}