Add linter and fix linter warnings

This commit is contained in:
wiggin77
2025-05-24 23:46:23 -04:00
parent a4af8bc89b
commit 2e10ada37b
9 changed files with 452 additions and 193 deletions

70
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,70 @@
name: CI
on:
push:
branches: [ master, main ]
pull_request:
branches: [ master, main ]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23.8'
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.63.4
args: --timeout=5m
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23.8'
- name: Download dependencies
run: go mod download
- name: Run tests
run: go test -v ./...
- name: Run tests with race detector
run: go test -race -v ./...
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23.8'
- name: Download dependencies
run: go mod download
- name: Build
run: go build -v ./...
- name: Build for multiple architectures
run: |
GOOS=linux GOARCH=amd64 go build -o mailrelay-linux-amd64 .
GOOS=linux GOARCH=arm64 go build -o mailrelay-linux-arm64 .
GOOS=darwin GOARCH=amd64 go build -o mailrelay-darwin-amd64 .
GOOS=darwin GOARCH=arm64 go build -o mailrelay-darwin-arm64 .
GOOS=windows GOARCH=amd64 go build -o mailrelay-windows-amd64.exe .

View File

@ -1,22 +0,0 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
name: CI Build
on:
push:
branches:
- master
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23.9'
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...

View File

@ -1,73 +1,224 @@
# golangci-lint configuration for mailrelay project
# See https://golangci-lint.run/usage/configuration/ for configuration options
run:
timeout: 5m
issues-exit-code: 1
tests: true
modules-download-mode: readonly
output:
formats:
- format: colored-line-number
print-issued-lines: true
print-linter-name: true
sort-results: true
linters-settings: linters-settings:
gocritic: # Cyclomatic complexity
enabled-tags: cyclop:
- diagnostic max-complexity: 15
- experimental package-average: 10.0
- performance skip-tests: false
- style
disabled-checks: # Duplicate code detection
- dupImport # https://github.com/go-critic/go-critic/issues/845 dupl:
- ifElseChain threshold: 100
- octalLiteral
- whyNoLint # Function length
- wrapperFunc funlen:
govet: lines: 80
check-shadowing: true statements: 50
# Cognitive complexity
gocognit:
min-complexity: 20
# Cyclomatic complexity (alternative to cyclop)
gocyclo:
min-complexity: 15
# Line length
lll:
line-length: 120
# Naming conventions
revive:
rules:
- name: exported
severity: warning
disabled: false
- name: unexported-return
severity: warning
disabled: false
- name: time-naming
severity: warning
disabled: false
- name: var-declaration
severity: warning
disabled: false
- name: package-comments
severity: warning
disabled: false
# Security checks
gosec:
excludes:
- G402 # TLS InsecureSkipVerify set true (we have it configurable with comments)
config:
G306: "0644"
# Unused parameters
unparam:
check-exported: false
# Unused variables
unused:
check-exported: false
# Error handling
errcheck:
check-type-assertions: true
check-blank: true
# Go format
gofmt:
simplify: true
# Import organization
goimports:
local-prefixes: github.com/wiggin77/mailrelay
# Misspelling
misspell: misspell:
locale: US locale: US
linters: linters:
# please, do not use `enable-all`: it's deprecated and will be removed soon.
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
disable-all: true
enable: enable:
- bodyclose # Default linters
- deadcode
- depguard
- dogsled
- dupl
- errcheck - errcheck
# - funlen
- gochecknoinits
- goconst
# - gocritic
- gocyclo
- gofmt
- goimports
- golint
- gomnd
- goprintffuncname
- gosec
- gosimple - gosimple
- govet - govet
- ineffassign - ineffassign
- interfacer
- lll
- misspell
- nakedret
# - nolintlint
- rowserrcheck
- scopelint
- staticcheck - staticcheck
- structcheck
- stylecheck
- typecheck - typecheck
- unconvert
- unparam
- unused - unused
- varcheck
- whitespace
# don't enable: # Additional recommended linters
# - asciicheck - asciicheck # Check for non-ASCII characters
# - gochecknoglobals - bodyclose # Check HTTP response body is closed
# - gocognit - cyclop # Cyclomatic complexity
# - godot - dupl # Duplicate code detection
# - godox - durationcheck # Duration checks
# - goerr113 - errname # Error naming conventions
# - maligned - errorlint # Error wrapping
# - nestif - exhaustive # Exhaustiveness checks
# - prealloc - copyloopvar # Loop variable capturing (exportloopref renamed)
# - testpackage - funlen # Function length
# - wsl - gochecknoinits # No init functions
- gocognit # Cognitive complexity
- goconst # Repeated strings that could be constants
- gocritic # Go source code linter
- gocyclo # Cyclomatic complexity
- gofmt # Gofmt checks
- goimports # Import formatting
- mnd # Magic numbers (gomnd renamed)
- gomoddirectives # Go.mod directives
- gomodguard # Go.mod guard
- goprintffuncname # Printf function naming
- gosec # Security checks
- lll # Line length
- makezero # Slice initialization
- misspell # Misspellings
- nilerr # Nil error checks
- nilnil # Nil nil checks
- noctx # HTTP request without context
- nolintlint # Nolint directive checks
- predeclared # Predeclared identifier checks
- revive # Golint replacement
- rowserrcheck # SQL rows error check
- sqlclosecheck # SQL close check
- tparallel # Test parallelism
- unparam # Unused parameters
- wastedassign # Wasted assignments
- whitespace # Whitespace checks
disable:
- forbidigo # Not needed for this project
- gci # Import organization (we use goimports)
- godox # TODO comments are OK
- err113 # Too strict for this project (goerr113 renamed)
- wrapcheck # Too strict for this project
- godot # Comment periods are pedantic
- gofumpt # Standard gofmt is sufficient
- nestif # Sometimes deep nesting is clearest
- nakedret # Naked returns OK in short functions
- prealloc # Micro-optimizations not always worth it
- unconvert # Type conversions can aid clarity
issues:
# Excluding configuration per-path, per-linter, per-text and per-source
uniq-by-line: true
exclude-rules:
# Exclude many linters from running on test files
- path: _test\.go
linters:
- mnd # Magic numbers are common in tests
- goconst # String constants less important in tests
- funlen # Test functions can be longer
- dupl # Duplicate code acceptable in tests
- gocognit # Cognitive complexity relaxed for tests
- gocyclo # Cyclomatic complexity relaxed for tests
- cyclop # Cyclomatic complexity relaxed for tests
- errcheck # Error checking can be relaxed in tests
- gosec # Security checks relaxed for test code
- lll # Line length can be longer in tests
- revive # General style checks relaxed
- ineffassign # Ineffectual assignments OK in mock code
- unparam # Unused parameters OK in test helpers
# Exclude linters specifically for mock SMTP server
- path: mock_smtp_server\.go
linters:
- mnd # Magic numbers acceptable in mock server
- goconst # String constants less important in mock code
- funlen # Mock functions can be longer
- gocognit # Cognitive complexity relaxed for mock server
- gocyclo # Cyclomatic complexity relaxed for mock server
- cyclop # Cyclomatic complexity relaxed for mock server
- errcheck # Error checking relaxed in mock server
- gosec # Security checks not needed in mock code
- lll # Line length relaxed for mock server
- ineffassign # Ineffectual assignments OK in mock code
# Exclude known linter issues or false positives
- text: "weak cryptographic primitive"
linters:
- gosec
# Allow long lines in generated files
- path: ".*\\.pb\\.go"
linters:
- lll
# Allow complexity in main function (CLI parsing)
- path: main\.go
text: "cognitive complexity"
linters:
- gocognit
# Show only new issues from the last revision
new: false
# Fix issues automatically where possible
fix: false
# Maximum issues count per one linter
max-issues-per-linter: 0
# Maximum count of issues with the same text
max-same-issues: 0
severity:
default-severity: error
case-sensitive: false

View File

@ -63,6 +63,14 @@ test-config: build
check-ip: build check-ip: build
./$(BINARY_NAME) -config=./mailrelay.json -checkIP -ip=$(IP) ./$(BINARY_NAME) -config=./mailrelay.json -checkIP -ip=$(IP)
# Install golangci-lint and run code style checks
.PHONY: check-style
check-style:
@echo "Installing golangci-lint v1.63.4..."
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.63.4
@echo "Running golangci-lint..."
golangci-lint run
# Show help # Show help
.PHONY: help .PHONY: help
help: help:
@ -70,6 +78,7 @@ help:
@echo " build - Build for current architecture" @echo " build - Build for current architecture"
@echo " buildall - Build for all supported architectures" @echo " buildall - Build for all supported architectures"
@echo " test - Run tests" @echo " test - Run tests"
@echo " check-style - Install golangci-lint and run code style checks"
@echo " clean - Remove build artifacts" @echo " clean - Remove build artifacts"
@echo " run - Build and run the application" @echo " run - Build and run the application"
@echo " test-config - Test configuration with sample email" @echo " test-config - Test configuration with sample email"

View File

@ -44,7 +44,7 @@ func sendMail(e *mail.Envelope, config *relayConfig) error {
// InsecureSkipVerify is configurable to support legacy SMTP servers with // InsecureSkipVerify is configurable to support legacy SMTP servers with
// self-signed certificates or hostname mismatches. This should only be // self-signed certificates or hostname mismatches. This should only be
// enabled in trusted network environments. // enabled in trusted network environments.
InsecureSkipVerify: config.SkipVerify, //nolint:gosec InsecureSkipVerify: config.SkipVerify,
ServerName: config.Server, ServerName: config.Server,
} }
@ -59,13 +59,13 @@ func sendMail(e *mail.Envelope, config *relayConfig) error {
} }
if client, err = smtp.NewClient(conn, config.Server); err != nil { if client, err = smtp.NewClient(conn, config.Server); err != nil {
close(conn, "conn") closeConn(conn, "conn")
return errors.Wrap(err, "newclient error") return errors.Wrap(err, "newclient error")
} }
shouldCloseClient := true shouldCloseClient := true
defer func(shouldClose *bool) { defer func(shouldClose *bool) {
if *shouldClose { if *shouldClose {
close(client, "client") closeConn(client, "client")
} }
}(&shouldCloseClient) }(&shouldCloseClient)
@ -87,7 +87,7 @@ func sendMail(e *mail.Envelope, config *relayConfig) error {
return errors.Wrap(err, "data error") return errors.Wrap(err, "data error")
} }
_, err = writer.Write(msg.Bytes()) _, err = writer.Write(msg.Bytes())
close(writer, "writer") closeConn(writer, "writer")
if err != nil { if err != nil {
return errors.Wrap(err, "write error") return errors.Wrap(err, "write error")
} }
@ -115,7 +115,7 @@ func handshake(client *smtp.Client, config *relayConfig, tlsConfig *tls.Config)
} }
} }
var auth smtp.Auth = nil var auth smtp.Auth
if config.LoginAuthType { if config.LoginAuthType {
auth = LoginAuth(config.Username, config.Password) auth = LoginAuth(config.Username, config.Password)
@ -131,7 +131,7 @@ func handshake(client *smtp.Client, config *relayConfig, tlsConfig *tls.Config)
return nil return nil
} }
func close(c closeable, what string) { func closeConn(c closeable, what string) {
err := c.Close() err := c.Close()
if err != nil { if err != nil {
fmt.Printf("Error closing %s: %v\n", what, err) fmt.Printf("Error closing %s: %v\n", what, err)
@ -142,7 +142,8 @@ func isQuitError(err error) bool {
if err == nil { if err == nil {
return false return false
} }
e, ok := err.(*textproto.Error) var e *textproto.Error
ok := errors.As(err, &e)
if ok { if ok {
// SMTP codes 221 or 250 are acceptable here // SMTP codes 221 or 250 are acceptable here
if e.Code == 221 || e.Code == 250 { if e.Code == 221 || e.Code == 250 {
@ -154,7 +155,7 @@ func isQuitError(err error) bool {
// getTo returns the array of email addresses in the envelope. // getTo returns the array of email addresses in the envelope.
func getTo(e *mail.Envelope) []string { func getTo(e *mail.Envelope) []string {
var ret []string ret := make([]string, 0, len(e.RcptTo))
for i := range e.RcptTo { for i := range e.RcptTo {
ret = append(ret, e.RcptTo[i].String()) ret = append(ret, e.RcptTo[i].String())
} }

View File

@ -12,7 +12,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// setupTestLogger initializes the logger for testing // setupTestLogger initializes the logger for testing.
func setupTestLogger(t *testing.T) { func setupTestLogger(t *testing.T) {
var err error var err error
Logger, err = log.GetLogger("stdout", "info") Logger, err = log.GetLogger("stdout", "info")

90
main.go
View File

@ -17,16 +17,17 @@ import (
const ( const (
DefaultSMTPPort = 465 DefaultSMTPPort = 465
DefaultMaxEmailSize = (10 << 23) // 83 MB DefaultMaxEmailSize = 83886080 // 80 MB (80 * 1024 * 1024)
DefaultLocalListenIP = "0.0.0.0" DefaultLocalListenIP = "0.0.0.0"
DefaultLocalListenPort = 2525 DefaultLocalListenPort = 2525
DefaultTimeoutSecs = 300 // 5 minutes DefaultTimeoutSecs = 300 // 5 minutes
MinEmailSizeBytes = 1024
) )
// Logger is the global logger // Logger is the global logger.
var Logger log.Logger var Logger log.Logger
// Global List of Allowed Sender IPs: // AllowedSendersFilter holds the global list of allowed sender IPs.
var AllowedSendersFilter = ipfilter.New(ipfilter.Options{}) var AllowedSendersFilter = ipfilter.New(ipfilter.Options{})
type mailRelayConfig struct { type mailRelayConfig struct {
@ -56,6 +57,39 @@ func main() {
} }
func run() error { func run() error {
configFile, test, testsender, testrcpt, checkIP, ipToCheck, verbose := parseFlags()
appConfig, err := loadConfig(configFile)
if err != nil {
flag.Usage()
return fmt.Errorf("loading config: %w", err)
}
if err := setupIPFilter(appConfig); err != nil {
return err
}
if err := setupLogger(verbose); err != nil {
return err
}
if err := Start(appConfig, verbose); err != nil {
flag.Usage()
return fmt.Errorf("starting server: %w", err)
}
if test {
return runTest(testsender, testrcpt, appConfig.LocalListenPort)
}
if checkIP {
return runIPCheck(ipToCheck)
}
return waitForSignal()
}
func parseFlags() (string, bool, string, string, bool, string, bool) {
var configFile string var configFile string
var test bool var test bool
var testsender string var testsender string
@ -63,6 +97,7 @@ func run() error {
var checkIP bool var checkIP bool
var ipToCheck string var ipToCheck string
var verbose bool var verbose bool
flag.StringVar(&configFile, "config", "/etc/mailrelay.json", "specifies JSON config file") flag.StringVar(&configFile, "config", "/etc/mailrelay.json", "specifies JSON config file")
flag.BoolVar(&test, "test", false, "sends a test message to SMTP server") flag.BoolVar(&test, "test", false, "sends a test message to SMTP server")
flag.StringVar(&testsender, "sender", "", "used with 'test' to specify sender email address") flag.StringVar(&testsender, "sender", "", "used with 'test' to specify sender email address")
@ -72,18 +107,19 @@ func run() error {
flag.StringVar(&ipToCheck, "ip", "", "used with 'checkIP' to specify IP address to test") flag.StringVar(&ipToCheck, "ip", "", "used with 'checkIP' to specify IP address to test")
flag.Parse() flag.Parse()
appConfig, err := loadConfig(configFile) return configFile, test, testsender, testrcpt, checkIP, ipToCheck, verbose
if err != nil { }
flag.Usage()
return fmt.Errorf("loading config: %w", err) func setupIPFilter(appConfig *mailRelayConfig) error {
if appConfig.AllowedSenders == "*" {
return nil
} }
if appConfig.AllowedSenders != "*" {
file, err := os.Open(appConfig.AllowedSenders) file, err := os.Open(appConfig.AllowedSenders)
if err != nil { if err != nil {
return fmt.Errorf("failed opening file: %s", err) return fmt.Errorf("failed opening file: %w", err)
} }
defer file.Close()
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
@ -93,42 +129,42 @@ func run() error {
allowedIPsAndRanges = append(allowedIPsAndRanges, scanner.Text()) allowedIPsAndRanges = append(allowedIPsAndRanges, scanner.Text())
} }
file.Close()
AllowedSendersFilter = ipfilter.New(ipfilter.Options{ AllowedSendersFilter = ipfilter.New(ipfilter.Options{
//AllowedIPs: []string{"192.168.0.0/24"},
AllowedIPs: allowedIPsAndRanges, AllowedIPs: allowedIPsAndRanges,
BlockByDefault: true, BlockByDefault: true,
}) })
return nil
} }
func setupLogger(verbose bool) error {
logLevel := "info" logLevel := "info"
if verbose { if verbose {
logLevel = "debug" logLevel = "debug"
} }
var err error
Logger, err = log.GetLogger("stdout", logLevel) Logger, err = log.GetLogger("stdout", logLevel)
if err != nil { if err != nil {
return fmt.Errorf("creating logger: %w", err) return fmt.Errorf("creating logger: %w", err)
} }
return nil
err = Start(appConfig, verbose)
if err != nil {
flag.Usage()
return fmt.Errorf("starting server: %w", err)
} }
if test { func runTest(testsender, testrcpt string, port int) error {
err = sendTest(testsender, testrcpt, appConfig.LocalListenPort) err := sendTest(testsender, testrcpt, port)
if err != nil { if err != nil {
return fmt.Errorf("sending test message: %w", err) return fmt.Errorf("sending test message: %w", err)
} }
return nil return nil
} }
if checkIP { func runIPCheck(ipToCheck string) error {
if ipToCheck == "" { if ipToCheck == "" {
return errors.New("IP address to check is required when `checkIP` flag is used. Provide an IP address using the `-ip` flag") return errors.New("IP address to check is required when `checkIP` flag is used. " +
"Provide an IP address using the `-ip` flag")
} }
result := "" result := ""
if !AllowedSendersFilter.Blocked(ipToCheck) { if !AllowedSendersFilter.Blocked(ipToCheck) {
result = "NOT " result = "NOT "
@ -137,11 +173,9 @@ func run() error {
return nil return nil
} }
// Wait for SIGINT func waitForSignal() error {
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM) signal.Notify(c, os.Interrupt, syscall.SIGTERM)
// Block until a signal is received.
<-c <-c
return nil return nil
} }
@ -181,7 +215,7 @@ func configDefaults(config *mailRelayConfig) {
config.TimeoutSecs = DefaultTimeoutSecs config.TimeoutSecs = DefaultTimeoutSecs
} }
// validateConfig validates the configuration values // validateConfig validates the configuration values.
func validateConfig(config *mailRelayConfig) error { func validateConfig(config *mailRelayConfig) error {
if config.SMTPServer == "" { if config.SMTPServer == "" {
return errors.New("smtp_server is required") return errors.New("smtp_server is required")
@ -195,7 +229,7 @@ func validateConfig(config *mailRelayConfig) error {
return errors.New("local_listen_port must be between 1 and 65535") return errors.New("local_listen_port must be between 1 and 65535")
} }
if config.MaxEmailSize < 1024 { if config.MaxEmailSize < MinEmailSizeBytes {
return errors.New("smtp_max_email_size must be at least 1024 bytes") return errors.New("smtp_max_email_size must be at least 1024 bytes")
} }
@ -206,7 +240,7 @@ func validateConfig(config *mailRelayConfig) error {
return nil return nil
} }
// sendTest sends a test message to the SMTP server specified in mailrelay.json // sendTest sends a test message to the SMTP server specified in mailrelay.json.
func sendTest(sender string, rcpt string, port int) error { func sendTest(sender string, rcpt string, port int) error {
conn, err := smtp.Dial(fmt.Sprintf("localhost:%d", port)) conn, err := smtp.Dial(fmt.Sprintf("localhost:%d", port))
if err != nil { if err != nil {

View File

@ -16,7 +16,15 @@ import (
"time" "time"
) )
// MockSMTPServer represents a mock SMTP server for testing const (
smtpSTARTTLS = "STARTTLS"
rsaKeyBits = 2048
ipOctet127 = 127
sleepDurationMs = 10
minAuthParts = 2
)
// MockSMTPServer represents a mock SMTP server for testing.
type MockSMTPServer struct { type MockSMTPServer struct {
listener net.Listener listener net.Listener
tlsConfig *tls.Config tlsConfig *tls.Config
@ -48,7 +56,7 @@ type MockConnection struct {
UsedTLS bool UsedTLS bool
} }
// NewMockSMTPServer creates a new mock SMTP server // NewMockSMTPServer creates a new mock SMTP server.
func NewMockSMTPServer(t *testing.T) *MockSMTPServer { func NewMockSMTPServer(t *testing.T) *MockSMTPServer {
cert, err := generateTestCert() cert, err := generateTestCert()
if err != nil { if err != nil {
@ -68,7 +76,7 @@ func NewMockSMTPServer(t *testing.T) *MockSMTPServer {
} }
} }
// Start starts the mock SMTP server // Start starts the mock SMTP server.
func (s *MockSMTPServer) Start() error { func (s *MockSMTPServer) Start() error {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -79,18 +87,19 @@ func (s *MockSMTPServer) Start() error {
} }
s.listener = listener s.listener = listener
s.address = listener.Addr().(*net.TCPAddr).IP.String() addr := listener.Addr().(*net.TCPAddr)
s.port = listener.Addr().(*net.TCPAddr).Port s.address = addr.IP.String()
s.port = addr.Port
s.running = true s.running = true
go s.acceptConnections() go s.acceptConnections()
// Give the server a moment to start // Give the server a moment to start
time.Sleep(10 * time.Millisecond) time.Sleep(sleepDurationMs * time.Millisecond)
return nil return nil
} }
// StartTLS starts the mock SMTP server with implicit TLS // StartTLS starts the mock SMTP server with implicit TLS.
func (s *MockSMTPServer) StartTLS() error { func (s *MockSMTPServer) StartTLS() error {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -102,19 +111,20 @@ func (s *MockSMTPServer) StartTLS() error {
tlsListener := tls.NewListener(listener, s.tlsConfig) tlsListener := tls.NewListener(listener, s.tlsConfig)
s.listener = tlsListener s.listener = tlsListener
s.address = listener.Addr().(*net.TCPAddr).IP.String() addr := listener.Addr().(*net.TCPAddr)
s.port = listener.Addr().(*net.TCPAddr).Port s.address = addr.IP.String()
s.port = addr.Port
s.running = true s.running = true
s.ImplicitTLS = true s.ImplicitTLS = true
go s.acceptConnections() go s.acceptConnections()
// Give the server a moment to start // Give the server a moment to start
time.Sleep(10 * time.Millisecond) time.Sleep(sleepDurationMs * time.Millisecond)
return nil return nil
} }
// Stop stops the mock SMTP server // Stop stops the mock SMTP server.
func (s *MockSMTPServer) Stop() { func (s *MockSMTPServer) Stop() {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -125,17 +135,17 @@ func (s *MockSMTPServer) Stop() {
} }
} }
// Address returns the server address // Address returns the server address.
func (s *MockSMTPServer) Address() string { func (s *MockSMTPServer) Address() string {
return s.address return s.address
} }
// Port returns the server port // Port returns the server port.
func (s *MockSMTPServer) Port() int { func (s *MockSMTPServer) Port() int {
return s.port return s.port
} }
// GetLastConnection returns the most recent connection // GetLastConnection returns the most recent connection.
func (s *MockSMTPServer) GetLastConnection() *MockConnection { func (s *MockSMTPServer) GetLastConnection() *MockConnection {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -146,7 +156,7 @@ func (s *MockSMTPServer) GetLastConnection() *MockConnection {
return &s.Connections[len(s.Connections)-1] return &s.Connections[len(s.Connections)-1]
} }
// Reset clears all recorded connections // Reset clears all recorded connections.
func (s *MockSMTPServer) Reset() { func (s *MockSMTPServer) Reset() {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -189,7 +199,7 @@ func (s *MockSMTPServer) handleConnection(conn net.Conn) {
} }
// Send greeting // Send greeting
writer.WriteString("220 mock.smtp.server ESMTP ready\r\n") _, _ = writer.WriteString("220 mock.smtp.server ESMTP ready\r\n")
writer.Flush() writer.Flush()
for { for {
@ -210,14 +220,14 @@ func (s *MockSMTPServer) handleConnection(conn net.Conn) {
// Check if we should fail this command // Check if we should fail this command
if s.FailCommands[cmd] { if s.FailCommands[cmd] {
writer.WriteString("550 Command failed\r\n") _, _ = writer.WriteString("550 Command failed\r\n")
writer.Flush() writer.Flush()
continue continue
} }
// Check for custom responses // Check for custom responses
if response, exists := s.CustomResponses[cmd]; exists { if response, exists := s.CustomResponses[cmd]; exists {
writer.WriteString(response + "\r\n") _, _ = writer.WriteString(response + "\r\n")
writer.Flush() writer.Flush()
continue continue
} }
@ -225,7 +235,7 @@ func (s *MockSMTPServer) handleConnection(conn net.Conn) {
switch cmd { switch cmd {
case "EHLO", "HELO": case "EHLO", "HELO":
s.handleEHLO(writer) s.handleEHLO(writer)
case "STARTTLS": case smtpSTARTTLS:
tlsConn, newReader, newWriter, upgraded := s.handleSTARTTLS(conn, reader, writer, &mockConn) tlsConn, newReader, newWriter, upgraded := s.handleSTARTTLS(conn, reader, writer, &mockConn)
if upgraded { if upgraded {
// Connection was upgraded to TLS, switch to new connection // Connection was upgraded to TLS, switch to new connection
@ -242,14 +252,14 @@ func (s *MockSMTPServer) handleConnection(conn net.Conn) {
case "DATA": case "DATA":
s.handleDATA(reader, writer, &mockConn) s.handleDATA(reader, writer, &mockConn)
case "QUIT": case "QUIT":
writer.WriteString("221 Bye\r\n") _, _ = writer.WriteString("221 Bye\r\n")
writer.Flush() writer.Flush()
s.mu.Lock() s.mu.Lock()
s.Connections = append(s.Connections, mockConn) s.Connections = append(s.Connections, mockConn)
s.mu.Unlock() s.mu.Unlock()
return return
default: default:
writer.WriteString("500 Command not recognized\r\n") _, _ = writer.WriteString("500 Command not recognized\r\n")
writer.Flush() writer.Flush()
} }
} }
@ -260,18 +270,18 @@ func (s *MockSMTPServer) handleConnection(conn net.Conn) {
} }
func (s *MockSMTPServer) handleEHLO(writer *bufio.Writer) { func (s *MockSMTPServer) handleEHLO(writer *bufio.Writer) {
writer.WriteString("250-mock.smtp.server\r\n") _, _ = writer.WriteString("250-mock.smtp.server\r\n")
if s.RequireSTARTTLS { if s.RequireSTARTTLS {
writer.WriteString("250-STARTTLS\r\n") _, _ = writer.WriteString("250-STARTTLS\r\n")
} }
if s.RequireAuth { if s.RequireAuth {
if s.SupportLoginAuth { if s.SupportLoginAuth {
writer.WriteString("250-AUTH PLAIN LOGIN\r\n") _, _ = writer.WriteString("250-AUTH PLAIN LOGIN\r\n")
} else { } else {
writer.WriteString("250-AUTH PLAIN\r\n") _, _ = writer.WriteString("250-AUTH PLAIN\r\n")
} }
} }
writer.WriteString("250 SIZE 10240000\r\n") _, _ = writer.WriteString("250 SIZE 10240000\r\n")
writer.Flush() writer.Flush()
} }
@ -296,8 +306,8 @@ func (s *MockSMTPServer) handleSTARTTLS(conn net.Conn, reader *bufio.Reader, wri
} }
func (s *MockSMTPServer) handleAUTH(parts []string, reader *bufio.Reader, writer *bufio.Writer, mockConn *MockConnection) { func (s *MockSMTPServer) handleAUTH(parts []string, reader *bufio.Reader, writer *bufio.Writer, mockConn *MockConnection) {
if len(parts) < 2 { if len(parts) < minAuthParts {
writer.WriteString("501 Syntax error\r\n") _, _ = writer.WriteString("501 Syntax error\r\n")
writer.Flush() writer.Flush()
return return
} }
@ -307,54 +317,56 @@ func (s *MockSMTPServer) handleAUTH(parts []string, reader *bufio.Reader, writer
switch authType { switch authType {
case "PLAIN": case "PLAIN":
// PLAIN auth can be sent in initial command or as a response to challenge // PLAIN auth can be sent in initial command or as a response to challenge
if len(parts) > 2 { if len(parts) > minAuthParts {
// Credentials provided in initial command // Credentials provided in initial command
// authData := parts[2] // In a real implementation, we'd decode base64 and parse username/password // authData := parts[2] // In a real implementation, we'd decode base64 and parse username/password
mockConn.AuthUser = "testuser" mockConn.AuthUser = "testuser"
mockConn.AuthPass = "testpass" mockConn.AuthPass = "testpass"
writer.WriteString("235 Authentication successful\r\n") _, _ = writer.WriteString("235 Authentication successful\r\n")
writer.Flush() writer.Flush()
} else { } else {
// Challenge/response mode // Challenge/response mode
writer.WriteString("334 \r\n") _, _ = writer.WriteString("334 \r\n")
writer.Flush() writer.Flush()
authData, _ := reader.ReadString('\n') authData, _ := reader.ReadString('\n')
authData = strings.TrimSpace(authData) _ = strings.TrimSpace(authData)
// In a real implementation, we'd decode base64 and parse username/password // In a real implementation, we'd decode base64 and parse username/password
mockConn.AuthUser = "testuser" mockConn.AuthUser = "testuser"
mockConn.AuthPass = "testpass" mockConn.AuthPass = "testpass"
writer.WriteString("235 Authentication successful\r\n") _, _ = writer.WriteString("235 Authentication successful\r\n")
writer.Flush() writer.Flush()
} }
case "LOGIN": case "LOGIN":
writer.WriteString("334 VXNlcm5hbWU6\r\n") // "Username:" in base64 _, _ = writer.WriteString("334 VXNlcm5hbWU6\r\n") // "Username:" in base64
writer.Flush() writer.Flush()
username, _ := reader.ReadString('\n') username, _ := reader.ReadString('\n')
_ = username
mockConn.AuthUser = strings.TrimSpace(username) mockConn.AuthUser = strings.TrimSpace(username)
writer.WriteString("334 UGFzc3dvcmQ6\r\n") // "Password:" in base64 _, _ = writer.WriteString("334 UGFzc3dvcmQ6\r\n") // "Password:" in base64
writer.Flush() writer.Flush()
password, _ := reader.ReadString('\n') password, _ := reader.ReadString('\n')
_ = password
mockConn.AuthPass = strings.TrimSpace(password) mockConn.AuthPass = strings.TrimSpace(password)
writer.WriteString("235 Authentication successful\r\n") _, _ = writer.WriteString("235 Authentication successful\r\n")
writer.Flush() writer.Flush()
default: default:
writer.WriteString("504 Authentication mechanism not supported\r\n") _, _ = writer.WriteString("504 Authentication mechanism not supported\r\n")
writer.Flush() writer.Flush()
} }
} }
func (s *MockSMTPServer) handleMAIL(parts []string, writer *bufio.Writer, mockConn *MockConnection) { func (s *MockSMTPServer) handleMAIL(parts []string, writer *bufio.Writer, mockConn *MockConnection) {
if len(parts) < 2 { if len(parts) < minAuthParts {
writer.WriteString("501 Syntax error\r\n") _, _ = writer.WriteString("501 Syntax error\r\n")
writer.Flush() writer.Flush()
return return
} }
@ -363,13 +375,13 @@ func (s *MockSMTPServer) handleMAIL(parts []string, writer *bufio.Writer, mockCo
fromAddr = strings.Trim(fromAddr, "<>") fromAddr = strings.Trim(fromAddr, "<>")
mockConn.From = fromAddr mockConn.From = fromAddr
writer.WriteString("250 OK\r\n") _, _ = writer.WriteString("250 OK\r\n")
writer.Flush() writer.Flush()
} }
func (s *MockSMTPServer) handleRCPT(parts []string, writer *bufio.Writer, mockConn *MockConnection) { func (s *MockSMTPServer) handleRCPT(parts []string, writer *bufio.Writer, mockConn *MockConnection) {
if len(parts) < 2 { if len(parts) < minAuthParts {
writer.WriteString("501 Syntax error\r\n") _, _ = writer.WriteString("501 Syntax error\r\n")
writer.Flush() writer.Flush()
return return
} }
@ -378,12 +390,12 @@ func (s *MockSMTPServer) handleRCPT(parts []string, writer *bufio.Writer, mockCo
toAddr = strings.Trim(toAddr, "<>") toAddr = strings.Trim(toAddr, "<>")
mockConn.To = append(mockConn.To, toAddr) mockConn.To = append(mockConn.To, toAddr)
writer.WriteString("250 OK\r\n") _, _ = writer.WriteString("250 OK\r\n")
writer.Flush() writer.Flush()
} }
func (s *MockSMTPServer) handleDATA(reader *bufio.Reader, writer *bufio.Writer, mockConn *MockConnection) { func (s *MockSMTPServer) handleDATA(reader *bufio.Reader, writer *bufio.Writer, mockConn *MockConnection) {
writer.WriteString("354 Start mail input; end with <CRLF>.<CRLF>\r\n") _, _ = writer.WriteString("354 Start mail input; end with <CRLF>.<CRLF>\r\n")
writer.Flush() writer.Flush()
var dataBuilder strings.Builder var dataBuilder strings.Builder
@ -402,14 +414,14 @@ func (s *MockSMTPServer) handleDATA(reader *bufio.Reader, writer *bufio.Writer,
mockConn.Data = dataBuilder.String() mockConn.Data = dataBuilder.String()
writer.WriteString("250 OK: message accepted\r\n") _, _ = writer.WriteString("250 OK: message accepted\r\n")
writer.Flush() writer.Flush()
} }
// generateTestCert creates a self-signed certificate for testing // generateTestCert creates a self-signed certificate for testing.
func generateTestCert() (tls.Certificate, error) { func generateTestCert() (tls.Certificate, error) {
// Generate a private key // Generate a private key
priv, err := rsa.GenerateKey(rand.Reader, 2048) priv, err := rsa.GenerateKey(rand.Reader, rsaKeyBits)
if err != nil { if err != nil {
return tls.Certificate{}, err return tls.Certificate{}, err
} }
@ -429,7 +441,7 @@ func generateTestCert() (tls.Certificate, error) {
NotAfter: time.Now().Add(365 * 24 * time.Hour), NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, IPAddresses: []net.IP{net.IPv4(ipOctet127, 0, 0, 1), net.IPv6loopback},
DNSNames: []string{"localhost"}, DNSNames: []string{"localhost"},
} }

View File

@ -76,7 +76,11 @@ var mailRelayProcessor = func() backends.Decorator {
if err != nil { if err != nil {
return err return err
} }
config = bcfg.(*relayConfig) var ok bool
config, ok = bcfg.(*relayConfig)
if !ok {
return fmt.Errorf("failed to cast config to relayConfig")
}
return nil return nil
}) })
backends.Svc.AddInitializer(initFunc) backends.Svc.AddInitializer(initFunc)