email module implemented and covered by 50%
This commit is contained in:
@@ -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 ""
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user