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 <noreply@anthropic.com>
This commit is contained in:
parent
024282be26
commit
8eec398c9f
4 changed files with 17 additions and 15 deletions
|
|
@ -32,13 +32,13 @@ func NewEmailChannel(cfg config.EmailConfig, messageBus *bus.MessageBus) (*Email
|
||||||
if cfg.SMTPHost == "" {
|
if cfg.SMTPHost == "" {
|
||||||
return nil, fmt.Errorf("email smtp_host is required")
|
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")
|
return nil, fmt.Errorf("email smtp_from is required")
|
||||||
}
|
}
|
||||||
if cfg.IMAPHost == "" {
|
if cfg.IMAPHost == "" {
|
||||||
return nil, fmt.Errorf("email imap_host is required")
|
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")
|
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)
|
addr := fmt.Sprintf("%s:%d", c.config.SMTPHost, smtpPort)
|
||||||
smtpUser := c.config.SMTPUser
|
smtpUser := c.config.SMTPUser.String()
|
||||||
if smtpUser == "" {
|
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",
|
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
|
var auth smtp.Auth
|
||||||
if c.config.SMTPPassword.String() != "" {
|
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)
|
return nil, fmt.Errorf("smtp new client: %w", channels.ErrTemporary)
|
||||||
}
|
}
|
||||||
defer client.Close()
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
return nil, fmt.Errorf("smtp send: %w: %w", err, channels.ErrTemporary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +215,7 @@ func (c *EmailChannel) pollIMAP() {
|
||||||
}
|
}
|
||||||
defer client.Close()
|
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})
|
logger.WarnCF("email", "IMAP login failed", map[string]any{"err": err})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@ func TestNewEmailChannel(t *testing.T) {
|
||||||
|
|
||||||
validCfg := config.EmailConfig{
|
validCfg := config.EmailConfig{
|
||||||
SMTPHost: "smtp.example.com",
|
SMTPHost: "smtp.example.com",
|
||||||
SMTPFrom: "bot@example.com",
|
SMTPFrom: *config.NewSecureString("bot@example.com"),
|
||||||
IMAPHost: "imap.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) {
|
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) {
|
t.Run("missing smtp_from", func(t *testing.T) {
|
||||||
cfg := validCfg
|
cfg := validCfg
|
||||||
cfg.SMTPFrom = ""
|
cfg.SMTPFrom = *config.NewSecureString("")
|
||||||
_, err := NewEmailChannel(cfg, msgBus)
|
_, err := NewEmailChannel(cfg, msgBus)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("expected error for missing smtp_from, got 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) {
|
t.Run("missing imap_user", func(t *testing.T) {
|
||||||
cfg := validCfg
|
cfg := validCfg
|
||||||
cfg.IMAPUser = ""
|
cfg.IMAPUser = *config.NewSecureString("")
|
||||||
_, err := NewEmailChannel(cfg, msgBus)
|
_, err := NewEmailChannel(cfg, msgBus)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("expected error for missing imap_user, got nil")
|
t.Error("expected error for missing imap_user, got nil")
|
||||||
|
|
|
||||||
|
|
@ -603,14 +603,14 @@ type EmailConfig struct {
|
||||||
// SMTP (outbound)
|
// SMTP (outbound)
|
||||||
SMTPHost string `json:"smtp_host" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_HOST"`
|
SMTPHost string `json:"smtp_host" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_HOST"`
|
||||||
SMTPPort int `json:"smtp_port" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_PORT"`
|
SMTPPort int `json:"smtp_port" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_PORT"`
|
||||||
SMTPFrom string `json:"smtp_from" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_FROM"`
|
SMTPFrom SecureString `json:"smtp_from,omitzero" yaml:"smtp_from,omitempty" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_FROM"`
|
||||||
SMTPUser string `json:"smtp_user" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_USER"`
|
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"`
|
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"`
|
DefaultSubject string `json:"default_subject" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_DEFAULT_SUBJECT"`
|
||||||
// IMAP (inbound)
|
// IMAP (inbound)
|
||||||
IMAPHost string `json:"imap_host" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_HOST"`
|
IMAPHost string `json:"imap_host" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_HOST"`
|
||||||
IMAPPort int `json:"imap_port" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_PORT"`
|
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"`
|
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"`
|
PollIntervalSecs int `json:"poll_interval_secs" yaml:"-" env:"PICOCLAW_CHANNELS_EMAIL_POLL_INTERVAL_SECS"`
|
||||||
// Common
|
// Common
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ type channelsConfigV0 struct {
|
||||||
WeCom wecomConfigV0 `json:"wecom" envPrefix:"PICOCLAW_CHANNELS_WECOM_"`
|
WeCom wecomConfigV0 `json:"wecom" envPrefix:"PICOCLAW_CHANNELS_WECOM_"`
|
||||||
Pico picoConfigV0 `json:"pico"`
|
Pico picoConfigV0 `json:"pico"`
|
||||||
IRC ircConfigV0 `json:"irc"`
|
IRC ircConfigV0 `json:"irc"`
|
||||||
|
Email EmailConfig `json:"email"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *channelsConfigV0) ToChannelsConfig() ChannelsConfig {
|
func (v *channelsConfigV0) ToChannelsConfig() ChannelsConfig {
|
||||||
|
|
@ -134,6 +135,7 @@ func (v *channelsConfigV0) ToChannelsConfig() ChannelsConfig {
|
||||||
WeCom: wecom,
|
WeCom: wecom,
|
||||||
Pico: pico,
|
Pico: pico,
|
||||||
IRC: irc,
|
IRC: irc,
|
||||||
|
Email: v.Email,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue