├── .dockerignore ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── test.yml │ └── build.yaml ├── docker-compose.yml ├── Dockerfile ├── cmd └── mailcatcher │ ├── main.go │ ├── config.go │ ├── server.go │ └── config_test.go ├── tests └── e2e │ ├── test_config.go │ ├── test_helpers.go │ ├── mailpit_client.go │ └── mailcatcher_full_test.go ├── README.md ├── Makefile ├── go.mod ├── LICENSE └── go.sum /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | vendor/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .idea/ 3 | 4 | # Package Files # 5 | *.war 6 | *.ear 7 | *.zip 8 | *.tar.gz 9 | *.rar 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | assignees: 10 | - 0xERR0R 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ main, master ] 6 | pull_request: 7 | branches: [ main, master ] 8 | 9 | jobs: 10 | tests: 11 | name: Tests 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v4 19 | with: 20 | go-version-file: go.mod 21 | 22 | 23 | - name: Run tests 24 | run: make test 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | services: 3 | mailcatcher: 4 | image: ghcr.io/0xerr0r/mailcatcher 5 | restart: always 6 | container_name: mailcatcher 7 | ports: 8 | - 1025:1025 9 | environment: 10 | - MC_PORT=1025 11 | - MC_HOST=xxx.dyndns.org 12 | - MC_REDIRECT_TO=xxx@gmail.com 13 | - MC_SENDER_MAIL=yyy@googlemail.com 14 | - MC_SMTP_HOST=smtp.gmail.com 15 | - MC_SMTP_PORT=587 16 | - MC_SMTP_USER=yyy@googlemail.com 17 | - MC_SMTP_PASSWORD=yyy123 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | 3 | RUN apk --no-cache add gcc musl-dev 4 | 5 | WORKDIR ${GOPATH}/src/github.com/0xERR0R/mailcatcher 6 | COPY go.mod go.sum ./ 7 | RUN go mod download 8 | 9 | COPY . . 10 | RUN go build -o /go/bin/mailcatcher cmd/mailcatcher/*.go 11 | 12 | FROM alpine 13 | 14 | LABEL org.opencontainers.image.source="https://github.com/0xERR0R/mailcatcher" \ 15 | org.opencontainers.image.url="https://github.com/0xERR0R/mailcatcher" \ 16 | org.opencontainers.image.title="Self hosted mail trash service" 17 | 18 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 19 | COPY --from=builder /go/bin/mailcatcher /app 20 | ENTRYPOINT ["/app"] 21 | -------------------------------------------------------------------------------- /cmd/mailcatcher/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/tkanos/gonfig" 7 | "os" 8 | ) 9 | 10 | func main() { 11 | 12 | var configPath string 13 | if len(os.Args) > 1 { 14 | configPath = os.Args[1] 15 | } 16 | configuration := Configuration{} 17 | err := gonfig.GetConf(configPath, &configuration) 18 | if err != nil { 19 | log.Fatal("can't read configuration: ", err) 20 | } 21 | 22 | if err := configuration.Validate(); err != nil { 23 | 24 | log.Fatal("please check the configuration") 25 | } 26 | 27 | log.Println("Using configuration:", configuration) 28 | 29 | err1 := NewServer(&configuration) 30 | if err1 != nil { 31 | log.Fatal("can't start server: ", err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | docker: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | - name: Set up QEMU 14 | uses: docker/setup-qemu-action@v1 15 | - name: Set up Docker Buildx 16 | uses: docker/setup-buildx-action@v1 17 | - name: Login to GitHub Container Registry 18 | uses: docker/login-action@v1 19 | with: 20 | registry: ghcr.io 21 | username: ${{ github.repository_owner }} 22 | password: ${{ secrets.CR_PAT }} 23 | - name: Build and push 24 | uses: docker/build-push-action@v2 25 | with: 26 | platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 27 | push: true 28 | tags: ghcr.io/0xerr0r/mailcatcher:latest 29 | -------------------------------------------------------------------------------- /tests/e2e/test_config.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | ) 7 | 8 | type TestConfig struct { 9 | MailpitImage string 10 | MailcatcherImage string 11 | TestDomain string 12 | TestTimeout int 13 | } 14 | 15 | func LoadTestConfig() *TestConfig { 16 | config := &TestConfig{ 17 | MailpitImage: getEnvOrDefault("MAILPIT_IMAGE", "axllent/mailpit:latest"), 18 | MailcatcherImage: getEnvOrDefault("MAILCATCHER_IMAGE", ""), // Use local build if empty 19 | TestDomain: getEnvOrDefault("TEST_DOMAIN", "test.example.com"), 20 | TestTimeout: getEnvIntOrDefault("TEST_TIMEOUT", 30), 21 | } 22 | return config 23 | } 24 | 25 | func getEnvOrDefault(key, defaultValue string) string { 26 | if value := os.Getenv(key); value != "" { 27 | return value 28 | } 29 | return defaultValue 30 | } 31 | 32 | func getEnvIntOrDefault(key string, defaultValue int) int { 33 | if value := os.Getenv(key); value != "" { 34 | if intValue, err := strconv.Atoi(value); err == nil { 35 | return intValue 36 | } 37 | } 38 | return defaultValue 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build](https://github.com/0xERR0R/mailcatcher/workflows/Build/badge.svg) 2 | 3 | # mailcatcher 4 | 5 | #### *mailcatcher* is a small self hosted SMTP server which catches all incoming mails and sends them to a defined mail address. Can be used with dyndns to create own addresses for trash mails. Works fine on Raspberry PI 3! 6 | 7 | 8 | ## Installation with docker 9 | * copy docker-compose.yml 10 | * change variables (see bellow) 11 | * run with "docker-compose up -d" 12 | * configure port forwarding for internet port 25 in your router (for example map internet port 25 to your Raspberry PI's port 1025) 13 | 14 | #### Variables: 15 | | Name | Description | 16 | | ---- |------ | 17 | | MC_PORT | SMTP listening port. Must match mapped port of container. | 18 | | MC_HOST | email with this host name will be accepted (typically your dyndns host name) | 19 | | MC_REDIRECT_TO | destination address (all mails will be redirected to this address | 20 | | MC_SENDER_MAIL | This address will be used for mail sending | 21 | | MC_SMTP_HOST | use this SMTP server | 22 | | MC_SMTP_PORT | with SMTP Port | 23 | | MC_SMTP_USER | Authentication for SMTP server | 24 | | MC_SMTP_PASSWORD| Authentication for SMTP server | 25 | 26 | 27 | ## Hints 28 | 29 | If you are using GMail SMTP, please activate the usage of insecure apps on https://support.google.com/accounts/answer/6010255?hl=en . 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build test test-e2e clean docker-build docker-run 2 | 3 | # Build the mailcatcher binary 4 | build: 5 | go build -o bin/mailcatcher ./cmd/mailcatcher 6 | 7 | 8 | # Run tests 9 | test: 10 | docker build -t mailcatcher:test . 11 | go test ./... 12 | go test -v ./tests/e2e/... -timeout 5m 13 | 14 | 15 | # Clean build artifacts 16 | clean: 17 | rm -rf bin/ 18 | go clean -cache 19 | 20 | # Build Docker image 21 | docker-build: 22 | docker build -t mailcatcher:latest . 23 | 24 | # Run mailcatcher with Docker Compose 25 | docker-run: 26 | docker-compose up -d 27 | 28 | # Stop mailcatcher with Docker Compose 29 | docker-stop: 30 | docker-compose down 31 | 32 | # Show logs 33 | logs: 34 | docker-compose logs -f 35 | 36 | # Install dependencies 37 | deps: 38 | go mod download 39 | go mod tidy 40 | 41 | # Format code 42 | fmt: 43 | go fmt ./... 44 | 45 | # Lint code 46 | lint: 47 | golangci-lint run 48 | 49 | 50 | # Help target 51 | help: 52 | @echo "Available targets:" 53 | @echo " build - Build the mailcatcher binary" 54 | @echo " test - Run unit tests" 55 | @echo " clean - Clean build artifacts" 56 | @echo " docker-build - Build Docker image" 57 | @echo " docker-run - Run with Docker Compose" 58 | @echo " docker-stop - Stop Docker Compose" 59 | @echo " logs - Show logs" 60 | @echo " deps - Install dependencies" 61 | @echo " fmt - Format code" 62 | @echo " lint - Lint code" 63 | @echo " help - Show this help" 64 | -------------------------------------------------------------------------------- /cmd/mailcatcher/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/go-playground/locales/en" 8 | ut "github.com/go-playground/universal-translator" 9 | "gopkg.in/go-playground/validator.v9" 10 | en_translations "gopkg.in/go-playground/validator.v9/translations/en" 11 | ) 12 | 13 | type Configuration struct { 14 | MC_PORT int `validate:"required,gte=0,lte=65535"` 15 | MC_HOST string `validate:"required,hostname"` 16 | MC_REDIRECT_TO string `validate:"required,email"` 17 | MC_SENDER_MAIL string `validate:"required,email"` 18 | MC_SMTP_HOST string `validate:"required,hostname"` 19 | MC_SMTP_PORT int `validate:"required,gte=0,lte=65535"` 20 | MC_SMTP_USER string `validate:"omitempty"` 21 | MC_SMTP_PASSWORD string `validate:"omitempty"` 22 | } 23 | 24 | func (c *Configuration) Validate() error { 25 | 26 | en := en.New() 27 | uni := ut.New(en, en) 28 | 29 | trans, _ := uni.GetTranslator("en") 30 | 31 | validate := validator.New() 32 | en_translations.RegisterDefaultTranslations(validate, trans) 33 | 34 | err := validate.Struct(c) 35 | if err != nil { 36 | errs := err.(validator.ValidationErrors) 37 | 38 | for _, e := range errs { 39 | log.Printf("configuration error: %s", e.Translate(trans)) 40 | } 41 | } 42 | return err 43 | } 44 | 45 | func (c Configuration) String() string { 46 | return fmt.Sprintf(` 47 | MC_PORT: %d 48 | MC_HOST: %s 49 | MC_REDIRECT_TO: %s 50 | MC_SENDER_MAIL: %s 51 | MC_SMTP_HOST: %s 52 | MC_SMTP_PORT: %d 53 | MC_SMTP_USER: %s`, 54 | c.MC_PORT, c.MC_HOST, c.MC_REDIRECT_TO, c.MC_SENDER_MAIL, c.MC_SMTP_HOST, c.MC_SMTP_PORT, c.MC_SMTP_USER) 55 | } 56 | -------------------------------------------------------------------------------- /cmd/mailcatcher/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/smtp" 7 | "strconv" 8 | "strings" 9 | 10 | "fmt" 11 | 12 | gosmtp "github.com/emersion/go-smtp" 13 | "github.com/veqryn/go-email/email" 14 | ) 15 | 16 | var config *Configuration 17 | 18 | type Backend struct{} 19 | 20 | func (bkd *Backend) NewSession(c *gosmtp.Conn) (gosmtp.Session, error) { 21 | return &Session{ 22 | to: make([]string, 0), 23 | }, nil 24 | } 25 | 26 | type Session struct { 27 | from string 28 | to []string 29 | } 30 | 31 | func (s *Session) Mail(from string, opts *gosmtp.MailOptions) error { 32 | s.from = from 33 | return nil 34 | } 35 | 36 | func (s *Session) Rcpt(to string, opts *gosmtp.RcptOptions) error { 37 | s.to = append(s.to, to) 38 | return nil 39 | } 40 | 41 | func (s *Session) Data(r io.Reader) error { 42 | log.Printf("New message from '%s' to '%s' received", s.from, s.to) 43 | if isRecipientValid(s.to) { 44 | if msg, err := email.ParseMessage(r); err != nil { 45 | log.Fatal("error", err) 46 | return err 47 | } else { 48 | msg.Header.SetSubject(fmt.Sprintf("[MAILCATCHER] %s", msg.Header.Subject())) 49 | msg.Header.SetTo(fmt.Sprintf("\"%s\" <%s>", msg.Header.To()[0], config.MC_REDIRECT_TO)) 50 | msg.Header.SetFrom(fmt.Sprintf("\"%s\" <%s>", "MAILCATCHER", config.MC_SENDER_MAIL)) 51 | 52 | sendMail(msg) 53 | 54 | if err != nil { 55 | log.Printf("smtp error: %s", err) 56 | } 57 | } 58 | } else { 59 | log.Print("ignoring message") 60 | } 61 | return nil 62 | } 63 | 64 | func (s *Session) Reset() {} 65 | 66 | func (s *Session) Logout() error { 67 | return nil 68 | } 69 | 70 | func isRecipientValid(recipients []string) bool { 71 | for _, recipient := range recipients { 72 | if strings.HasSuffix(recipient, config.MC_HOST) { 73 | return true 74 | } 75 | } 76 | return false 77 | } 78 | 79 | func sendMail(msg *email.Message) { 80 | if err := msg.Save(); err != nil { 81 | log.Printf("can't save message: %s", err) 82 | return 83 | } 84 | b, err := msg.Bytes() 85 | if err != nil { 86 | log.Printf("can't convert message: %s", err) 87 | return 88 | } 89 | 90 | var auth smtp.Auth 91 | if config.MC_SMTP_USER != "" && config.MC_SMTP_PASSWORD != "" { 92 | auth = smtp.PlainAuth("", config.MC_SMTP_USER, config.MC_SMTP_PASSWORD, config.MC_SMTP_HOST) 93 | } 94 | 95 | err = smtp.SendMail(fmt.Sprintf("%s:%d", config.MC_SMTP_HOST, config.MC_SMTP_PORT), 96 | auth, 97 | config.MC_SENDER_MAIL, []string{config.MC_REDIRECT_TO}, b) 98 | 99 | if err != nil { 100 | log.Printf("smtp error: %s", err) 101 | return 102 | } 103 | } 104 | 105 | func NewServer(configuration *Configuration) error { 106 | config = configuration 107 | be := &Backend{} 108 | 109 | s := gosmtp.NewServer(be) 110 | 111 | s.Addr = ":" + strconv.Itoa(config.MC_PORT) 112 | s.Domain = config.MC_HOST 113 | s.MaxMessageBytes = 1024 * 1024 * 20 114 | s.MaxRecipients = 50 115 | s.AllowInsecureAuth = true 116 | 117 | log.Println("Starting server at", s.Addr) 118 | return s.ListenAndServe() 119 | } 120 | -------------------------------------------------------------------------------- /tests/e2e/test_helpers.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/testcontainers/testcontainers-go" 10 | "github.com/testcontainers/testcontainers-go/wait" 11 | "gopkg.in/gomail.v2" 12 | ) 13 | 14 | // WaitForContainerHealth waits for a container to be healthy 15 | func WaitForContainerHealth(container testcontainers.Container, timeout time.Duration) error { 16 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 17 | defer cancel() 18 | 19 | for { 20 | select { 21 | case <-ctx.Done(): 22 | return fmt.Errorf("timeout waiting for container health") 23 | default: 24 | state, err := container.State(ctx) 25 | if err != nil { 26 | log.Printf("Error getting container state: %v", err) 27 | time.Sleep(1 * time.Second) 28 | continue 29 | } 30 | 31 | if state.Running { 32 | return nil 33 | } 34 | 35 | time.Sleep(1 * time.Second) 36 | } 37 | } 38 | } 39 | 40 | // CreateMailpitContainer creates and starts a mailpit container 41 | func CreateMailpitContainer(ctx context.Context, image string) (testcontainers.Container, error) { 42 | req := testcontainers.ContainerRequest{ 43 | Image: image, 44 | ExposedPorts: []string{"1025/tcp", "8025/tcp"}, 45 | WaitingFor: wait.ForLog("accessible via http://localhost:8025/"), 46 | } 47 | 48 | container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 49 | ContainerRequest: req, 50 | Started: true, 51 | }) 52 | 53 | return container, err 54 | } 55 | 56 | // CreateMailcatcherContainer creates and starts a mailcatcher container 57 | func CreateMailcatcherContainer(ctx context.Context, dockerfilePath string, env map[string]string) (testcontainers.Container, error) { 58 | req := testcontainers.ContainerRequest{ 59 | FromDockerfile: testcontainers.FromDockerfile{ 60 | Context: dockerfilePath, 61 | Dockerfile: "Dockerfile", 62 | }, 63 | ExposedPorts: []string{"1025/tcp"}, 64 | Env: env, 65 | WaitingFor: wait.ForLog("Starting server at"), 66 | } 67 | 68 | container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 69 | ContainerRequest: req, 70 | Started: true, 71 | }) 72 | 73 | return container, err 74 | } 75 | 76 | // CleanupContainer safely terminates a container 77 | func CleanupContainer(container testcontainers.Container) { 78 | if container != nil { 79 | ctx := context.Background() 80 | if err := container.Terminate(ctx); err != nil { 81 | log.Printf("Error terminating container: %v", err) 82 | } 83 | } 84 | } 85 | 86 | // RetryWithTimeout retries a function until it succeeds or timeout is reached 87 | func RetryWithTimeout(operation func() error, timeout time.Duration, interval time.Duration) error { 88 | deadline := time.Now().Add(timeout) 89 | 90 | for time.Now().Before(deadline) { 91 | if err := operation(); err == nil { 92 | return nil 93 | } 94 | time.Sleep(interval) 95 | } 96 | 97 | return fmt.Errorf("operation failed after %v", timeout) 98 | } 99 | 100 | // sendTestEmail sends a test email to the specified host and port 101 | func sendTestEmail(host string, port int, to string) error { 102 | m := gomail.NewMessage() 103 | m.SetHeader("From", "sender@example.com") 104 | m.SetHeader("To", to) 105 | m.SetHeader("Subject", "Test Email") 106 | m.SetBody("text/plain", "This is a test email body") 107 | 108 | d := gomail.NewDialer(host, port, "", "") 109 | return d.DialAndSend(m) 110 | } 111 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module mailcatcher 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | github.com/emersion/go-smtp v0.24.0 7 | github.com/go-playground/locales v0.14.1 8 | github.com/go-playground/universal-translator v0.18.1 9 | github.com/stretchr/testify v1.11.1 10 | github.com/testcontainers/testcontainers-go v0.40.0 11 | github.com/tkanos/gonfig v0.0.0-20210106201359-53e13348de2f 12 | github.com/veqryn/go-email v0.0.0-20240310084539-bc822271db61 13 | gopkg.in/go-playground/validator.v9 v9.31.0 14 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df 15 | ) 16 | 17 | require ( 18 | dario.cat/mergo v1.0.2 // indirect 19 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect 20 | github.com/Microsoft/go-winio v0.6.2 // indirect 21 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 22 | github.com/containerd/errdefs v1.0.0 // indirect 23 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 24 | github.com/containerd/log v0.1.0 // indirect 25 | github.com/containerd/platforms v0.2.1 // indirect 26 | github.com/cpuguy83/dockercfg v0.3.2 // indirect 27 | github.com/davecgh/go-spew v1.1.1 // indirect 28 | github.com/distribution/reference v0.6.0 // indirect 29 | github.com/docker/docker v28.5.1+incompatible // indirect 30 | github.com/docker/go-connections v0.6.0 // indirect 31 | github.com/docker/go-units v0.5.0 // indirect 32 | github.com/ebitengine/purego v0.8.4 // indirect 33 | github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect 34 | github.com/felixge/httpsnoop v1.0.4 // indirect 35 | github.com/ghodss/yaml v1.0.0 // indirect 36 | github.com/go-logr/logr v1.4.3 // indirect 37 | github.com/go-logr/stdr v1.2.2 // indirect 38 | github.com/go-ole/go-ole v1.3.0 // indirect 39 | github.com/google/uuid v1.6.0 // indirect 40 | github.com/klauspost/compress v1.18.0 // indirect 41 | github.com/leodido/go-urn v1.4.0 // indirect 42 | github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect 43 | github.com/magiconair/properties v1.8.10 // indirect 44 | github.com/moby/docker-image-spec v1.3.1 // indirect 45 | github.com/moby/go-archive v0.1.0 // indirect 46 | github.com/moby/patternmatcher v0.6.0 // indirect 47 | github.com/moby/sys/sequential v0.6.0 // indirect 48 | github.com/moby/sys/user v0.4.0 // indirect 49 | github.com/moby/sys/userns v0.1.0 // indirect 50 | github.com/moby/term v0.5.2 // indirect 51 | github.com/morikuni/aec v1.0.0 // indirect 52 | github.com/opencontainers/go-digest v1.0.0 // indirect 53 | github.com/opencontainers/image-spec v1.1.1 // indirect 54 | github.com/pkg/errors v0.9.1 // indirect 55 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 56 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 57 | github.com/shirou/gopsutil/v4 v4.25.7 // indirect 58 | github.com/sirupsen/logrus v1.9.3 // indirect 59 | github.com/tklauser/go-sysconf v0.3.15 // indirect 60 | github.com/tklauser/numcpus v0.10.0 // indirect 61 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 62 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 63 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect 64 | go.opentelemetry.io/otel v1.37.0 // indirect 65 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect 66 | go.opentelemetry.io/otel/metric v1.37.0 // indirect 67 | go.opentelemetry.io/otel/trace v1.37.0 // indirect 68 | go.opentelemetry.io/proto/otlp v1.0.0 // indirect 69 | golang.org/x/crypto v0.45.0 // indirect 70 | golang.org/x/sys v0.38.0 // indirect 71 | golang.org/x/time v0.12.0 // indirect 72 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect 73 | google.golang.org/grpc v1.74.2 // indirect 74 | google.golang.org/protobuf v1.36.7 // indirect 75 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 76 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 77 | gopkg.in/yaml.v2 v2.4.0 // indirect 78 | gopkg.in/yaml.v3 v3.0.1 // indirect 79 | ) 80 | -------------------------------------------------------------------------------- /tests/e2e/mailpit_client.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | type MailpitClient struct { 12 | baseURL string 13 | client *http.Client 14 | } 15 | 16 | type MailpitEmail struct { 17 | ID string `json:"ID"` 18 | From MailpitAddress `json:"From"` 19 | To []MailpitAddress `json:"To"` 20 | Subject string `json:"Subject"` 21 | Text string `json:"Text"` 22 | HTML string `json:"HTML"` 23 | Created time.Time `json:"Created"` 24 | } 25 | 26 | type MailpitResponse struct { 27 | Total int `json:"total"` 28 | Unread int `json:"unread"` 29 | Count int `json:"count"` 30 | MessagesCount int `json:"messages_count"` 31 | MessagesUnread int `json:"messages_unread"` 32 | Start int `json:"start"` 33 | Tags []string `json:"tags"` 34 | Messages []MailpitEmail `json:"messages"` 35 | } 36 | 37 | type MailpitAddress struct { 38 | Name string `json:"Name"` 39 | Address string `json:"Address"` 40 | } 41 | 42 | func NewMailpitClient(host string, port int) *MailpitClient { 43 | return &MailpitClient{ 44 | baseURL: fmt.Sprintf("http://%s:%d", host, port), 45 | client: &http.Client{ 46 | Timeout: 10 * time.Second, 47 | }, 48 | } 49 | } 50 | 51 | func (c *MailpitClient) GetMessages() ([]MailpitEmail, error) { 52 | resp, err := c.client.Get(fmt.Sprintf("%s/api/v1/messages", c.baseURL)) 53 | if err != nil { 54 | return nil, fmt.Errorf("failed to get messages: %w", err) 55 | } 56 | defer resp.Body.Close() 57 | 58 | if resp.StatusCode != http.StatusOK { 59 | return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) 60 | } 61 | 62 | body, err := io.ReadAll(resp.Body) 63 | if err != nil { 64 | return nil, fmt.Errorf("failed to read response body: %w", err) 65 | } 66 | 67 | var response MailpitResponse 68 | if err := json.Unmarshal(body, &response); err != nil { 69 | return nil, fmt.Errorf("failed to unmarshal response: %w", err) 70 | } 71 | 72 | return response.Messages, nil 73 | } 74 | 75 | func (c *MailpitClient) GetMessage(id string) (*MailpitEmail, error) { 76 | resp, err := c.client.Get(fmt.Sprintf("%s/api/v1/message/%s", c.baseURL, id)) 77 | if err != nil { 78 | return nil, fmt.Errorf("failed to get message: %w", err) 79 | } 80 | defer resp.Body.Close() 81 | 82 | if resp.StatusCode != http.StatusOK { 83 | return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) 84 | } 85 | 86 | body, err := io.ReadAll(resp.Body) 87 | if err != nil { 88 | return nil, fmt.Errorf("failed to read response body: %w", err) 89 | } 90 | 91 | var message MailpitEmail 92 | if err := json.Unmarshal(body, &message); err != nil { 93 | return nil, fmt.Errorf("failed to unmarshal message: %w", err) 94 | } 95 | 96 | return &message, nil 97 | } 98 | 99 | func (c *MailpitClient) DeleteAllMessages() error { 100 | req, err := http.NewRequest("DELETE", fmt.Sprintf("%s/api/v1/messages", c.baseURL), nil) 101 | if err != nil { 102 | return fmt.Errorf("failed to create delete request: %w", err) 103 | } 104 | 105 | resp, err := c.client.Do(req) 106 | if err != nil { 107 | return fmt.Errorf("failed to delete messages: %w", err) 108 | } 109 | defer resp.Body.Close() 110 | 111 | if resp.StatusCode != http.StatusOK { 112 | return fmt.Errorf("unexpected status code: %d", resp.StatusCode) 113 | } 114 | 115 | return nil 116 | } 117 | 118 | func (c *MailpitClient) WaitForMessages(expectedCount int, timeout time.Duration) ([]MailpitEmail, error) { 119 | deadline := time.Now().Add(timeout) 120 | 121 | for time.Now().Before(deadline) { 122 | messages, err := c.GetMessages() 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | if len(messages) >= expectedCount { 128 | return messages, nil 129 | } 130 | 131 | time.Sleep(500 * time.Millisecond) 132 | } 133 | 134 | return nil, fmt.Errorf("timeout waiting for %d messages", expectedCount) 135 | } 136 | -------------------------------------------------------------------------------- /cmd/mailcatcher/config_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestConfiguration_Validate(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | config Configuration 13 | expectError bool 14 | }{ 15 | { 16 | name: "valid configuration", 17 | config: Configuration{ 18 | MC_PORT: 1025, 19 | MC_HOST: "test.example.com", 20 | MC_REDIRECT_TO: "test@example.com", 21 | MC_SENDER_MAIL: "sender@example.com", 22 | MC_SMTP_HOST: "smtp.example.com", 23 | MC_SMTP_PORT: 587, 24 | MC_SMTP_USER: "user", 25 | MC_SMTP_PASSWORD: "password", 26 | }, 27 | expectError: false, 28 | }, 29 | { 30 | name: "invalid port - negative", 31 | config: Configuration{ 32 | MC_PORT: -1, 33 | MC_HOST: "test.example.com", 34 | MC_REDIRECT_TO: "test@example.com", 35 | MC_SENDER_MAIL: "sender@example.com", 36 | MC_SMTP_HOST: "smtp.example.com", 37 | MC_SMTP_PORT: 587, 38 | MC_SMTP_USER: "user", 39 | MC_SMTP_PASSWORD: "password", 40 | }, 41 | expectError: true, 42 | }, 43 | { 44 | name: "invalid port - too high", 45 | config: Configuration{ 46 | MC_PORT: 70000, 47 | MC_HOST: "test.example.com", 48 | MC_REDIRECT_TO: "test@example.com", 49 | MC_SENDER_MAIL: "sender@example.com", 50 | MC_SMTP_HOST: "smtp.example.com", 51 | MC_SMTP_PORT: 587, 52 | MC_SMTP_USER: "user", 53 | MC_SMTP_PASSWORD: "password", 54 | }, 55 | expectError: true, 56 | }, 57 | { 58 | name: "invalid hostname", 59 | config: Configuration{ 60 | MC_PORT: 1025, 61 | MC_HOST: "invalid hostname with spaces", 62 | MC_REDIRECT_TO: "test@example.com", 63 | MC_SENDER_MAIL: "sender@example.com", 64 | MC_SMTP_HOST: "smtp.example.com", 65 | MC_SMTP_PORT: 587, 66 | MC_SMTP_USER: "user", 67 | MC_SMTP_PASSWORD: "password", 68 | }, 69 | expectError: true, 70 | }, 71 | { 72 | name: "invalid email - redirect_to", 73 | config: Configuration{ 74 | MC_PORT: 1025, 75 | MC_HOST: "test.example.com", 76 | MC_REDIRECT_TO: "invalid-email", 77 | MC_SENDER_MAIL: "sender@example.com", 78 | MC_SMTP_HOST: "smtp.example.com", 79 | MC_SMTP_PORT: 587, 80 | MC_SMTP_USER: "user", 81 | MC_SMTP_PASSWORD: "password", 82 | }, 83 | expectError: true, 84 | }, 85 | { 86 | name: "invalid email - sender_mail", 87 | config: Configuration{ 88 | MC_PORT: 1025, 89 | MC_HOST: "test.example.com", 90 | MC_REDIRECT_TO: "test@example.com", 91 | MC_SENDER_MAIL: "invalid-email", 92 | MC_SMTP_HOST: "smtp.example.com", 93 | MC_SMTP_PORT: 587, 94 | MC_SMTP_USER: "user", 95 | MC_SMTP_PASSWORD: "password", 96 | }, 97 | expectError: true, 98 | }, 99 | } 100 | 101 | for _, tt := range tests { 102 | t.Run(tt.name, func(t *testing.T) { 103 | err := tt.config.Validate() 104 | if tt.expectError { 105 | assert.Error(t, err) 106 | } else { 107 | assert.NoError(t, err) 108 | } 109 | }) 110 | } 111 | } 112 | 113 | func TestConfiguration_String(t *testing.T) { 114 | config := Configuration{ 115 | MC_PORT: 1025, 116 | MC_HOST: "test.example.com", 117 | MC_REDIRECT_TO: "test@example.com", 118 | MC_SENDER_MAIL: "sender@example.com", 119 | MC_SMTP_HOST: "smtp.example.com", 120 | MC_SMTP_PORT: 587, 121 | MC_SMTP_USER: "testuser", 122 | MC_SMTP_PASSWORD: "testpass", 123 | } 124 | 125 | result := config.String() 126 | 127 | // Verify all fields are present in the string representation 128 | assert.Contains(t, result, "1025") 129 | assert.Contains(t, result, "test.example.com") 130 | assert.Contains(t, result, "test@example.com") 131 | assert.Contains(t, result, "sender@example.com") 132 | assert.Contains(t, result, "smtp.example.com") 133 | assert.Contains(t, result, "587") 134 | assert.Contains(t, result, "testuser") 135 | 136 | // Verify password is not exposed 137 | assert.NotContains(t, result, "testpass") 138 | } 139 | -------------------------------------------------------------------------------- /tests/e2e/mailcatcher_full_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "github.com/testcontainers/testcontainers-go" 12 | "github.com/testcontainers/testcontainers-go/wait" 13 | ) 14 | 15 | // TestMailcatcherFullWorkflow tests the complete mailcatcher workflow 16 | // This test requires the mailcatcher:test image to be built first 17 | func TestMailcatcherFullWorkflow(t *testing.T) { 18 | ctx := context.Background() 19 | 20 | // Create a network for the containers to communicate 21 | networkName := "mailcatcher-test-network" 22 | network, err := testcontainers.GenericNetwork(ctx, testcontainers.GenericNetworkRequest{ 23 | NetworkRequest: testcontainers.NetworkRequest{ 24 | Name: networkName, 25 | }, 26 | }) 27 | require.NoError(t, err) 28 | defer func() { 29 | if err := network.Remove(ctx); err != nil { 30 | t.Logf("Error removing network: %v", err) 31 | } 32 | }() 33 | 34 | // Start Mailpit container with SMTP authentication enabled 35 | mailpitReq := testcontainers.ContainerRequest{ 36 | Image: "axllent/mailpit:latest", 37 | ExposedPorts: []string{"1025/tcp", "8025/tcp"}, 38 | Networks: []string{networkName}, 39 | NetworkAliases: map[string][]string{ 40 | networkName: {"mailpit"}, 41 | }, 42 | Env: map[string]string{ 43 | "MP_SMTP_AUTH_ACCEPT_ANY": "1", 44 | "MP_SMTP_AUTH_ALLOW_INSECURE": "1", 45 | }, 46 | WaitingFor: wait.ForLog("accessible via http://localhost:8025/"), 47 | } 48 | 49 | mailpitContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 50 | ContainerRequest: mailpitReq, 51 | Started: true, 52 | }) 53 | require.NoError(t, err) 54 | defer func() { 55 | if err := mailpitContainer.Terminate(ctx); err != nil { 56 | t.Logf("Error terminating mailpit container: %v", err) 57 | } 58 | }() 59 | 60 | // Get Mailpit web port 61 | mailpitWebPort, err := mailpitContainer.MappedPort(ctx, "8025") 62 | require.NoError(t, err) 63 | 64 | mailpitHost, err := mailpitContainer.Host(ctx) 65 | require.NoError(t, err) 66 | 67 | // Start Mailcatcher container using pre-built image 68 | mailcatcherReq := testcontainers.ContainerRequest{ 69 | Image: "mailcatcher:test", 70 | ExposedPorts: []string{"1025/tcp"}, 71 | Networks: []string{networkName}, 72 | Env: map[string]string{ 73 | "MC_PORT": "1025", 74 | "MC_HOST": "test.example.com", 75 | "MC_REDIRECT_TO": "test@example.com", 76 | "MC_SENDER_MAIL": "mailcatcher@test.example.com", 77 | "MC_SMTP_HOST": "mailpit", 78 | "MC_SMTP_PORT": "1025", 79 | "MC_SMTP_USER": "", 80 | "MC_SMTP_PASSWORD": "", 81 | }, 82 | WaitingFor: wait.ForLog("Starting server at"), 83 | } 84 | 85 | mailcatcherContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 86 | ContainerRequest: mailcatcherReq, 87 | Started: true, 88 | }) 89 | require.NoError(t, err) 90 | defer func() { 91 | if err := mailcatcherContainer.Terminate(ctx); err != nil { 92 | t.Logf("Error terminating mailcatcher container: %v", err) 93 | } 94 | }() 95 | 96 | // Get Mailcatcher port 97 | mailcatcherPort, err := mailcatcherContainer.MappedPort(ctx, "1025") 98 | require.NoError(t, err) 99 | 100 | mailcatcherHost, err := mailcatcherContainer.Host(ctx) 101 | require.NoError(t, err) 102 | 103 | // Create mailpit client 104 | mailpitClient := NewMailpitClient(mailpitHost, mailpitWebPort.Int()) 105 | 106 | // Clear any existing messages 107 | err = mailpitClient.DeleteAllMessages() 108 | require.NoError(t, err) 109 | 110 | t.Run("Forward email through mailcatcher", func(t *testing.T) { 111 | // Send email to mailcatcher 112 | err := sendTestEmail(mailcatcherHost, mailcatcherPort.Int(), "test@test.example.com") 113 | require.NoError(t, err) 114 | 115 | // Wait a bit and check mailcatcher logs 116 | time.Sleep(2 * time.Second) 117 | logs, err := mailcatcherContainer.Logs(ctx) 118 | if err == nil { 119 | logContent, _ := io.ReadAll(logs) 120 | t.Logf("Mailcatcher logs: %s", string(logContent)) 121 | } 122 | 123 | // Wait for email to be processed and forwarded 124 | messages, err := mailpitClient.WaitForMessages(1, 10*time.Second) 125 | require.NoError(t, err) 126 | assert.Len(t, messages, 1, "Expected 1 email in mailpit") 127 | 128 | if len(messages) > 0 { 129 | message := messages[0] 130 | assert.Contains(t, message.Subject, "[MAILCATCHER]", "Email subject should contain [MAILCATCHER] prefix") 131 | assert.Equal(t, "mailcatcher@test.example.com", message.From.Address, "Email should be from mailcatcher") 132 | assert.Len(t, message.To, 1, "Email should have one recipient") 133 | if len(message.To) > 0 { 134 | assert.Equal(t, "test@example.com", message.To[0].Address, "Email should be forwarded to the configured address") 135 | } 136 | } 137 | }) 138 | 139 | t.Run("Ignore invalid domain", func(t *testing.T) { 140 | // Clear previous messages 141 | err := mailpitClient.DeleteAllMessages() 142 | require.NoError(t, err) 143 | 144 | // Send email with invalid domain 145 | err = sendTestEmail(mailcatcherHost, mailcatcherPort.Int(), "test@invalid-domain.com") 146 | require.NoError(t, err) 147 | 148 | // Wait a bit and check that no email was forwarded 149 | time.Sleep(3 * time.Second) 150 | 151 | messages, err := mailpitClient.GetMessages() 152 | require.NoError(t, err) 153 | assert.Len(t, messages, 0, "Expected 0 emails in mailpit for invalid domain") 154 | }) 155 | } 156 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= 2 | dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 3 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= 4 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 5 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= 6 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 7 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 8 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 9 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 10 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 11 | github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= 12 | github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 13 | github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= 14 | github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= 15 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 16 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 17 | github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= 18 | github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= 19 | github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= 20 | github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= 21 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 22 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 27 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 28 | github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= 29 | github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 30 | github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= 31 | github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= 32 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 33 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 34 | github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= 35 | github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 36 | github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= 37 | github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= 38 | github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk= 39 | github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ= 40 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 41 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 42 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 43 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 44 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 45 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 46 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 47 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 48 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 49 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 50 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 51 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 52 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 53 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 54 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 55 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 56 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 57 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 58 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 59 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 60 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= 61 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= 62 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 63 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 64 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 65 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 66 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 67 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 68 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 69 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 70 | github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= 71 | github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= 72 | github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= 73 | github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 74 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 75 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 76 | github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= 77 | github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= 78 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 79 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 80 | github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= 81 | github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= 82 | github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= 83 | github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= 84 | github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= 85 | github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= 86 | github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= 87 | github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 88 | github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= 89 | github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= 90 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 91 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 92 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 93 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 94 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 95 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 96 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 97 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 98 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 99 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 100 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 101 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= 102 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 103 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 104 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 105 | github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM= 106 | github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U= 107 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 108 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 109 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 110 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 111 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 112 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 113 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 114 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 115 | github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= 116 | github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= 117 | github.com/tkanos/gonfig v0.0.0-20210106201359-53e13348de2f h1:xDFq4NVQD34ekH5UsedBSgfxsBuPU2aZf7v4t0tH2jY= 118 | github.com/tkanos/gonfig v0.0.0-20210106201359-53e13348de2f/go.mod h1:DaZPBuToMc2eezA9R9nDAnmS2RMwL7yEa5YD36ESQdI= 119 | github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= 120 | github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= 121 | github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= 122 | github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= 123 | github.com/veqryn/go-email v0.0.0-20240310084539-bc822271db61 h1:nAqnfye4RkKGGMVGaWrg/USsTXQZKzlZgbolQye8two= 124 | github.com/veqryn/go-email v0.0.0-20240310084539-bc822271db61/go.mod h1:/wkXUpcdIFR3mp0fMes97u0UtjtbrdJ9WbBZ1IjsfEc= 125 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 126 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 127 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 128 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 129 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= 130 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= 131 | go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 132 | go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 133 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= 134 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= 135 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= 136 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= 137 | go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 138 | go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 139 | go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= 140 | go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= 141 | go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= 142 | go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= 143 | go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 144 | go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 145 | go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= 146 | go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= 147 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 148 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 149 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 150 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 151 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 152 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 153 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 154 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 155 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 156 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 157 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 158 | golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= 159 | golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 160 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 161 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 162 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 163 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 164 | google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= 165 | google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= 166 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc= 167 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= 168 | google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= 169 | google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= 170 | google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= 171 | google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 172 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= 173 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= 174 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 175 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 176 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 177 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 178 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 179 | gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= 180 | gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 181 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= 182 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= 183 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 184 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 185 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 186 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 187 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 188 | gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= 189 | gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= 190 | --------------------------------------------------------------------------------