Commit 90622795ab
Verified · cmc
Layout: unified · split
internal/config/config.go +28 −1
| @@ -209,10 +209,34 @@ type Limits struct { | |||
| 209 | } | 209 | } |
| 210 | 210 | ||
| 211 | type Mail struct { | 211 | type Mail struct { |
| 212 | SMTPHost string `toml:"smtp_host"` // host:port (port defaults to 587) | 212 | SMTPHost string `toml:"smtp_host"` // host:port (port defaults to 587, 465 with tls = "implicit") |
| 213 | From string `toml:"from"` | 213 | From string `toml:"from"` |
| 214 | SMTPUser string `toml:"smtp_user,omitempty"` | 214 | SMTPUser string `toml:"smtp_user,omitempty"` |
| 215 | SMTPPass string `toml:"smtp_pass,omitempty"` | 215 | SMTPPass string `toml:"smtp_pass,omitempty"` |
| 216 | // RequireTLS fails delivery when the relay does not offer STARTTLS, | ||
| 217 | // instead of sending in clear. Unset, it is on for any relay but | ||
| 218 | // localhost or a loopback address (TLSRequired). | ||
| 219 | RequireTLS *bool `toml:"require_tls,omitempty"` | ||
| 220 | // TLS is "starttls" (the default, also when empty) or "implicit": | ||
| 221 | // TLS from the first byte, as relays on port 465 expect. | ||
| 222 | TLS string `toml:"tls,omitempty"` | ||
| 223 | } | ||
| 224 | |||
| 225 | // TLSRequired reports whether mail must not go to the relay in clear. | ||
| 226 | func (m Mail) TLSRequired() bool { | ||
| 227 | if m.RequireTLS != nil { | ||
| 228 | return *m.RequireTLS | ||
| 229 | } | ||
| 230 | host := m.SMTPHost | ||
| 231 | if h, _, err := net.SplitHostPort(host); err == nil { | ||
| 232 | host = h | ||
| 233 | } | ||
| 234 | host = strings.Trim(host, "[]") | ||
| 235 | if host == "localhost" { | ||
| 236 | return false | ||
| 237 | } | ||
| 238 | ip := net.ParseIP(host) | ||
| 239 | return ip == nil || !ip.IsLoopback() | ||
| 216 | } | 240 | } |
| 217 | 241 | ||
| 218 | // Push is APNs delivery to registered Apple devices. A key belongs to a | 242 | // Push is APNs delivery to registered Apple devices. A key belongs to a |
| @@ -406,6 +430,9 @@ func (c Config) Validate() error { | |||
| 406 | if c.Mail.SMTPHost != "" && c.Mail.From == "" { | 430 | if c.Mail.SMTPHost != "" && c.Mail.From == "" { |
| 407 | errs = append(errs, errors.New("[mail] from is required when smtp_host is set")) | 431 | errs = append(errs, errors.New("[mail] from is required when smtp_host is set")) |
| 408 | } | 432 | } |
| 433 | if t := c.Mail.TLS; t != "" && t != "starttls" && t != "implicit" { | ||
| 434 | errs = append(errs, fmt.Errorf("mail.tls must be starttls or implicit, got %q", t)) | ||
| 435 | } | ||
| 409 | if c.Registration.Mode != "closed" && c.Mail.SMTPHost == "" { | 436 | if c.Registration.Mode != "closed" && c.Mail.SMTPHost == "" { |
| 410 | errs = append(errs, fmt.Errorf( | 437 | errs = append(errs, fmt.Errorf( |
| 411 | "registration.mode = %q requires [mail] smtp_host: email verification cannot run without SMTP", | 438 | "registration.mode = %q requires [mail] smtp_host: email verification cannot run without SMTP", |
internal/config/config_test.go +26
| @@ -65,6 +65,11 @@ func TestContradictions(t *testing.T) { | |||
| 65 | minimal + "\n[ssh]\nmode = \"system\"\n[registration]\nmode = \"open\"\n[mail]\nsmtp_host = \"mx.example\"\nfrom = \"gitbay@example\"\n", | 65 | minimal + "\n[ssh]\nmode = \"system\"\n[registration]\nmode = \"open\"\n[mail]\nsmtp_host = \"mx.example\"\nfrom = \"gitbay@example\"\n", |
| 66 | "requires registration.mode = \"closed\"", | 66 | "requires registration.mode = \"closed\"", |
| 67 | }, | 67 | }, |
| 68 | { | ||
| 69 | "unknown mail.tls", | ||
| 70 | minimal + "\n[mail]\nsmtp_host = \"mx.example\"\nfrom = \"gitbay@example\"\ntls = \"ssl\"\n", | ||
| 71 | "mail.tls must be starttls or implicit", | ||
| 72 | }, | ||
| 68 | { | 73 | { |
| 69 | "password auth in view_only", | 74 | "password auth in view_only", |
| 70 | minimal + "\n[web]\nmode = \"view_only\"\npassword_auth = true\n", | 75 | minimal + "\n[web]\nmode = \"view_only\"\npassword_auth = true\n", |
| @@ -249,3 +254,24 @@ func TestPushHost(t *testing.T) { | |||
| 249 | t.Fatalf("GITBAY_APNS_HOST ignored: %q", got) | 254 | t.Fatalf("GITBAY_APNS_HOST ignored: %q", got) |
| 250 | } | 255 | } |
| 251 | } | 256 | } |
| 257 | |||
| 258 | func TestMailTLSRequired(t *testing.T) { | ||
| 259 | off, on := false, true | ||
| 260 | for _, tc := range []struct { | ||
| 261 | m Mail | ||
| 262 | want bool | ||
| 263 | }{ | ||
| 264 | {Mail{SMTPHost: "smtp.example.com:587"}, true}, | ||
| 265 | {Mail{SMTPHost: "smtp.example.com"}, true}, | ||
| 266 | {Mail{SMTPHost: "localhost:25"}, false}, | ||
| 267 | {Mail{SMTPHost: "localhost"}, false}, | ||
| 268 | {Mail{SMTPHost: "127.0.0.1:25"}, false}, | ||
| 269 | {Mail{SMTPHost: "[::1]:25"}, false}, | ||
| 270 | {Mail{SMTPHost: "smtp.example.com:587", RequireTLS: &off}, false}, | ||
| 271 | {Mail{SMTPHost: "127.0.0.1:25", RequireTLS: &on}, true}, | ||
| 272 | } { | ||
| 273 | if got := tc.m.TLSRequired(); got != tc.want { | ||
| 274 | t.Errorf("%+v: TLSRequired = %v, want %v", tc.m, got, tc.want) | ||
| 275 | } | ||
| 276 | } | ||
| 277 | } | ||