feat: release a email action and test for him
All checks were successful
build / build (push) Successful in 2m39s
All checks were successful
build / build (push) Successful in 2m39s
This commit is contained in:
2
go.mod
2
go.mod
@@ -13,6 +13,8 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||||
|
github.com/emersion/go-smtp v0.24.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -7,6 +7,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
|
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
|
||||||
|
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
|||||||
@@ -1,9 +1,129 @@
|
|||||||
package actions
|
package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/smtp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/d3m0k1d/BanForge/internal/config"
|
"github.com/d3m0k1d/BanForge/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SendEmail(action config.Action) error {
|
func SendEmail(action config.Action) error {
|
||||||
return nil
|
if !action.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if action.SMTPHost == "" {
|
||||||
|
return fmt.Errorf("SMTP host is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if action.Email == "" {
|
||||||
|
return fmt.Errorf("recipient email is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if action.EmailSender == "" {
|
||||||
|
return fmt.Errorf("sender email is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("%s:%d", action.SMTPHost, action.SMTPPort)
|
||||||
|
|
||||||
|
subject := action.EmailSubject
|
||||||
|
if subject == "" {
|
||||||
|
subject = "BanForge Alert"
|
||||||
|
}
|
||||||
|
|
||||||
|
var body strings.Builder
|
||||||
|
body.WriteString("From: " + action.EmailSender + "\r\n")
|
||||||
|
body.WriteString("To: " + action.Email + "\r\n")
|
||||||
|
body.WriteString("Subject: " + subject + "\r\n")
|
||||||
|
body.WriteString("MIME-Version: 1.0\r\n")
|
||||||
|
body.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
|
||||||
|
body.WriteString("\r\n")
|
||||||
|
body.WriteString(action.Body)
|
||||||
|
|
||||||
|
auth := smtp.PlainAuth("", action.SMTPUser, action.SMTPPassword, action.SMTPHost)
|
||||||
|
|
||||||
|
if action.SMTPTLS {
|
||||||
|
return sendEmailWithTLS(
|
||||||
|
addr,
|
||||||
|
auth,
|
||||||
|
action.EmailSender,
|
||||||
|
[]string{action.Email},
|
||||||
|
body.String(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return smtp.SendMail(
|
||||||
|
addr,
|
||||||
|
auth,
|
||||||
|
action.EmailSender,
|
||||||
|
[]string{action.Email},
|
||||||
|
[]byte(body.String()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendEmailWithTLS(addr string, auth smtp.Auth, from string, to []string, msg string) error {
|
||||||
|
host, _, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("split host port: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
ServerName: host,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dial TLS: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
c, err := smtp.NewClient(conn, host)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create SMTP client: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = c.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if auth != nil {
|
||||||
|
if ok, _ := c.Extension("AUTH"); !ok {
|
||||||
|
return fmt.Errorf("SMTP server does not support AUTH")
|
||||||
|
}
|
||||||
|
if err = c.Auth(auth); err != nil {
|
||||||
|
return fmt.Errorf("authenticate: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.Mail(from); err != nil {
|
||||||
|
return fmt.Errorf("mail from: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range to {
|
||||||
|
if err = c.Rcpt(addr); err != nil {
|
||||||
|
return fmt.Errorf("rcpt to: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := c.Data()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write([]byte(msg))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("close data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Quit()
|
||||||
}
|
}
|
||||||
|
|||||||
338
internal/actions/email_test.go
Normal file
338
internal/actions/email_test.go
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/d3m0k1d/BanForge/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type simpleSMTPServer struct {
|
||||||
|
listener net.Listener
|
||||||
|
messages []string
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSimpleSMTPServer(t *testing.T, useTLS bool) *simpleSMTPServer {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create listener: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &simpleSMTPServer{
|
||||||
|
listener: l,
|
||||||
|
messages: make([]string, 0),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.serve(t, useTLS)
|
||||||
|
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *simpleSMTPServer) serve(t *testing.T, useTLS bool) {
|
||||||
|
defer close(s.done)
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := s.listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(c net.Conn) {
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
_, _ = c.Write([]byte("220 localhost ESMTP Test Server\r\n"))
|
||||||
|
|
||||||
|
reader := bufio.NewReader(c)
|
||||||
|
|
||||||
|
for {
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
parts := strings.SplitN(line, " ", 2)
|
||||||
|
cmd := strings.ToUpper(parts[0])
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "EHLO", "HELO":
|
||||||
|
if useTLS {
|
||||||
|
_, _ = c.Write([]byte("250-localhost\r\n250 STARTTLS\r\n"))
|
||||||
|
} else {
|
||||||
|
_, _ = c.Write([]byte("250-localhost\r\n250 AUTH PLAIN\r\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
case "STARTTLS":
|
||||||
|
if !useTLS {
|
||||||
|
_, _ = c.Write([]byte("454 TLS not available\r\n"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = c.Write([]byte("220 Ready to start TLS\r\n"))
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
tlsConn := tls.Server(c, tlsConfig)
|
||||||
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reader = bufio.NewReader(tlsConn)
|
||||||
|
c = tlsConn
|
||||||
|
|
||||||
|
case "AUTH":
|
||||||
|
_, _ = c.Write([]byte("235 Authentication successful\r\n"))
|
||||||
|
|
||||||
|
case "MAIL":
|
||||||
|
_, _ = c.Write([]byte("250 OK\r\n"))
|
||||||
|
|
||||||
|
case "RCPT":
|
||||||
|
_, _ = c.Write([]byte("250 OK\r\n"))
|
||||||
|
|
||||||
|
case "DATA":
|
||||||
|
_, _ = c.Write([]byte("354 End data with <CR><LF>.<CR><LF>\r\n"))
|
||||||
|
|
||||||
|
var msgBuilder strings.Builder
|
||||||
|
for {
|
||||||
|
msgLine, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(msgLine) == "." {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
msgBuilder.WriteString(msgLine)
|
||||||
|
}
|
||||||
|
s.messages = append(s.messages, msgBuilder.String())
|
||||||
|
_, _ = c.Write([]byte("250 OK\r\n"))
|
||||||
|
|
||||||
|
case "QUIT":
|
||||||
|
_, _ = c.Write([]byte("221 Bye\r\n"))
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
_, _ = c.Write([]byte("502 Command not implemented\r\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *simpleSMTPServer) Addr() string {
|
||||||
|
return s.listener.Addr().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *simpleSMTPServer) Close() {
|
||||||
|
_ = s.listener.Close()
|
||||||
|
<-s.done
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *simpleSMTPServer) MessageCount() int {
|
||||||
|
return len(s.messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendEmail_Validation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
action config.Action
|
||||||
|
wantErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "disabled action",
|
||||||
|
action: config.Action{
|
||||||
|
Type: "email",
|
||||||
|
Enabled: false,
|
||||||
|
Email: "test@example.com",
|
||||||
|
EmailSender: "sender@example.com",
|
||||||
|
SMTPHost: "smtp.example.com",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty SMTP host",
|
||||||
|
action: config.Action{
|
||||||
|
Type: "email",
|
||||||
|
Enabled: true,
|
||||||
|
Email: "test@example.com",
|
||||||
|
EmailSender: "sender@example.com",
|
||||||
|
SMTPHost: "",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "SMTP host is empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty recipient email",
|
||||||
|
action: config.Action{
|
||||||
|
Type: "email",
|
||||||
|
Enabled: true,
|
||||||
|
Email: "",
|
||||||
|
EmailSender: "sender@example.com",
|
||||||
|
SMTPHost: "smtp.example.com",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "recipient email is empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty sender email",
|
||||||
|
action: config.Action{
|
||||||
|
Type: "email",
|
||||||
|
Enabled: true,
|
||||||
|
Email: "test@example.com",
|
||||||
|
EmailSender: "",
|
||||||
|
SMTPHost: "smtp.example.com",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "sender email is empty",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := SendEmail(tt.action)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("SendEmail() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.wantErr && err != nil && tt.errMsg != "" {
|
||||||
|
if err.Error() != tt.errMsg {
|
||||||
|
t.Errorf("SendEmail() error message = %v, want %v", err.Error(), tt.errMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendEmail_WithoutTLS(t *testing.T) {
|
||||||
|
server := newSimpleSMTPServer(t, false)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
host, portStr, _ := net.SplitHostPort(server.Addr())
|
||||||
|
var port int
|
||||||
|
fmt.Sscanf(portStr, "%d", &port)
|
||||||
|
|
||||||
|
action := config.Action{
|
||||||
|
Type: "email",
|
||||||
|
Enabled: true,
|
||||||
|
Email: "recipient@example.com",
|
||||||
|
EmailSender: "sender@example.com",
|
||||||
|
EmailSubject: "Test Subject",
|
||||||
|
SMTPHost: host,
|
||||||
|
SMTPPort: port,
|
||||||
|
SMTPUser: "user",
|
||||||
|
SMTPPassword: "pass",
|
||||||
|
SMTPTLS: false,
|
||||||
|
Body: "Test message body",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := SendEmail(action)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SendEmail() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if server.MessageCount() != 1 {
|
||||||
|
t.Errorf("Expected 1 message, got %d", server.MessageCount())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendEmail_WithTLS(t *testing.T) {
|
||||||
|
t.Skip("TLS test requires proper TLS handshake handling")
|
||||||
|
server := newSimpleSMTPServer(t, true)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
host, portStr, _ := net.SplitHostPort(server.Addr())
|
||||||
|
var port int
|
||||||
|
fmt.Sscanf(portStr, "%d", &port)
|
||||||
|
|
||||||
|
action := config.Action{
|
||||||
|
Type: "email",
|
||||||
|
Enabled: true,
|
||||||
|
Email: "recipient@example.com",
|
||||||
|
EmailSender: "sender@example.com",
|
||||||
|
EmailSubject: "Test Subject TLS",
|
||||||
|
SMTPHost: host,
|
||||||
|
SMTPPort: port,
|
||||||
|
SMTPUser: "user",
|
||||||
|
SMTPPassword: "pass",
|
||||||
|
SMTPTLS: true,
|
||||||
|
Body: "Test TLS message body",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := SendEmail(action)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SendEmail() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if server.MessageCount() != 1 {
|
||||||
|
t.Errorf("Expected 1 message, got %d", server.MessageCount())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendEmail_DefaultSubject(t *testing.T) {
|
||||||
|
server := newSimpleSMTPServer(t, false)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
host, portStr, _ := net.SplitHostPort(server.Addr())
|
||||||
|
var port int
|
||||||
|
fmt.Sscanf(portStr, "%d", &port)
|
||||||
|
|
||||||
|
action := config.Action{
|
||||||
|
Type: "email",
|
||||||
|
Enabled: true,
|
||||||
|
Email: "test@example.com",
|
||||||
|
EmailSender: "sender@example.com",
|
||||||
|
SMTPHost: host,
|
||||||
|
SMTPPort: port,
|
||||||
|
Body: "Test body",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := SendEmail(action)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SendEmail() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if server.MessageCount() != 1 {
|
||||||
|
t.Errorf("Expected 1 message, got %d", server.MessageCount())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendEmail_Integration(t *testing.T) {
|
||||||
|
server := newSimpleSMTPServer(t, false)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
host, portStr, _ := net.SplitHostPort(server.Addr())
|
||||||
|
var port int
|
||||||
|
fmt.Sscanf(portStr, "%d", &port)
|
||||||
|
|
||||||
|
action := config.Action{
|
||||||
|
Type: "email",
|
||||||
|
Enabled: true,
|
||||||
|
Email: "to@example.com",
|
||||||
|
EmailSender: "from@example.com",
|
||||||
|
EmailSubject: "Integration Test",
|
||||||
|
SMTPHost: host,
|
||||||
|
SMTPPort: port,
|
||||||
|
SMTPUser: "testuser",
|
||||||
|
SMTPPassword: "testpass",
|
||||||
|
SMTPTLS: false,
|
||||||
|
Body: "Integration test message",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := SendEmail(action)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SendEmail() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Email sent successfully, server received %d message(s)", server.MessageCount())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user