Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,10 @@ type EmailConfig struct {
Text string `yaml:"text,omitempty" json:"text,omitempty"`
RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"`
TLSConfig *commoncfg.TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"`
// ImplicitTLS controls whether to use implicit TLS (direct TLS connection).
// true: force use of implicit TLS (direct TLS connection)
// nil (default): auto-detect based on port (465=implicit, other=explicit) for backward compatibility
ImplicitTLS *bool `yaml:"implicit_tls,omitempty" json:"implicit_tls,omitempty"`
Threading ThreadingConfig `yaml:"threading,omitempty" json:"threading,omitempty"`
}

Expand Down
26 changes: 26 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,11 @@ to: <tmpl_string>
# Note that Go does not support unencrypted connections to remote SMTP endpoints.
[ require_tls: <bool> | default = global.smtp_require_tls ]

# Force use of implicit TLS (direct TLS connection) for better security.
# true: force use of implicit TLS (direct TLS connection on any port)
# nil (default): auto-detect based on port (465=implicit, other=explicit) for backward compatibility
[ implicit_tls: <bool> | default = nil ]

# TLS configuration.
tls_config:
[ <tls_config> | default = global.smtp_tls_config ]
Expand All @@ -1000,6 +1005,27 @@ tls_config:
# previously set by the notification implementation.
[ headers: { <string>: <tmpl_string>, ... } ]

#### Email TLS Configuration Examples

```yaml
# Example 1: Force implicit TLS on any port (recommended for security)
receivers:
- name: email-implicit-tls
email_configs:
- to: alerts@example.com
smarthost: smtp.example.com:8465
implicit_tls: true # Use direct TLS connection on port 8465

# Example 2: Backward compatible (no implicit_tls specified)
receivers:
- name: email-default
email_configs:
- to: alerts@example.com
smarthost: smtp.example.com:465 # Auto-detects implicit TLS
- to: alerts@example.com
smarthost: smtp.example.com:587 # Auto-detects explicit TLS
```

# Email threading configuration.
threading:
# Whether to enable threading, which makes alert notifications in the same
Expand Down
14 changes: 12 additions & 2 deletions notify/email/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,17 @@ func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
err error
success = false
)
if n.conf.Smarthost.Port == "465" {
// Determine whether to use Implicit TLS
var useImplicitTLS bool
if n.conf.ImplicitTLS != nil && *n.conf.ImplicitTLS {
// User explicitly configured to use implicit TLS
useImplicitTLS = true
} else {
// Default logic: port 465 uses implicit TLS (backward compatibility)
useImplicitTLS = n.conf.Smarthost.Port == "465"
}

if useImplicitTLS {
tlsConfig, err := commoncfg.NewTLSConfig(n.conf.TLSConfig)
if err != nil {
return false, fmt.Errorf("parse TLS configuration: %w", err)
Expand Down Expand Up @@ -173,7 +183,7 @@ func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
}

// Global Config guarantees RequireTLS is not nil.
if *n.conf.RequireTLS {
if *n.conf.RequireTLS && !useImplicitTLS {
if ok, _ := c.Extension("STARTTLS"); !ok {
return true, fmt.Errorf("'require_tls' is true (default) but %q does not advertise the STARTTLS extension", n.conf.Smarthost)
}
Expand Down
71 changes: 71 additions & 0 deletions notify/email/email_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -870,3 +870,74 @@ func TestEmailNotifyWithThreading(t *testing.T) {
})
}
}

func TestEmailImplicitTLS(t *testing.T) {
tests := []struct {
name string
port string
implicitTLS *bool
expectImplicit bool
}{
{
name: "default behavior - port 465",
port: "465",
implicitTLS: nil,
expectImplicit: true,
},
{
name: "default behavior - port 587",
port: "587",
implicitTLS: nil,
expectImplicit: false,
},
{
name: "force implicit_tls=true on port 587",
port: "587",
implicitTLS: ptrTo(true),
expectImplicit: true,
},
{
name: "force implicit_tls=true on custom port",
port: "8465",
implicitTLS: ptrTo(true),
expectImplicit: true,
},
{
name: "implicit_tls=false behaves like default on port 465",
port: "465",
implicitTLS: ptrTo(false),
expectImplicit: true,
},
{
name: "implicit_tls=false behaves like default on port 587",
port: "587",
implicitTLS: ptrTo(false),
expectImplicit: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &config.EmailConfig{
Smarthost: config.HostPort{Host: "localhost", Port: tt.port},
ImplicitTLS: tt.implicitTLS,
}

// Simulate the judgment logic
var useImplicitTLS bool
if cfg.ImplicitTLS != nil && *cfg.ImplicitTLS {
useImplicitTLS = true
} else {
useImplicitTLS = cfg.Smarthost.Port == "465"
}

require.Equal(t, tt.expectImplicit, useImplicitTLS,
"Expected useImplicitTLS=%v for port=%s with implicitTLS=%v",
tt.expectImplicit, tt.port, tt.implicitTLS)
})
}
}

func ptrTo(b bool) *bool {
return &b
}