mirror of
https://github.com/wiggin77/mailrelay.git
synced 2025-08-13 08:57:07 +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:
|
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
|
9
Makefile
9
Makefile
@ -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"
|
||||||
|
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
|
// 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())
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
144
main.go
144
main.go
@ -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,76 +107,75 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupIPFilter(appConfig *mailRelayConfig) error {
|
||||||
|
if appConfig.AllowedSenders == "*" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(appConfig.AllowedSenders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
flag.Usage()
|
return fmt.Errorf("failed opening file: %w", err)
|
||||||
return fmt.Errorf("loading config: %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 != "*" {
|
AllowedSendersFilter = ipfilter.New(ipfilter.Options{
|
||||||
file, err := os.Open(appConfig.AllowedSenders)
|
AllowedIPs: allowedIPsAndRanges,
|
||||||
|
BlockByDefault: true,
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
return 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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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)
|
func runTest(testsender, testrcpt string, port int) error {
|
||||||
|
err := sendTest(testsender, testrcpt, port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
flag.Usage()
|
return fmt.Errorf("sending test message: %w", err)
|
||||||
return fmt.Errorf("starting server: %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 {
|
result := ""
|
||||||
err = sendTest(testsender, testrcpt, appConfig.LocalListenPort)
|
if !AllowedSendersFilter.Blocked(ipToCheck) {
|
||||||
if err != nil {
|
result = "NOT "
|
||||||
return fmt.Errorf("sending test message: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
fmt.Printf("IP address %s is %sallowed to send email\n", ipToCheck, result)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if checkIP {
|
func waitForSignal() 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")
|
|
||||||
}
|
|
||||||
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
|
|
||||||
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 {
|
||||||
|
@ -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"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
Reference in New Issue
Block a user