mirror of
https://github.com/wiggin77/mailrelay.git
synced 2025-08-13 00:47:04 +02:00
Add linter and fix linter warnings
This commit is contained in:
70
.github/workflows/ci.yml
vendored
Normal file
70
.github/workflows/ci.yml
vendored
Normal 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 .
|
22
.github/workflows/go.yml
vendored
22
.github/workflows/go.yml
vendored
@ -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 ./...
|
267
.golangci.yml
267
.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
|
9
Makefile
9
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"
|
||||
|
17
client.go
17
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())
|
||||
}
|
||||
|
@ -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
90
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,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 {
|
||||
|
@ -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"},
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user