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:
gocritic:
enabled-tags:
- diagnostic
- experimental
- performance
- style
disabled-checks:
- dupImport # https://github.com/go-critic/go-critic/issues/845
- ifElseChain
- octalLiteral
- whyNoLint
- wrapperFunc
govet:
check-shadowing: true
# Cyclomatic complexity
cyclop:
max-complexity: 15
package-average: 10.0
skip-tests: false
# Duplicate code detection
dupl:
threshold: 100
# Function length
funlen:
lines: 80
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:
locale: US
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:
- bodyclose
- deadcode
- depguard
- dogsled
- dupl
# Default linters
- errcheck
# - funlen
- gochecknoinits
- goconst
# - gocritic
- gocyclo
- gofmt
- goimports
- golint
- gomnd
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- interfacer
- lll
- misspell
- nakedret
# - nolintlint
- rowserrcheck
- scopelint
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- varcheck
- whitespace
# don't enable:
# - asciicheck
# - gochecknoglobals
# - gocognit
# - godot
# - godox
# - goerr113
# - maligned
# - nestif
# - prealloc
# - testpackage
# - wsl
# Additional recommended linters
- asciicheck # Check for non-ASCII characters
- bodyclose # Check HTTP response body is closed
- cyclop # Cyclomatic complexity
- dupl # Duplicate code detection
- durationcheck # Duration checks
- errname # Error naming conventions
- errorlint # Error wrapping
- exhaustive # Exhaustiveness checks
- copyloopvar # Loop variable capturing (exportloopref renamed)
- funlen # Function length
- 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
./$(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
.PHONY: help
help:
@ -70,6 +78,7 @@ help:
@echo " build - Build for current architecture"
@echo " buildall - Build for all supported architectures"
@echo " test - Run tests"
@echo " check-style - Install golangci-lint and run code style checks"
@echo " clean - Remove build artifacts"
@echo " run - Build and run the application"
@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
// self-signed certificates or hostname mismatches. This should only be
// enabled in trusted network environments.
InsecureSkipVerify: config.SkipVerify, //nolint:gosec
InsecureSkipVerify: config.SkipVerify,
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 {
close(conn, "conn")
closeConn(conn, "conn")
return errors.Wrap(err, "newclient error")
}
shouldCloseClient := true
defer func(shouldClose *bool) {
if *shouldClose {
close(client, "client")
closeConn(client, "client")
}
}(&shouldCloseClient)
@ -87,7 +87,7 @@ func sendMail(e *mail.Envelope, config *relayConfig) error {
return errors.Wrap(err, "data error")
}
_, err = writer.Write(msg.Bytes())
close(writer, "writer")
closeConn(writer, "writer")
if err != nil {
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 {
auth = LoginAuth(config.Username, config.Password)
@ -131,7 +131,7 @@ func handshake(client *smtp.Client, config *relayConfig, tlsConfig *tls.Config)
return nil
}
func close(c closeable, what string) {
func closeConn(c closeable, what string) {
err := c.Close()
if err != nil {
fmt.Printf("Error closing %s: %v\n", what, err)
@ -142,7 +142,8 @@ func isQuitError(err error) bool {
if err == nil {
return false
}
e, ok := err.(*textproto.Error)
var e *textproto.Error
ok := errors.As(err, &e)
if ok {
// SMTP codes 221 or 250 are acceptable here
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.
func getTo(e *mail.Envelope) []string {
var ret []string
ret := make([]string, 0, len(e.RcptTo))
for i := range e.RcptTo {
ret = append(ret, e.RcptTo[i].String())
}

View File

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

90
main.go
View File

@ -17,16 +17,17 @@ import (
const (
DefaultSMTPPort = 465
DefaultMaxEmailSize = (10 << 23) // 83 MB
DefaultMaxEmailSize = 83886080 // 80 MB (80 * 1024 * 1024)
DefaultLocalListenIP = "0.0.0.0"
DefaultLocalListenPort = 2525
DefaultTimeoutSecs = 300 // 5 minutes
MinEmailSizeBytes = 1024
)
// Logger is the global logger
// Logger is the global 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{})
type mailRelayConfig struct {
@ -56,6 +57,39 @@ func main() {
}
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 test bool
var testsender string
@ -63,6 +97,7 @@ func run() error {
var checkIP bool
var ipToCheck string
var verbose bool
flag.StringVar(&configFile, "config", "/etc/mailrelay.json", "specifies JSON config file")
flag.BoolVar(&test, "test", false, "sends a test message to SMTP server")
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.Parse()
appConfig, err := loadConfig(configFile)
if err != nil {
flag.Usage()
return fmt.Errorf("loading config: %w", err)
return configFile, test, testsender, testrcpt, checkIP, ipToCheck, verbose
}
func setupIPFilter(appConfig *mailRelayConfig) error {
if appConfig.AllowedSenders == "*" {
return nil
}
if appConfig.AllowedSenders != "*" {
file, err := os.Open(appConfig.AllowedSenders)
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.Split(bufio.ScanLines)
@ -93,42 +129,42 @@ func run() error {
allowedIPsAndRanges = append(allowedIPsAndRanges, scanner.Text())
}
file.Close()
AllowedSendersFilter = ipfilter.New(ipfilter.Options{
//AllowedIPs: []string{"192.168.0.0/24"},
AllowedIPs: allowedIPsAndRanges,
BlockByDefault: true,
})
return nil
}
func setupLogger(verbose bool) error {
logLevel := "info"
if verbose {
logLevel = "debug"
}
var err error
Logger, err = log.GetLogger("stdout", logLevel)
if err != nil {
return fmt.Errorf("creating logger: %w", err)
}
err = Start(appConfig, verbose)
if err != nil {
flag.Usage()
return fmt.Errorf("starting server: %w", err)
return nil
}
if test {
err = sendTest(testsender, testrcpt, appConfig.LocalListenPort)
func runTest(testsender, testrcpt string, port int) error {
err := sendTest(testsender, testrcpt, port)
if err != nil {
return fmt.Errorf("sending test message: %w", err)
}
return nil
}
if checkIP {
func runIPCheck(ipToCheck string) error {
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 := ""
if !AllowedSendersFilter.Blocked(ipToCheck) {
result = "NOT "
@ -137,11 +173,9 @@ func run() error {
return nil
}
// Wait for SIGINT
func waitForSignal() error {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
// Block until a signal is received.
<-c
return nil
}
@ -181,7 +215,7 @@ func configDefaults(config *mailRelayConfig) {
config.TimeoutSecs = DefaultTimeoutSecs
}
// validateConfig validates the configuration values
// validateConfig validates the configuration values.
func validateConfig(config *mailRelayConfig) error {
if config.SMTPServer == "" {
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")
}
if config.MaxEmailSize < 1024 {
if config.MaxEmailSize < MinEmailSizeBytes {
return errors.New("smtp_max_email_size must be at least 1024 bytes")
}
@ -206,7 +240,7 @@ func validateConfig(config *mailRelayConfig) error {
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 {
conn, err := smtp.Dial(fmt.Sprintf("localhost:%d", port))
if err != nil {

View File

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

View File

@ -76,7 +76,11 @@ var mailRelayProcessor = func() backends.Decorator {
if err != nil {
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
})
backends.Svc.AddInitializer(initFunc)