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:
@@ -1,9 +1,129 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
|
||||
"github.com/d3m0k1d/BanForge/internal/config"
|
||||
)
|
||||
|
||||
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