// 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 . 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 := net.JoinHostPort(cfg.SmtpServer, fmt.Sprintf("%d", 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() }