From 2e10ada37b78a971ce7ff8ec947ff93df82116c0 Mon Sep 17 00:00:00 2001 From: wiggin77 Date: Sat, 24 May 2025 23:46:23 -0400 Subject: [PATCH] Add linter and fix linter warnings --- .github/workflows/ci.yml | 70 ++++++++++ .github/workflows/go.yml | 22 ---- .golangci.yml | 267 ++++++++++++++++++++++++++++++--------- Makefile | 9 ++ client.go | 17 +-- integration_test.go | 2 +- main.go | 144 +++++++++++++-------- mock_smtp_server.go | 108 +++++++++------- server.go | 6 +- 9 files changed, 452 insertions(+), 193 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/go.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..689b696 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 . \ No newline at end of file diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml deleted file mode 100644 index 9a6d602..0000000 --- a/.github/workflows/go.yml +++ /dev/null @@ -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 ./... diff --git a/.golangci.yml b/.golangci.yml index 3b551bb..623076d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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 \ No newline at end of file diff --git a/Makefile b/Makefile index 8bfc7a9..c472632 100644 --- a/Makefile +++ b/Makefile @@ -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" diff --git a/client.go b/client.go index d8dac75..ebbb16e 100644 --- a/client.go +++ b/client.go @@ -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()) } diff --git a/integration_test.go b/integration_test.go index 03d4463..1d4e428 100644 --- a/integration_test.go +++ b/integration_test.go @@ -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") diff --git a/main.go b/main.go index e588865..caa0959 100644 --- a/main.go +++ b/main.go @@ -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,76 +107,75 @@ func run() error { flag.StringVar(&ipToCheck, "ip", "", "used with 'checkIP' to specify IP address to test") flag.Parse() - appConfig, err := loadConfig(configFile) + return configFile, test, testsender, testrcpt, checkIP, ipToCheck, verbose +} + +func setupIPFilter(appConfig *mailRelayConfig) error { + if appConfig.AllowedSenders == "*" { + return nil + } + + file, err := os.Open(appConfig.AllowedSenders) if err != nil { - flag.Usage() - return fmt.Errorf("loading config: %w", err) + return fmt.Errorf("failed opening file: %w", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + var allowedIPsAndRanges []string + + for scanner.Scan() { + allowedIPsAndRanges = append(allowedIPsAndRanges, scanner.Text()) } - if appConfig.AllowedSenders != "*" { - file, err := os.Open(appConfig.AllowedSenders) + AllowedSendersFilter = ipfilter.New(ipfilter.Options{ + AllowedIPs: allowedIPsAndRanges, + BlockByDefault: true, + }) - if err != nil { - return fmt.Errorf("failed opening file: %s", err) - } - - scanner := bufio.NewScanner(file) - scanner.Split(bufio.ScanLines) - var allowedIPsAndRanges []string - - for scanner.Scan() { - 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) } + return nil +} - err = Start(appConfig, verbose) +func runTest(testsender, testrcpt string, port int) error { + err := sendTest(testsender, testrcpt, port) if err != nil { - flag.Usage() - return fmt.Errorf("starting server: %w", err) + return fmt.Errorf("sending test message: %w", err) + } + return nil +} + +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") } - if test { - err = sendTest(testsender, testrcpt, appConfig.LocalListenPort) - if err != nil { - return fmt.Errorf("sending test message: %w", err) - } - return nil + result := "" + if !AllowedSendersFilter.Blocked(ipToCheck) { + result = "NOT " } + fmt.Printf("IP address %s is %sallowed to send email\n", ipToCheck, result) + return nil +} - if checkIP { - if ipToCheck == "" { - 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 " - } - fmt.Printf("IP address %s is %sallowed to send email\n", ipToCheck, result) - 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 { diff --git a/mock_smtp_server.go b/mock_smtp_server.go index 35e3db8..588f1b8 100644 --- a/mock_smtp_server.go +++ b/mock_smtp_server.go @@ -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 .\r\n") + _, _ = writer.WriteString("354 Start mail input; end with .\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"}, } diff --git a/server.go b/server.go index b03a7cc..d9f6ff6 100644 --- a/server.go +++ b/server.go @@ -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)