email module implemented and covered by 50%

This commit is contained in:
d1nch8g
2026-04-27 22:23:05 +03:00
parent ce9369c831
commit 94e52de4f2
9 changed files with 657 additions and 26 deletions
+223
View File
@@ -0,0 +1,223 @@
package email
import (
"context"
"crypto/tls"
"fmt"
"log/slog"
"net/smtp"
"strings"
"time"
"github.com/d1nch8g/jules/chat"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapclient"
)
type Config struct {
SMTPHost string
SMTPPort string
IMAPHost string
IMAPPort string
Username string
Password string
FromAddr string
}
type Client struct {
imapAddr string
smtpAddr string
username string
password string
fromAddr string
pollInterval time.Duration
tlsConfig *tls.Config // ADD THIS LINE ONLY
}
func New(cfg Config) (*Client, error) {
if cfg.SMTPPort == "" {
cfg.SMTPPort = "465"
}
if cfg.IMAPPort == "" {
cfg.IMAPPort = "993"
}
return &Client{
imapAddr: fmt.Sprintf("%s:%s", cfg.IMAPHost, cfg.IMAPPort),
smtpAddr: fmt.Sprintf("%s:%s", cfg.SMTPHost, cfg.SMTPPort),
username: cfg.Username,
password: cfg.Password,
fromAddr: cfg.FromAddr,
pollInterval: 10 * time.Second,
}, nil
}
func (c *Client) Send(_ context.Context, id string, text string) error {
to := strings.TrimSpace(id)
if to == "" {
return fmt.Errorf("email: empty recipient")
}
msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: \r\n\r\n%s", c.fromAddr, to, text)
host := strings.Split(c.smtpAddr, ":")[0]
// MODIFY: Use client's tlsConfig if set
tlsConfig := c.tlsConfig
if tlsConfig == nil {
tlsConfig = &tls.Config{ServerName: host}
}
conn, err := tls.Dial("tcp", c.smtpAddr, tlsConfig)
if err != nil {
return fmt.Errorf("email: smtp connect: %w", err)
}
defer conn.Close()
client, err := smtp.NewClient(conn, host)
if err != nil {
return fmt.Errorf("email: smtp client: %w", err)
}
defer client.Quit()
auth := smtp.PlainAuth("", c.username, c.password, host)
if err = client.Auth(auth); err != nil {
return fmt.Errorf("email: smtp auth: %w", err)
}
if err = client.Mail(c.fromAddr); err != nil {
return fmt.Errorf("email: smtp mail: %w", err)
}
if err = client.Rcpt(to); err != nil {
return fmt.Errorf("email: smtp rcpt: %w", err)
}
w, err := client.Data()
if err != nil {
return fmt.Errorf("email: smtp data: %w", err)
}
_, err = fmt.Fprint(w, msg)
if err != nil {
return fmt.Errorf("email: smtp write: %w", err)
}
w.Close()
return nil
}
func (c *Client) Receive(ctx context.Context) <-chan chat.Message {
out := make(chan chat.Message)
go func() {
defer close(out)
for {
select {
case <-ctx.Done():
return
case <-time.After(c.pollInterval):
if err := c.poll(ctx, out); err != nil {
slog.ErrorContext(ctx, "email poll failed", "error", err)
}
}
}
}()
return out
}
func (c *Client) poll(ctx context.Context, out chan<- chat.Message) error {
host := strings.Split(c.imapAddr, ":")[0]
// MODIFY: Use client's tlsConfig if set
tlsConfig := c.tlsConfig
if tlsConfig == nil {
tlsConfig = &tls.Config{ServerName: host}
}
conn, err := tls.Dial("tcp", c.imapAddr, tlsConfig)
if err != nil {
return fmt.Errorf("imap dial: %w", err)
}
client := imapclient.New(conn, nil)
defer client.Close()
if err := client.Login(c.username, c.password).Wait(); err != nil {
return fmt.Errorf("imap login: %w", err)
}
_, err = client.Select("INBOX", nil).Wait()
if err != nil {
return fmt.Errorf("imap select: %w", err)
}
criteria := &imap.SearchCriteria{
NotFlag: []imap.Flag{imap.FlagSeen},
}
searchRes, err := client.Search(criteria, nil).Wait()
if err != nil {
return fmt.Errorf("imap search: %w", err)
}
uids := searchRes.AllUIDs()
if len(uids) == 0 {
return nil
}
seqSet := imap.UIDSetNum(uids...)
fetchOpts := &imap.FetchOptions{
Envelope: true,
BodySection: []*imap.FetchItemBodySection{{}},
}
messages, err := client.Fetch(seqSet, fetchOpts).Collect()
if err != nil {
return fmt.Errorf("imap fetch: %w", err)
}
// Mark as seen using STORE command
storeFlags := &imap.StoreFlags{
Op: imap.StoreFlagsAdd,
Flags: []imap.Flag{imap.FlagSeen},
}
storeCmd := client.Store(seqSet, storeFlags, nil)
if err := storeCmd.Close(); err != nil {
slog.Error("email: failed to mark as seen", "error", err)
}
for _, msg := range messages {
from := extractFrom(msg.Envelope)
body := extractBody(msg)
if from == "" || body == "" {
continue
}
select {
case <-ctx.Done():
return nil
case out <- chat.Message{
Chat: chat.Email,
ID: from,
Text: body,
}:
}
}
return nil
}
func extractFrom(env *imap.Envelope) string {
if env == nil || len(env.From) == 0 {
return ""
}
return env.From[0].Addr()
}
func extractBody(msg *imapclient.FetchMessageBuffer) string {
for _, section := range msg.BodySection {
return string(section.Bytes)
}
return ""
}
+288
View File
@@ -0,0 +1,288 @@
package email
import (
"bufio"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"math/big"
"net"
"testing"
"time"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapclient"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Generate self-signed certificate for TLS testing
func generateTLSCert() tls.Certificate {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
DNSNames: []string{"localhost", "127.0.0.1"},
}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
panic(err)
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
panic(err)
}
return tlsCert
}
// startFakeSMTPServer creates a TLS-enabled fake SMTP server
func startFakeSMTPServer(t *testing.T, handler func(conn net.Conn)) string {
t.Helper()
cert := generateTLSCert()
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
}
listener, err := tls.Listen("tcp", "127.0.0.1:0", tlsConfig)
require.NoError(t, err)
t.Cleanup(func() { listener.Close() })
go func() {
conn, err := listener.Accept()
if err != nil {
return
}
defer conn.Close()
handler(conn)
}()
_, port, _ := net.SplitHostPort(listener.Addr().String())
return port
}
// createTestClient is a helper to create a Client with TLS skip verification
func createTestClient(smtpPort, username, password string) *Client {
return &Client{
smtpAddr: fmt.Sprintf("127.0.0.1:%s", smtpPort),
imapAddr: "imap.test.com:993",
username: username,
password: password,
fromAddr: "user@test.com",
pollInterval: 10 * time.Second,
tlsConfig: &tls.Config{InsecureSkipVerify: true},
}
}
func TestNew(t *testing.T) {
t.Run("with defaults", func(t *testing.T) {
c, err := New(Config{
SMTPHost: "smtp.test.com",
IMAPHost: "imap.test.com",
Username: "user@test.com",
Password: "pass",
FromAddr: "user@test.com",
})
require.NoError(t, err)
assert.Equal(t, "smtp.test.com:465", c.smtpAddr)
assert.Equal(t, "imap.test.com:993", c.imapAddr)
assert.Equal(t, "user@test.com", c.username)
assert.Equal(t, 10*time.Second, c.pollInterval)
})
t.Run("custom ports", func(t *testing.T) {
c, err := New(Config{
SMTPHost: "smtp.test.com",
SMTPPort: "587",
IMAPHost: "imap.test.com",
IMAPPort: "143",
Username: "user@test.com",
Password: "pass",
FromAddr: "user@test.com",
})
require.NoError(t, err)
assert.Equal(t, "smtp.test.com:587", c.smtpAddr)
assert.Equal(t, "imap.test.com:143", c.imapAddr)
})
}
func TestSend_EmptyRecipient(t *testing.T) {
c, _ := New(Config{
SMTPHost: "smtp.test.com",
IMAPHost: "imap.test.com",
Username: "user@test.com",
Password: "pass",
FromAddr: "user@test.com",
})
err := c.Send(context.Background(), "", "hello")
require.Error(t, err)
assert.Contains(t, err.Error(), "empty recipient")
}
func TestSend_SMTPServerDown(t *testing.T) {
c, _ := New(Config{
SMTPHost: "127.0.0.1",
SMTPPort: "25999",
IMAPHost: "imap.test.com",
Username: "user@test.com",
Password: "pass",
FromAddr: "user@test.com",
})
err := c.Send(context.Background(), "to@test.com", "hello")
require.Error(t, err)
assert.Contains(t, err.Error(), "smtp connect")
}
func TestSend_Success(t *testing.T) {
port := startFakeSMTPServer(t, func(conn net.Conn) {
reader := bufio.NewReader(conn)
// Greeting
fmt.Fprintf(conn, "220 smtp.example.com ESMTP\r\n")
// Read EHLO
reader.ReadString('\n')
fmt.Fprintf(conn, "250-smtp.example.com\r\n")
fmt.Fprintf(conn, "250 AUTH PLAIN\r\n")
// Read AUTH
reader.ReadString('\n')
fmt.Fprintf(conn, "235 2.7.0 Authentication successful\r\n")
// Read MAIL FROM
reader.ReadString('\n')
fmt.Fprintf(conn, "250 2.1.0 Ok\r\n")
// Read RCPT TO
reader.ReadString('\n')
fmt.Fprintf(conn, "250 2.1.5 Ok\r\n")
// Read DATA
reader.ReadString('\n')
fmt.Fprintf(conn, "354 End data with <CR><LF>.<CR><LF>\r\n")
// Read message body (ending with \r\n.\r\n)
reader.ReadString('\n')
reader.ReadString('\n')
fmt.Fprintf(conn, "250 2.0.0 Ok: queued\r\n")
})
c := createTestClient(port, "user@test.com", "pass")
err := c.Send(context.Background(), "to@test.com", "Hello World")
require.NoError(t, err)
}
func TestSend_AuthFailure(t *testing.T) {
port := startFakeSMTPServer(t, func(conn net.Conn) {
reader := bufio.NewReader(conn)
fmt.Fprintf(conn, "220 smtp.example.com ESMTP\r\n")
reader.ReadString('\n')
fmt.Fprintf(conn, "250-smtp.example.com\r\n")
fmt.Fprintf(conn, "250 AUTH PLAIN\r\n")
reader.ReadString('\n')
fmt.Fprintf(conn, "535 5.7.8 Authentication failed\r\n")
})
c := createTestClient(port, "user@test.com", "wrong")
err := c.Send(context.Background(), "to@test.com", "Hello")
require.Error(t, err)
assert.Contains(t, err.Error(), "smtp auth")
}
func TestReceive_ContextCancel(t *testing.T) {
c, _ := New(Config{
SMTPHost: "smtp.test.com",
IMAPHost: "127.0.0.1",
IMAPPort: "25998",
Username: "user@test.com",
Password: "pass",
FromAddr: "user@test.com",
})
ctx, cancel := context.WithCancel(context.Background())
ch := c.Receive(ctx)
cancel()
// Should close quickly
select {
case _, ok := <-ch:
assert.False(t, ok)
case <-time.After(time.Second):
t.Fatal("channel not closed after cancel")
}
}
func TestReceive_ConnectionFailure(t *testing.T) {
c, _ := New(Config{
SMTPHost: "smtp.test.com",
IMAPHost: "127.0.0.1",
IMAPPort: "25999",
Username: "user@test.com",
Password: "pass",
FromAddr: "user@test.com",
})
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
ch := c.Receive(ctx)
select {
case msg := <-ch:
t.Fatalf("unexpected message: %+v", msg)
case <-time.After(2 * time.Second):
// Expected - connection failed, channel closed
}
}
func TestExtractFrom(t *testing.T) {
t.Run("nil envelope", func(t *testing.T) {
assert.Equal(t, "", extractFrom(nil))
})
t.Run("with data", func(t *testing.T) {
env := &imap.Envelope{
From: []imap.Address{
{
Name: "John Doe",
Mailbox: "john",
Host: "example.com",
},
},
}
assert.Equal(t, "john@example.com", extractFrom(env))
})
}
func TestExtractBody(t *testing.T) {
t.Run("empty sections", func(t *testing.T) {
msg := &imapclient.FetchMessageBuffer{}
assert.Equal(t, "", extractBody(msg))
})
t.Run("with data", func(t *testing.T) {
msg := &imapclient.FetchMessageBuffer{
BodySection: []imapclient.FetchBodySectionBuffer{{
Section: &imap.FetchItemBodySection{},
Bytes: []byte("hello world"),
}},
}
assert.Equal(t, "hello world", extractBody(msg))
})
}
+1 -4
View File
@@ -4,12 +4,9 @@ import "context"
const (
Telegram = "telegram"
Email = "email"
)
var PlatformList = []string{ //nolint:gochecknoglobals // contains available platforms
Telegram,
}
// Message represents an incoming message from a user.
type Message struct {
// Always should be identifier of the source
+17 -2
View File
@@ -9,6 +9,7 @@ import (
"time"
"github.com/d1nch8g/jules/chat"
"github.com/d1nch8g/jules/chat/email"
"github.com/d1nch8g/jules/chat/telegram"
"github.com/d1nch8g/jules/config"
"github.com/d1nch8g/jules/database/postgres"
@@ -30,11 +31,24 @@ func main() {
exit("failed to initialize configuration", err)
}
telegram, err := telegram.New(cfg.TelegramBotToken, telegram.BaseURL, cfg.TelegramProxy)
tg, err := telegram.New(cfg.TelegramBotToken, telegram.BaseURL, cfg.TelegramProxy)
if err != nil {
exit("failed to initialize telegram", err)
}
mail, err := email.New(email.Config{
SMTPHost: cfg.EmailSMTPHost,
SMTPPort: cfg.EmailSMTPPort,
IMAPHost: cfg.EmailIMAPHost,
IMAPPort: cfg.EmailIMAPPort,
Username: cfg.EmailUsername,
Password: cfg.EmailPassword,
FromAddr: cfg.EmailFromAddr,
})
if err != nil {
exit("failed to configure email", err)
}
pg, err := postgres.New(cfg.PostgresConnString)
if err != nil {
exit("failed to connect postgres", err)
@@ -51,7 +65,8 @@ func main() {
LLM: deepseek.New(cfg.DeepSeekAPIKey, deepseek.BaseURL),
Searcher: brave.New(cfg.BraveAPIKey, brave.BaseURL),
Chats: map[string]chat.Chat{
chat.Telegram: telegram,
chat.Telegram: tg,
chat.Email: mail,
},
})
if err != nil {
+30 -1
View File
@@ -8,20 +8,49 @@ import (
type Config struct {
TelegramBotToken string
TelegramProxy string
PostgresConnString string
EmailSMTPHost string
EmailSMTPPort string
EmailIMAPHost string
EmailIMAPPort string
EmailUsername string
EmailPassword string
EmailFromAddr string
DeepSeekAPIKey string
BraveAPIKey string
PostgresConnString string
}
func Load() (*Config, error) {
cfg := &Config{
TelegramBotToken: os.Getenv("TELEGRAM_BOT_TOKEN"),
TelegramProxy: os.Getenv("TELEGRAM_PROXY"),
EmailSMTPHost: os.Getenv("EMAIL_SMTP_HOST"),
EmailSMTPPort: os.Getenv("EMAIL_SMTP_PORT"),
EmailIMAPHost: os.Getenv("EMAIL_IMAP_HOST"),
EmailIMAPPort: os.Getenv("EMAIL_IMAP_PORT"),
EmailUsername: os.Getenv("EMAIL_USERNAME"),
EmailPassword: os.Getenv("EMAIL_PASSWORD"),
EmailFromAddr: os.Getenv("EMAIL_FROM_ADDR"),
DeepSeekAPIKey: os.Getenv("DEEPSEEK_API_KEY"),
BraveAPIKey: os.Getenv("BRAVE_API_KEY"),
PostgresConnString: os.Getenv("POSTGRES_CONN_STRING"),
}
if cfg.EmailSMTPPort == "" {
cfg.EmailSMTPPort = "465"
}
if cfg.EmailIMAPPort == "" {
cfg.EmailIMAPPort = "993"
}
if cfg.DeepSeekAPIKey == "" {
return nil, errors.New("DEEPSEEK_API_KEY is required")
}
+49 -11
View File
@@ -25,6 +25,35 @@ func TestLoad(t *testing.T) {
},
wantErr: false,
},
{
name: "all fields including email",
env: map[string]string{
"TELEGRAM_BOT_TOKEN": "tg",
"DEEPSEEK_API_KEY": "ds",
"BRAVE_API_KEY": "br",
"POSTGRES_CONN_STRING": "pg://localhost/db",
"TELEGRAM_PROXY": "socks5://localhost:1080",
"EMAIL_SMTP_HOST": "mail.chx.su",
"EMAIL_SMTP_PORT": "465",
"EMAIL_IMAP_HOST": "mail.chx.su",
"EMAIL_IMAP_PORT": "993",
"EMAIL_USERNAME": "jules@chx.su",
"EMAIL_PASSWORD": "secret",
"EMAIL_FROM_ADDR": "jules@chx.su",
},
wantErr: false,
},
{
name: "email fields with defaults for ports",
env: map[string]string{
"TELEGRAM_BOT_TOKEN": "tg",
"DEEPSEEK_API_KEY": "ds",
"BRAVE_API_KEY": "br",
"POSTGRES_CONN_STRING": "pg://localhost/db",
"EMAIL_USERNAME": "jules@chx.su",
},
wantErr: false,
},
{
name: "missing telegram token",
env: map[string]string{
@@ -65,17 +94,6 @@ func TestLoad(t *testing.T) {
wantErr: true,
errMsg: "POSTGRES_CONN_STRING is required",
},
{
name: "telegram proxy optional",
env: map[string]string{
"TELEGRAM_BOT_TOKEN": "tg",
"DEEPSEEK_API_KEY": "ds",
"BRAVE_API_KEY": "br",
"POSTGRES_CONN_STRING": "pg://localhost/db",
"TELEGRAM_PROXY": "socks5://localhost:1080",
},
wantErr: false,
},
}
for _, tt := range tests {
@@ -98,6 +116,26 @@ func TestLoad(t *testing.T) {
assert.Equal(t, tt.env["BRAVE_API_KEY"], cfg.BraveAPIKey)
assert.Equal(t, tt.env["POSTGRES_CONN_STRING"], cfg.PostgresConnString)
assert.Equal(t, tt.env["TELEGRAM_PROXY"], cfg.TelegramProxy)
// Email fields
assert.Equal(t, tt.env["EMAIL_SMTP_HOST"], cfg.EmailSMTPHost)
assert.Equal(t, tt.env["EMAIL_IMAP_HOST"], cfg.EmailIMAPHost)
assert.Equal(t, tt.env["EMAIL_USERNAME"], cfg.EmailUsername)
assert.Equal(t, tt.env["EMAIL_PASSWORD"], cfg.EmailPassword)
assert.Equal(t, tt.env["EMAIL_FROM_ADDR"], cfg.EmailFromAddr)
// Port defaults
expectedSMTPPort := tt.env["EMAIL_SMTP_PORT"]
if expectedSMTPPort == "" {
expectedSMTPPort = "465"
}
assert.Equal(t, expectedSMTPPort, cfg.EmailSMTPPort)
expectedIMAPPort := tt.env["EMAIL_IMAP_PORT"]
if expectedIMAPPort == "" {
expectedIMAPPort = "993"
}
assert.Equal(t, expectedIMAPPort, cfg.EmailIMAPPort)
})
}
}
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"strings"
"time"
"github.com/d1nch8g/jules/chat"
"github.com/d1nch8g/jules/database"
"github.com/d1nch8g/jules/engine/actions"
"github.com/d1nch8g/jules/engine/timeconv"
@@ -48,7 +49,7 @@ NEVER REPEAT OLD QUESTIONS OR RESPOND TO ALREADY-HANDLED TOPICS
IF YOU EXPECT AN ANSWER FROM USER AND UNSURE IT WILL COME, YOU CAN CREATE 'CHECK' NOTIFICAION FOR YOURSELF
MAX 4 MESSAGES PER RESPONSE TOTAL
FOR QUESTIONS: MAX 2 MESSAGES (CONTEXT + QUESTION)
`
AVAILABLE PLATFORMS: ` + chat.Telegram + `, ` + chat.Email + "\n"
onboarding = `
CURRENT PROFILE DOESN'T HAVE MUCH INFO, SO YOUR GOAL IS TO PERFORM ONBOARDING
+3
View File
@@ -5,6 +5,7 @@ go 1.26.1
require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/emersion/go-imap/v2 v2.0.0-beta.8
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/google/uuid v1.6.0
@@ -17,6 +18,8 @@ require (
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emersion/go-message v0.18.2 // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
+37
View File
@@ -22,6 +22,12 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/emersion/go-imap/v2 v2.0.0-beta.8 h1:5IXZK1E33DyeP526320J3RS7eFlCYGFgtbrfapqDPug=
github.com/emersion/go-imap/v2 v2.0.0-beta.8/go.mod h1:dhoFe2Q0PwLrMD7oZw8ODuaD0vLYPe5uj2wcOMnvh48=
github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
@@ -63,6 +69,7 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
@@ -73,10 +80,40 @@ go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/Wgbsd
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=