Commit 90622795ab

90622795ab3fac4f00599bf708b93d4eb1aafa5f

parent: 928b919242

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 08:29 UTC

config: mail.require_tls and mail.tls

Ref #280

Layout: unified · split

internal/config/config.go +28 −1
@@ -209,10 +209,34 @@ type Limits struct {
209209}
210210
211211type 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")
213213 From string `toml:"from"`
214214 SMTPUser string `toml:"smtp_user,omitempty"`
215215 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.
226func (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()
216240}
217241
218242// Push is APNs delivery to registered Apple devices. A key belongs to a
@@ -406,6 +430,9 @@ func (c Config) Validate() error {
406430 if c.Mail.SMTPHost != "" && c.Mail.From == "" {
407431 errs = append(errs, errors.New("[mail] from is required when smtp_host is set"))
408432 }
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 }
409436 if c.Registration.Mode != "closed" && c.Mail.SMTPHost == "" {
410437 errs = append(errs, fmt.Errorf(
411438 "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) {
6565 minimal + "\n[ssh]\nmode = \"system\"\n[registration]\nmode = \"open\"\n[mail]\nsmtp_host = \"mx.example\"\nfrom = \"gitbay@example\"\n",
6666 "requires registration.mode = \"closed\"",
6767 },
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 },
6873 {
6974 "password auth in view_only",
7075 minimal + "\n[web]\nmode = \"view_only\"\npassword_auth = true\n",
@@ -249,3 +254,24 @@ func TestPushHost(t *testing.T) {
249254 t.Fatalf("GITBAY_APNS_HOST ignored: %q", got)
250255 }
251256}
257
258func 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}