From 8eec398c9f6562a3470440246ad5591536b44c57 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 10 Apr 2026 20:12:45 +0200 Subject: [PATCH] fix(email): support env:// refs in smtp_from/smtp_user/imap_user and preserve email config through v0 migration - Change SMTPFrom, SMTPUser, IMAPUser from plain string to SecureString so env:// references are resolved at runtime (previously passed literally to SMTP/IMAP, causing auth failures) - Add Email field to channelsConfigV0 and ToChannelsConfig() so the email block is not silently dropped when loading a version-0 config (the migration was resetting enabled=false regardless of config contents) Co-Authored-By: Claude Sonnet 4.6 --- pkg/channels/email/email.go | 16 ++++++++-------- pkg/channels/email/email_test.go | 8 ++++---- pkg/config/config.go | 6 +++--- pkg/config/config_old.go | 2 ++ 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/pkg/channels/email/email.go b/pkg/channels/email/email.go index 19d359003..1290bcc50 100644 --- a/pkg/channels/email/email.go +++ b/pkg/channels/email/email.go @@ -32,13 +32,13 @@ func NewEmailChannel(cfg config.EmailConfig, messageBus *bus.MessageBus) (*Email if cfg.SMTPHost == "" { return nil, fmt.Errorf("email smtp_host is required") } - if cfg.SMTPFrom == "" { + if cfg.SMTPFrom.String() == "" { return nil, fmt.Errorf("email smtp_from is required") } if cfg.IMAPHost == "" { return nil, fmt.Errorf("email imap_host is required") } - if cfg.IMAPUser == "" { + if cfg.IMAPUser.String() == "" { return nil, fmt.Errorf("email imap_user is required") } @@ -110,13 +110,13 @@ func (c *EmailChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]str } addr := fmt.Sprintf("%s:%d", c.config.SMTPHost, smtpPort) - smtpUser := c.config.SMTPUser + smtpUser := c.config.SMTPUser.String() if smtpUser == "" { - smtpUser = c.config.SMTPFrom + smtpUser = c.config.SMTPFrom.String() } body := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s", - c.config.SMTPFrom, to, subject, msg.Content) + c.config.SMTPFrom.String(), to, subject, msg.Content) var auth smtp.Auth if c.config.SMTPPassword.String() != "" { @@ -135,11 +135,11 @@ func (c *EmailChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]str return nil, fmt.Errorf("smtp new client: %w", channels.ErrTemporary) } defer client.Close() - if err := sendViaSMTPClient(client, auth, c.config.SMTPFrom, to, []byte(body)); err != nil { + if err := sendViaSMTPClient(client, auth, c.config.SMTPFrom.String(), to, []byte(body)); err != nil { return nil, err } } else { - if err := smtp.SendMail(addr, auth, c.config.SMTPFrom, []string{to}, []byte(body)); err != nil { + if err := smtp.SendMail(addr, auth, c.config.SMTPFrom.String(), []string{to}, []byte(body)); err != nil { return nil, fmt.Errorf("smtp send: %w: %w", err, channels.ErrTemporary) } } @@ -215,7 +215,7 @@ func (c *EmailChannel) pollIMAP() { } defer client.Close() - if err = client.Login(c.config.IMAPUser, c.config.IMAPPassword.String()).Wait(); err != nil { + if err = client.Login(c.config.IMAPUser.String(), c.config.IMAPPassword.String()).Wait(); err != nil { logger.WarnCF("email", "IMAP login failed", map[string]any{"err": err}) return } diff --git a/pkg/channels/email/email_test.go b/pkg/channels/email/email_test.go index 133ff0650..0333e96ec 100644 --- a/pkg/channels/email/email_test.go +++ b/pkg/channels/email/email_test.go @@ -15,9 +15,9 @@ func TestNewEmailChannel(t *testing.T) { validCfg := config.EmailConfig{ SMTPHost: "smtp.example.com", - SMTPFrom: "bot@example.com", + SMTPFrom: *config.NewSecureString("bot@example.com"), IMAPHost: "imap.example.com", - IMAPUser: "bot@example.com", + IMAPUser: *config.NewSecureString("bot@example.com"), } t.Run("missing smtp_host", func(t *testing.T) { @@ -31,7 +31,7 @@ func TestNewEmailChannel(t *testing.T) { t.Run("missing smtp_from", func(t *testing.T) { cfg := validCfg - cfg.SMTPFrom = "" + cfg.SMTPFrom = *config.NewSecureString("") _, err := NewEmailChannel(cfg, msgBus) if err == nil { t.Error("expected error for missing smtp_from, got nil") @@ -49,7 +49,7 @@ func TestNewEmailChannel(t *testing.T) { t.Run("missing imap_user", func(t *testing.T) { cfg := validCfg - cfg.IMAPUser = "" + cfg.IMAPUser = *config.NewSecureString("") _, err := NewEmailChannel(cfg, msgBus) if err == nil { t.Error("expected error for missing imap_user, got nil") diff --git a/pkg/config/config.go b/pkg/config/config.go index 4eaeb2c4d..e5f706080 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -603,14 +603,14 @@ type EmailConfig struct { // SMTP (outbound) SMTPHost string `json:"smtp_host" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_HOST"` SMTPPort int `json:"smtp_port" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_PORT"` - SMTPFrom string `json:"smtp_from" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_FROM"` - SMTPUser string `json:"smtp_user" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_USER"` + SMTPFrom SecureString `json:"smtp_from,omitzero" yaml:"smtp_from,omitempty" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_FROM"` + SMTPUser SecureString `json:"smtp_user,omitzero" yaml:"smtp_user,omitempty" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_USER"` SMTPPassword SecureString `json:"smtp_password,omitzero" yaml:"smtp_password,omitempty" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_PASSWORD"` DefaultSubject string `json:"default_subject" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_DEFAULT_SUBJECT"` // IMAP (inbound) IMAPHost string `json:"imap_host" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_HOST"` IMAPPort int `json:"imap_port" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_PORT"` - IMAPUser string `json:"imap_user" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_USER"` + IMAPUser SecureString `json:"imap_user,omitzero" yaml:"imap_user,omitempty" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_USER"` IMAPPassword SecureString `json:"imap_password,omitzero" yaml:"imap_password,omitempty" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_PASSWORD"` PollIntervalSecs int `json:"poll_interval_secs" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_POLL_INTERVAL_SECS"` // Common diff --git a/pkg/config/config_old.go b/pkg/config/config_old.go index 150275aac..c567063ea 100644 --- a/pkg/config/config_old.go +++ b/pkg/config/config_old.go @@ -100,6 +100,7 @@ type channelsConfigV0 struct { WeCom wecomConfigV0 `json:"wecom" envPrefix:"PICOCLAW_CHANNELS_WECOM_"` Pico picoConfigV0 `json:"pico"` IRC ircConfigV0 `json:"irc"` + Email EmailConfig `json:"email"` } func (v *channelsConfigV0) ToChannelsConfig() ChannelsConfig { @@ -134,6 +135,7 @@ func (v *channelsConfigV0) ToChannelsConfig() ChannelsConfig { WeCom: wecom, Pico: pico, IRC: irc, + Email: v.Email, } }