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 {
+37 -8
View File
@@ -6,22 +6,51 @@ import (
)
type Config struct {
TelegramBotToken string
TelegramProxy string
TelegramBotToken string
TelegramProxy string
EmailSMTPHost string
EmailSMTPPort string
EmailIMAPHost string
EmailIMAPPort string
EmailUsername string
EmailPassword string
EmailFromAddr string
DeepSeekAPIKey string
BraveAPIKey string
PostgresConnString string
DeepSeekAPIKey string
BraveAPIKey string
}
func Load() (*Config, error) {
cfg := &Config{
TelegramBotToken: os.Getenv("TELEGRAM_BOT_TOKEN"),
TelegramProxy: os.Getenv("TELEGRAM_PROXY"),
DeepSeekAPIKey: os.Getenv("DEEPSEEK_API_KEY"),
BraveAPIKey: os.Getenv("BRAVE_API_KEY"),
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=