diff --git a/cmd/admin.go b/cmd/admin.go index e4a254c613909..988eecadffbb5 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -404,9 +404,9 @@ var ( Usage: "SMTP Authentication Type (PLAIN/LOGIN/CRAM-MD5) default PLAIN", }, cli.StringFlag{ - Name: "host", + Name: "addr", Value: "", - Usage: "SMTP Host", + Usage: "SMTP Addr", }, cli.IntFlag{ Name: "port", @@ -936,8 +936,8 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error { } conf.Auth = c.String("auth-type") } - if c.IsSet("host") { - conf.Host = c.String("host") + if c.IsSet("addr") { + conf.Addr = c.String("addr") } if c.IsSet("port") { conf.Port = c.Int("port") diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index cbfde9c8d8cc3..101fa33c15d41 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1503,30 +1503,37 @@ PATH = ;; Prefix displayed before subject in mail ;SUBJECT_PREFIX = ;; -;; Mail server -;; Gmail: smtp.gmail.com:587 -;; QQ: smtp.qq.com:465 -;; As per RFC 8314 using Implicit TLS/SMTPS on port 465 (if supported) is recommended, -;; otherwise STARTTLS on port 587 should be used. -;HOST = -;; -;; Disable HELO operation when hostnames are different. -;DISABLE_HELO = -;; -;; Custom hostname for HELO operation, if no value is provided, one is retrieved from system. +;; Mail server protocol. One of "smtp", "smtps", "smtp+startls", "smtp+unix". +;; If your provider does not explicitly say which protocol it uses but does provide a port, +;; you can set SMTP_PORT instead and this will be inferred. +;PROTOCOL = +; +;; Mail server address, e.g. smtp.gmail.com. +;; For smtp+unix, this should be a path to a unix socket instead. +;SMTP_ADDR = +;; +;; Mail server port. Common ports are: +;; 25: insecure SMTP +;; 465: SMTP Secure +;; 587: StartTLS +;; If no protocol is specified, it will be inferred by this setting. +;SMTP_PORT = +;; +;; Enable HELO operation. Defaults to true. +;ENABLE_HELO = true +;; +;; Custom hostname for HELO operation. +;; If no value is provided, one is retrieved from system. ;HELO_HOSTNAME = ;; -;; Whether or not to skip verification of certificates; `true` to disable verification. This option is unsafe. Consider adding the certificate to the system trust store instead. -;SKIP_VERIFY = false -;; -;; Use client certificate -;USE_CERTIFICATE = false -;CERT_FILE = custom/mailer/cert.pem -;KEY_FILE = custom/mailer/key.pem +;; If set to `true`, completely ignores server certificate validation errors. +;; This option is unsafe. Consider adding the certificate to the system trust store instead. +;FORCE_TRUST_SERVER_CERT = false ;; -;; Should SMTP connect with TLS, (if port ends with 465 TLS will always be used.) -;; If this is false but STARTTLS is supported the connection will be upgraded to TLS opportunistically. -;IS_TLS_ENABLED = false +;; Use client certificate in connection. +;USE_CLIENT_CERT = false +;CLIENT_CERT_FILE = custom/mailer/cert.pem +;CLIENT_KEY_FILE = custom/mailer/key.pem ;; ;; Mail from address, RFC 5322. This can be just an email address, or the `"Name" ` format ;FROM = @@ -1534,8 +1541,7 @@ PATH = ;; Sometimes it is helpful to use a different address on the envelope. Set this to use ENVELOPE_FROM as the from on the envelope. Set to `<>` to send an empty address. ;ENVELOPE_FROM = ;; -;; Mailer user name and password -;; Please Note: Authentication is only supported when the SMTP server communication is encrypted with TLS (this can be via STARTTLS) or `HOST=localhost`. +;; Mailer user name and password, if required by provider. ;USER = ;; ;; Use PASSWD = `your password` for quoting if you use special characters in the password. diff --git a/modules/setting/mailer.go b/modules/setting/mailer.go index 8a26f8b0c49f8..2887e9ca083d1 100644 --- a/modules/setting/mailer.go +++ b/modules/setting/mailer.go @@ -5,7 +5,9 @@ package setting import ( + "net" "net/mail" + "strings" "time" "code.gitea.io/gitea/modules/log" @@ -27,14 +29,16 @@ type Mailer struct { SubjectPrefix string // SMTP sender - Host string - User, Passwd string - DisableHelo bool - HeloHostname string - SkipVerify bool - UseCertificate bool - CertFile, KeyFile string - IsTLSEnabled bool + Protocol string + SMTPAddr string + SMTPPort string + User, Passwd string + EnableHelo bool + HeloHostname string + ForceTrustServerCert bool + UseClientCert bool + ClientCertFile string + ClientKeyFile string // Sendmail sender SendmailPath string @@ -58,17 +62,18 @@ func newMailService() { SendAsPlainText: sec.Key("SEND_AS_PLAIN_TEXT").MustBool(false), MailerType: sec.Key("MAILER_TYPE").In("", []string{"smtp", "sendmail", "dummy"}), - Host: sec.Key("HOST").String(), - User: sec.Key("USER").String(), - Passwd: sec.Key("PASSWD").String(), - DisableHelo: sec.Key("DISABLE_HELO").MustBool(), - HeloHostname: sec.Key("HELO_HOSTNAME").String(), - SkipVerify: sec.Key("SKIP_VERIFY").MustBool(), - UseCertificate: sec.Key("USE_CERTIFICATE").MustBool(), - CertFile: sec.Key("CERT_FILE").String(), - KeyFile: sec.Key("KEY_FILE").String(), - IsTLSEnabled: sec.Key("IS_TLS_ENABLED").MustBool(), - SubjectPrefix: sec.Key("SUBJECT_PREFIX").MustString(""), + Protocol: sec.Key("PROTOCOL").In("", []string{"smtp", "smtps", "smtp+startls", "smtp+unix"}), + SMTPAddr: sec.Key("SMTP_ADDR").String(), + SMTPPort: sec.Key("SMTP_PORT").String(), + User: sec.Key("USER").String(), + Passwd: sec.Key("PASSWD").String(), + EnableHelo: sec.Key("ENABLE_HELO").MustBool(true), + HeloHostname: sec.Key("HELO_HOSTNAME").String(), + ForceTrustServerCert: sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(false), + UseClientCert: sec.Key("USE_CLIENT_CERT").MustBool(false), + ClientCertFile: sec.Key("CLIENT_CERT_FILE").String(), + ClientKeyFile: sec.Key("CLIENT_KEY_FILE").String(), + SubjectPrefix: sec.Key("SUBJECT_PREFIX").MustString(""), SendmailPath: sec.Key("SENDMAIL_PATH").MustString("sendmail"), SendmailTimeout: sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute), @@ -77,12 +82,6 @@ func newMailService() { MailService.From = sec.Key("FROM").MustString(MailService.User) MailService.EnvelopeFrom = sec.Key("ENVELOPE_FROM").MustString("") - // FIXME: DEPRECATED to be removed in v1.18.0 - deprecatedSetting("mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT") - if sec.HasKey("ENABLE_HTML_ALTERNATIVE") { - MailService.SendAsPlainText = !sec.Key("ENABLE_HTML_ALTERNATIVE").MustBool(false) - } - // FIXME: DEPRECATED to be removed in v1.18.0 deprecatedSetting("mailer", "USE_SENDMAIL", "mailer", "MAILER_TYPE") if sec.HasKey("USE_SENDMAIL") { @@ -91,6 +90,109 @@ func newMailService() { } } + if MailService.MailerType == "" { + MailService.MailerType = "smtp" + } + + // FIXME: DEPRECATED to be removed in v1.18.0 + deprecatedSetting("mailer", "HOST", "mailer", "SMTP_ADDR") + if sec.HasKey("HOST") { + givenHost := sec.Key("HOST").String() + addr, port, err := net.SplitHostPort(givenHost) + if err != nil { + log.Fatal("Invalid mailer.HOST (%s): %v", givenHost, err) + } + MailService.SMTPAddr = addr + MailService.SMTPPort = port + } + + // FIXME: DEPRECATED to be removed in v1.18.0 + deprecatedSetting("mailer", "IS_TLS_ENABLED", "mailer", "PROTOCOL") + if sec.HasKey("IS_TLS_ENABLED") { + if sec.Key("IS_TLS_ENABLED").MustBool() { + MailService.Protocol = "smtps" + } else { + MailService.Protocol = "smtp+startls" + } + } + + if MailService.SMTPPort == "" { + switch MailService.Protocol { + case "smtp": + MailService.SMTPPort = "25" + case "smtps": + MailService.SMTPPort = "465" + case "smtp+startls": + MailService.SMTPPort = "587" + } + } + + if MailService.MailerType == "smtp" && MailService.Protocol == "" { + if strings.ContainsAny(MailService.SMTPAddr, "/\\") { + MailService.Protocol = "smtp+unix" + } else { + switch MailService.SMTPPort { + case "25": + MailService.Protocol = "smtp" + case "465": + MailService.Protocol = "smtps" + case "587": + MailService.Protocol = "smtp+startls" + default: + log.Fatal("unable to infer unspecified mailer.PROTOCOL from mailer.SMTP_PORT = \"%s\"", MailService.SMTPPort) + } + } + } + + if MailService.Protocol == "smtp" { + switch MailService.SMTPAddr { + case "localhost": + case "127.0.0.1": + case "::1": + case "[::1]": + // this is a local address, so, we're fine + break + default: + log.Warn("connect via insecure SMTP to non-local address") + } + } + + // FIXME: DEPRECATED to be removed in v1.18.0 + deprecatedSetting("mailer", "DISABLE_HELO", "mailer", "ENABLE_HELO") + if sec.HasKey("DISABLE_HELO") { + MailService.EnableHelo = !sec.Key("DISABLE_HELO").MustBool() + } + + // FIXME: DEPRECATED to be removed in v1.18.0 + deprecatedSetting("mailer", "SKIP_VERIFY", "mailer", "FORCE_TRUST_SERVER_CERT") + if sec.HasKey("SKIP_VERIFY") { + MailService.ForceTrustServerCert = sec.Key("SKIP_VERIFY").MustBool() + } + + // FIXME: DEPRECATED to be removed in v1.18.0 + deprecatedSetting("mailer", "USE_CERTIFICATE", "mailer", "USE_CLIENT_CERT") + if sec.HasKey("USE_CERTIFICATE") { + MailService.UseClientCert = sec.Key("USE_CLIENT_CERT").MustBool() + } + + // FIXME: DEPRECATED to be removed in v1.18.0 + deprecatedSetting("mailer", "CERT_FILE", "mailer", "CLIENT_CERT_FILE") + if sec.HasKey("CERT_FILE") { + MailService.ClientCertFile = sec.Key("CERT_FILE").String() + } + + // FIXME: DEPRECATED to be removed in v1.18.0 + deprecatedSetting("mailer", "KEY_FILE", "mailer", "CLIENT_KEY_FILE") + if sec.HasKey("KEY_FILE") { + MailService.ClientKeyFile = sec.Key("KEY_FILE").String() + } + + // FIXME: DEPRECATED to be removed in v1.18.0 + deprecatedSetting("mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT") + if sec.HasKey("ENABLE_HTML_ALTERNATIVE") { + MailService.SendAsPlainText = !sec.Key("ENABLE_HTML_ALTERNATIVE").MustBool(false) + } + parsed, err := mail.ParseAddress(MailService.From) if err != nil { log.Fatal("Invalid mailer.FROM (%s): %v", MailService.From, err) @@ -113,10 +215,6 @@ func newMailService() { MailService.EnvelopeFrom = parsed.Address } - if MailService.MailerType == "" { - MailService.MailerType = "smtp" - } - if MailService.MailerType == "sendmail" { MailService.SendmailArgs, err = shellquote.Split(sec.Key("SENDMAIL_ARGS").String()) if err != nil { diff --git a/routers/install/install.go b/routers/install/install.go index ec1719439f53a..f05664b9162d9 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -131,7 +131,8 @@ func Install(ctx *context.Context) { // E-mail service settings if setting.MailService != nil { - form.SMTPHost = setting.MailService.Host + form.SMTPAddr = setting.MailService.SMTPAddr + form.SMTPPort = setting.MailService.SMTPPort form.SMTPFrom = setting.MailService.From form.SMTPUser = setting.MailService.User } @@ -418,9 +419,10 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("server").Key("LFS_START_SERVER").SetValue("false") } - if len(strings.TrimSpace(form.SMTPHost)) > 0 { + if len(strings.TrimSpace(form.SMTPAddr)) > 0 { cfg.Section("mailer").Key("ENABLED").SetValue("true") - cfg.Section("mailer").Key("HOST").SetValue(form.SMTPHost) + cfg.Section("mailer").Key("SMTP_ADDR").SetValue(form.SMTPAddr) + cfg.Section("mailer").Key("SMTP_PORT").SetValue(form.SMTPPort) cfg.Section("mailer").Key("FROM").SetValue(form.SMTPFrom) cfg.Section("mailer").Key("USER").SetValue(form.SMTPUser) cfg.Section("mailer").Key("PASSWD").SetValue(form.SMTPPasswd) diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 1d72a88aa1bd9..815269d3a5a9d 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -159,7 +159,7 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source { func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source { return &smtp.Source{ Auth: form.SMTPAuth, - Host: form.SMTPHost, + Addr: form.SMTPAddr, Port: form.SMTPPort, AllowedDomains: form.AllowedDomains, ForceSMTPS: form.ForceSMTPS, diff --git a/services/auth/source/smtp/auth.go b/services/auth/source/smtp/auth.go index 8d0cbb11cdc9a..a9e4b0e5f4454 100644 --- a/services/auth/source/smtp/auth.go +++ b/services/auth/source/smtp/auth.go @@ -58,10 +58,10 @@ var ErrUnsupportedLoginType = errors.New("Login source is unknown") func Authenticate(a smtp.Auth, source *Source) error { tlsConfig := &tls.Config{ InsecureSkipVerify: source.SkipVerify, - ServerName: source.Host, + ServerName: source.Addr, } - conn, err := net.Dial("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port))) + conn, err := net.Dial("tcp", net.JoinHostPort(source.Addr, strconv.Itoa(source.Port))) if err != nil { return err } @@ -71,7 +71,7 @@ func Authenticate(a smtp.Auth, source *Source) error { conn = tls.Client(conn, tlsConfig) } - client, err := smtp.NewClient(conn, source.Host) + client, err := smtp.NewClient(conn, source.Addr) if err != nil { return fmt.Errorf("failed to create NewClient: %w", err) } diff --git a/services/auth/source/smtp/source.go b/services/auth/source/smtp/source.go index 5e69f912da35b..b2286d42a0ff7 100644 --- a/services/auth/source/smtp/source.go +++ b/services/auth/source/smtp/source.go @@ -19,7 +19,7 @@ import ( // Source holds configuration for the SMTP login source. type Source struct { Auth string - Host string + Addr string Port int AllowedDomains string `xorm:"TEXT"` ForceSMTPS bool diff --git a/services/auth/source/smtp/source_authenticate.go b/services/auth/source/smtp/source_authenticate.go index 3be2f1128de2f..6e3c5037877aa 100644 --- a/services/auth/source/smtp/source_authenticate.go +++ b/services/auth/source/smtp/source_authenticate.go @@ -32,7 +32,7 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str var auth smtp.Auth switch source.Auth { case PlainAuthentication: - auth = smtp.PlainAuth("", userName, password, source.Host) + auth = smtp.PlainAuth("", userName, password, source.Addr) case LoginAuthentication: auth = &loginAuthenticator{userName, password} case CRAMMD5Authentication: diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go index 7e7c75675299b..9064be2cca38e 100644 --- a/services/forms/auth_form.go +++ b/services/forms/auth_form.go @@ -45,7 +45,7 @@ type AuthenticationForm struct { IsActive bool IsSyncEnabled bool SMTPAuth string - SMTPHost string + SMTPAddr string SMTPPort int AllowedDomains string SecurityProtocol int `binding:"Range(0,2)"` diff --git a/services/forms/user_form.go b/services/forms/user_form.go index 405b4a9a490f2..c8f2b02d8c809 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -40,7 +40,8 @@ type InstallForm struct { AppURL string `binding:"Required"` LogRootPath string `binding:"Required"` - SMTPHost string + SMTPAddr string + SMTPPort string SMTPFrom string SMTPUser string `binding:"OmitEmpty;MaxSize(254)" locale:"install.mailer_user"` SMTPPasswd string diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go index 3ca9b50fc6147..11f4386972a5c 100644 --- a/services/mailer/mailer.go +++ b/services/mailer/mailer.go @@ -147,65 +147,82 @@ type smtpSender struct{} func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error { opts := setting.MailService - host, port, err := net.SplitHostPort(opts.Host) - if err != nil { - return err + var network string + var address string + if opts.Protocol == "smtp+unix" { + network = "unix" + address = opts.SMTPAddr + } else { + network = "tcp" + address = net.JoinHostPort(opts.SMTPAddr, opts.SMTPPort) } - tlsconfig := &tls.Config{ - InsecureSkipVerify: opts.SkipVerify, - ServerName: host, + conn, err := net.Dial(network, address) + if err != nil { + return fmt.Errorf("failed to establish network connection to SMTP server: %v", err) } + defer conn.Close() - if opts.UseCertificate { - cert, err := tls.LoadX509KeyPair(opts.CertFile, opts.KeyFile) - if err != nil { - return err + var tlsconfig *tls.Config + if opts.Protocol == "smtps" || opts.Protocol == "smtp+startls" { + tlsconfig = &tls.Config{ + InsecureSkipVerify: opts.ForceTrustServerCert, + ServerName: opts.SMTPAddr, } - tlsconfig.Certificates = []tls.Certificate{cert} - } - conn, err := net.Dial("tcp", net.JoinHostPort(host, port)) - if err != nil { - return err + if opts.UseClientCert { + cert, err := tls.LoadX509KeyPair(opts.ClientCertFile, opts.ClientKeyFile) + if err != nil { + return fmt.Errorf("could not load SMTP client certificate: %v", err) + } + tlsconfig.Certificates = []tls.Certificate{cert} + } } - defer conn.Close() - isSecureConn := opts.IsTLSEnabled || (strings.HasSuffix(port, "465")) - // Start TLS directly if the port ends with 465 (SMTPS protocol) - if isSecureConn { + if opts.Protocol == "smtps" { conn = tls.Client(conn, tlsconfig) } + host := "localhost" + if opts.Protocol == "smtp+unix" { + host = opts.SMTPAddr + } client, err := smtp.NewClient(conn, host) if err != nil { - return fmt.Errorf("NewClient: %v", err) + return fmt.Errorf("could not initiate SMTP session: %v", err) } - if !opts.DisableHelo { + if opts.EnableHelo { hostname := opts.HeloHostname if len(hostname) == 0 { hostname, err = os.Hostname() if err != nil { - return err + return fmt.Errorf("could not retrieve system hostname: %v", err) } } if err = client.Hello(hostname); err != nil { - return fmt.Errorf("Hello: %v", err) + return fmt.Errorf("failed to issue HELO command: %v", err) } } - // If not using SMTPS, always use STARTTLS if available - hasStartTLS, _ := client.Extension("STARTTLS") - if !isSecureConn && hasStartTLS { - if err = client.StartTLS(tlsconfig); err != nil { - return fmt.Errorf("StartTLS: %v", err) + if opts.Protocol == "smtp+startls" { + hasStartTLS, _ := client.Extension("STARTTLS") + if hasStartTLS { + if err = client.StartTLS(tlsconfig); err != nil { + return fmt.Errorf("failed to start TLS connection: %v", err) + } + } else { + log.Warn("StartTLS requested, but SMTP server does not support it; falling back to regular SMTP") } } canAuth, options := client.Extension("AUTH") - if canAuth && len(opts.User) > 0 { + if len(opts.User) > 0 { + if !canAuth { + return fmt.Errorf("SMTP server does not support AUTH, but credentials provided") + } + var auth smtp.Auth if strings.Contains(options, "CRAM-MD5") { @@ -219,34 +236,34 @@ func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error { if auth != nil { if err = client.Auth(auth); err != nil { - return fmt.Errorf("Auth: %v", err) + return fmt.Errorf("failed to authenticate SMTP: %v", err) } } } if opts.OverrideEnvelopeFrom { if err = client.Mail(opts.EnvelopeFrom); err != nil { - return fmt.Errorf("Mail: %v", err) + return fmt.Errorf("failed to issue MAIL command: %v", err) } } else { if err = client.Mail(from); err != nil { - return fmt.Errorf("Mail: %v", err) + return fmt.Errorf("failed to issue MAIL command: %v", err) } } for _, rec := range to { if err = client.Rcpt(rec); err != nil { - return fmt.Errorf("Rcpt: %v", err) + return fmt.Errorf("failed to issue RCPT command: %v", err) } } w, err := client.Data() if err != nil { - return fmt.Errorf("Data: %v", err) + return fmt.Errorf("failed to issue DATA command: %v", err) } else if _, err = msg.WriteTo(w); err != nil { - return fmt.Errorf("WriteTo: %v", err) + return fmt.Errorf("SMTP write failed: %v", err) } else if err = w.Close(); err != nil { - return fmt.Errorf("Close: %v", err) + return fmt.Errorf("SMTP close failed: %v", err) } return client.Quit()