Rework mailer settings (#18982)

* `PROTOCOL`: can be smtp, smtps, smtp+startls, smtp+unix, sendmail, dummy
* `SMTP_ADDR`: domain for SMTP, or path to unix socket
* `SMTP_PORT`: port for SMTP; defaults to 25 for `smtp`, 465 for `smtps`, and 587 for `smtp+startls`
* `ENABLE_HELO`, `HELO_HOSTNAME`: reverse `DISABLE_HELO` to `ENABLE_HELO`; default to false + system hostname
* `FORCE_TRUST_SERVER_CERT`: replace the unclear `SKIP_VERIFY`
* `CLIENT_CERT_FILE`, `CLIENT_KEY_FILE`, `USE_CLIENT_CERT`: clarify client certificates here

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
Clar Fon 2022-08-02 01:24:18 -04:00 committed by GitHub
parent ae3b88bef3
commit 036dd8a788
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 300 additions and 158 deletions

View file

@ -414,9 +414,9 @@ var (
Usage: "SMTP Authentication Type (PLAIN/LOGIN/CRAM-MD5) default PLAIN", Usage: "SMTP Authentication Type (PLAIN/LOGIN/CRAM-MD5) default PLAIN",
}, },
cli.StringFlag{ cli.StringFlag{
Name: "host", Name: "addr",
Value: "", Value: "",
Usage: "SMTP Host", Usage: "SMTP Addr",
}, },
cli.IntFlag{ cli.IntFlag{
Name: "port", Name: "port",
@ -956,8 +956,8 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
} }
conf.Auth = c.String("auth-type") conf.Auth = c.String("auth-type")
} }
if c.IsSet("host") { if c.IsSet("addr") {
conf.Host = c.String("host") conf.Addr = c.String("addr")
} }
if c.IsSet("port") { if c.IsSet("port") {
conf.Port = c.Int("port") conf.Port = c.Int("port")

View file

@ -1503,30 +1503,42 @@ ROUTER = console
;; Prefix displayed before subject in mail ;; Prefix displayed before subject in mail
;SUBJECT_PREFIX = ;SUBJECT_PREFIX =
;; ;;
;; Mail server ;; Mail server protocol. One of "smtp", "smtps", "smtp+startls", "smtp+unix", "sendmail", "dummy".
;; Gmail: smtp.gmail.com:587 ;; - sendmail: use the operating system's `sendmail` command instead of SMTP. This is common on Linux systems.
;; QQ: smtp.qq.com:465 ;; - dummy: send email messages to the log as a testing phase.
;; As per RFC 8314 using Implicit TLS/SMTPS on port 465 (if supported) is recommended, ;; If your provider does not explicitly say which protocol it uses but does provide a port,
;; otherwise STARTTLS on port 587 should be used. ;; you can set SMTP_PORT instead and this will be inferred.
;HOST = ;; (Before 1.18, this was controlled via MAILER_TYPE and IS_TLS_ENABLED.)
;PROTOCOL =
;; ;;
;; Disable HELO operation when hostnames are different. ;; Mail server address, e.g. smtp.gmail.com.
;DISABLE_HELO = ;; For smtp+unix, this should be a path to a unix socket instead.
;; (Before 1.18, this was combined with SMTP_PORT as HOST.)
;SMTP_ADDR =
;; ;;
;; Custom hostname for HELO operation, if no value is provided, one is retrieved from system. ;; 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.
;; (Before 1.18, this was combined with SMTP_ADDR as HOST.)
;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 = ;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. ;; If set to `true`, completely ignores server certificate validation errors.
;SKIP_VERIFY = false ;; This option is unsafe. Consider adding the certificate to the system trust store instead.
;FORCE_TRUST_SERVER_CERT = false
;; ;;
;; Use client certificate ;; Use client certificate in connection.
;USE_CERTIFICATE = false ;USE_CLIENT_CERT = false
;CERT_FILE = custom/mailer/cert.pem ;CLIENT_CERT_FILE = custom/mailer/cert.pem
;KEY_FILE = custom/mailer/key.pem ;CLIENT_KEY_FILE = custom/mailer/key.pem
;;
;; 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
;; ;;
;; Mail from address, RFC 5322. This can be just an email address, or the `"Name" <email@example.com>` format ;; Mail from address, RFC 5322. This can be just an email address, or the `"Name" <email@example.com>` format
;FROM = ;FROM =
@ -1534,19 +1546,15 @@ ROUTER = console
;; 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. ;; 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 = ;ENVELOPE_FROM =
;; ;;
;; Mailer user name and password ;; Mailer user name and password, if required by provider.
;; Please Note: Authentication is only supported when the SMTP server communication is encrypted with TLS (this can be via STARTTLS) or `HOST=localhost`.
;USER = ;USER =
;; ;;
;; Use PASSWD = `your password` for quoting if you use special characters in the password. ;; Use PASSWD = `your password` for quoting if you use special characters in the password.
;PASSWD = ;PASSWD =
;; ;;
;; Send mails as plain text ;; Send mails only in plain text, without HTML alternative
;SEND_AS_PLAIN_TEXT = false ;SEND_AS_PLAIN_TEXT = false
;; ;;
;; Set Mailer Type (either SMTP, sendmail or dummy to just send to the log)
;MAILER_TYPE = smtp
;;
;; Specify an alternative sendmail binary ;; Specify an alternative sendmail binary
;SENDMAIL_PATH = sendmail ;SENDMAIL_PATH = sendmail
;; ;;

View file

@ -647,41 +647,35 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type
## Mailer (`mailer`) ## Mailer (`mailer`)
- `ENABLED`: **false**: Enable to use a mail service. - `ENABLED`: **false**: Enable to use a mail service.
- `DISABLE_HELO`: **\<empty\>**: Disable HELO operation. - `PROTOCOL`: **\<empty\>**: Mail server protocol. One of "smtp", "smtps", "smtp+startls", "smtp+unix", "sendmail", "dummy". _Before 1.18, this was inferred from a combination of `MAILER_TYPE` and `IS_TLS_ENABLED`._
- `HELO_HOSTNAME`: **\<empty\>**: Custom hostname for HELO operation. - SMTP family, 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.
- `HOST`: **\<empty\>**: SMTP mail host address and port (example: smtp.gitea.io:587). - **sendmail** Use the operating system's `sendmail` command instead of SMTP. This is common on Linux systems.
- As per RFC 8314, if supported, Implicit TLS/SMTPS on port 465 is recommended, otherwise opportunistic TLS via STARTTLS on port 587 should be used. - **dummy** Send email messages to the log as a testing phase.
- `IS_TLS_ENABLED` : **false** : Forcibly use TLS to connect even if not on a default SMTPS port. - Note that enabling sendmail will ignore all other `mailer` settings except `ENABLED`, `FROM`, `SUBJECT_PREFIX` and `SENDMAIL_PATH`.
- Note, if the port ends with `465` Implicit TLS/SMTPS/SMTP over TLS will be used despite this setting. - Enabling dummy will ignore all settings except `ENABLED`, `SUBJECT_PREFIX` and `FROM`.
- Otherwise if `IS_TLS_ENABLED=false` and the server supports `STARTTLS` this will be used. Thus if `STARTTLS` is preferred you should set `IS_TLS_ENABLED=false`. - `SMTP_ADDR`: **\<empty\>**: Mail server address. e.g. smtp.gmail.com. For smtp+unix, this should be a path to a unix socket instead. _Before 1.18, this was combined with `SMTP_PORT` under the name `HOST`._
- `FROM`: **\<empty\>**: Mail from address, RFC 5322. This can be just an email address, or - `SMTP_PORT`: **\<empty\>**: Mail server port. If no protocol is specified, it will be inferred by this setting. Common ports are listed below. _Before 1.18, this was combined with `SMTP_ADDR` under the name `HOST`._
the "Name" \<email@example.com\> format. - 25: insecure SMTP
- `ENVELOPE_FROM`: **\<empty\>**: Address set as the From address on the SMTP mail envelope. Set to `<>` to send an empty address. - 465: SMTP Secure
- 587: StartTLS
- `USE_CLIENT_CERT`: **false**: Use client certificate for TLS/SSL.
- `CLIENT_CERT_FILE`: **custom/mailer/cert.pem**: Client certificate file.
- `CLIENT_KEY_FILE`: **custom/mailer/key.pem**: Client key file.
- `FORCE_TRUST_SERVER_CERT`: **false**: If set to `true`, completely ignores server certificate validation errors. This option is unsafe. Consider adding the certificate to the system trust store instead.
- `USER`: **\<empty\>**: Username of mailing user (usually the sender's e-mail address). - `USER`: **\<empty\>**: Username of mailing user (usually the sender's e-mail address).
- `PASSWD`: **\<empty\>**: Password of mailing user. Use \`your password\` for quoting if you use special characters in the password. - `PASSWD`: **\<empty\>**: Password of mailing user. Use \`your password\` for quoting if you use special characters in the password.
- Please note: authentication is only supported when the SMTP server communication is encrypted with TLS (this can be via `STARTTLS`) or `HOST=localhost`. See [Email Setup]({{< relref "doc/usage/email-setup.en-us.md" >}}) for more information. - Please note: authentication is only supported when the SMTP server communication is encrypted with TLS (this can be via `STARTTLS`) or SMTP host is localhost. See [Email Setup]({{< relref "doc/usage/email-setup.en-us.md" >}}) for more information.
- `SEND_AS_PLAIN_TEXT`: **false**: Send mails as plain text. - `ENABLE_HELO`: **true**: Enable HELO operation.
- `SKIP_VERIFY`: **false**: Whether or not to skip verification of certificates; `true` to disable verification. - `HELO_HOSTNAME`: **(retrieved from system)**: HELO hostname.
- **Warning:** This option is unsafe. Consider adding the certificate to the system trust store instead. - `FROM`: **\<empty\>**: Mail from address, RFC 5322. This can be just an email address, or the "Name" \<email@example.com\> format.
- **Note:** Gitea only supports SMTP with STARTTLS. - `ENVELOPE_FROM`: **\<empty\>**: Address set as the From address on the SMTP mail envelope. Set to `<>` to send an empty address.
- `USE_CERTIFICATE`: **false**: Use client certificate.
- `CERT_FILE`: **custom/mailer/cert.pem**
- `KEY_FILE`: **custom/mailer/key.pem**
- `SUBJECT_PREFIX`: **\<empty\>**: Prefix to be placed before e-mail subject lines. - `SUBJECT_PREFIX`: **\<empty\>**: Prefix to be placed before e-mail subject lines.
- `MAILER_TYPE`: **smtp**: \[smtp, sendmail, dummy\] - `SENDMAIL_PATH`: **sendmail**: The location of sendmail on the operating system (can be command or full path).
- **smtp** Use SMTP to send mail - `SENDMAIL_ARGS`: **\<empty\>**: Specify any extra sendmail arguments. (NOTE: you should be aware that email addresses can look like options - if your `sendmail` command takes options you must set the option terminator `--`)
- **sendmail** Use the operating system's `sendmail` command instead of SMTP.
This is common on Linux systems.
- **dummy** Send email messages to the log as a testing phase.
- Note that enabling sendmail will ignore all other `mailer` settings except `ENABLED`,
`FROM`, `SUBJECT_PREFIX` and `SENDMAIL_PATH`.
- Enabling dummy will ignore all settings except `ENABLED`, `SUBJECT_PREFIX` and `FROM`.
- `SENDMAIL_PATH`: **sendmail**: The location of sendmail on the operating system (can be
command or full path).
- `SENDMAIL_ARGS`: **_empty_**: Specify any extra sendmail arguments. (NOTE: you should be aware that email addresses can look like options - if your `sendmail` command takes options you must set the option terminator `--`)
- `SENDMAIL_TIMEOUT`: **5m**: default timeout for sending email through sendmail - `SENDMAIL_TIMEOUT`: **5m**: default timeout for sending email through sendmail
- `SENDMAIL_CONVERT_CRLF`: **true**: Most versions of sendmail prefer LF line endings rather than CRLF line endings. Set this to false if your version of sendmail requires CRLF line endings. - `SENDMAIL_CONVERT_CRLF`: **true**: Most versions of sendmail prefer LF line endings rather than CRLF line endings. Set this to false if your version of sendmail requires CRLF line endings.
- `SEND_BUFFER_LEN`: **100**: Buffer length of mailing queue. **DEPRECATED** use `LENGTH` in `[queue.mailer]` - `SEND_BUFFER_LEN`: **100**: Buffer length of mailing queue. **DEPRECATED** use `LENGTH` in `[queue.mailer]`
- `SEND_AS_PLAIN_TEXT`: **false**: Send mails only in plain text, without HTML alternative.
## Cache (`cache`) ## Cache (`cache`)

View file

@ -5,7 +5,9 @@
package setting package setting
import ( import (
"net"
"net/mail" "net/mail"
"strings"
"time" "time"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -23,18 +25,19 @@ type Mailer struct {
FromName string FromName string
FromEmail string FromEmail string
SendAsPlainText bool SendAsPlainText bool
MailerType string
SubjectPrefix string SubjectPrefix string
// SMTP sender // SMTP sender
Host string Protocol string
User, Passwd string SMTPAddr string
DisableHelo bool SMTPPort string
HeloHostname string User, Passwd string
SkipVerify bool EnableHelo bool
UseCertificate bool HeloHostname string
CertFile, KeyFile string ForceTrustServerCert bool
IsTLSEnabled bool UseClientCert bool
ClientCertFile string
ClientKeyFile string
// Sendmail sender // Sendmail sender
SendmailPath string SendmailPath string
@ -56,19 +59,19 @@ func newMailService() {
MailService = &Mailer{ MailService = &Mailer{
Name: sec.Key("NAME").MustString(AppName), Name: sec.Key("NAME").MustString(AppName),
SendAsPlainText: sec.Key("SEND_AS_PLAIN_TEXT").MustBool(false), SendAsPlainText: sec.Key("SEND_AS_PLAIN_TEXT").MustBool(false),
MailerType: sec.Key("MAILER_TYPE").In("", []string{"smtp", "sendmail", "dummy"}),
Host: sec.Key("HOST").String(), Protocol: sec.Key("PROTOCOL").In("", []string{"smtp", "smtps", "smtp+startls", "smtp+unix", "sendmail", "dummy"}),
User: sec.Key("USER").String(), SMTPAddr: sec.Key("SMTP_ADDR").String(),
Passwd: sec.Key("PASSWD").String(), SMTPPort: sec.Key("SMTP_PORT").String(),
DisableHelo: sec.Key("DISABLE_HELO").MustBool(), User: sec.Key("USER").String(),
HeloHostname: sec.Key("HELO_HOSTNAME").String(), Passwd: sec.Key("PASSWD").String(),
SkipVerify: sec.Key("SKIP_VERIFY").MustBool(), EnableHelo: sec.Key("ENABLE_HELO").MustBool(true),
UseCertificate: sec.Key("USE_CERTIFICATE").MustBool(), HeloHostname: sec.Key("HELO_HOSTNAME").String(),
CertFile: sec.Key("CERT_FILE").String(), ForceTrustServerCert: sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(false),
KeyFile: sec.Key("KEY_FILE").String(), UseClientCert: sec.Key("USE_CLIENT_CERT").MustBool(false),
IsTLSEnabled: sec.Key("IS_TLS_ENABLED").MustBool(), ClientCertFile: sec.Key("CLIENT_CERT_FILE").String(),
SubjectPrefix: sec.Key("SUBJECT_PREFIX").MustString(""), ClientKeyFile: sec.Key("CLIENT_KEY_FILE").String(),
SubjectPrefix: sec.Key("SUBJECT_PREFIX").MustString(""),
SendmailPath: sec.Key("SENDMAIL_PATH").MustString("sendmail"), SendmailPath: sec.Key("SENDMAIL_PATH").MustString("sendmail"),
SendmailTimeout: sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute), SendmailTimeout: sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute),
@ -77,26 +80,123 @@ func newMailService() {
MailService.From = sec.Key("FROM").MustString(MailService.User) MailService.From = sec.Key("FROM").MustString(MailService.User)
MailService.EnvelopeFrom = sec.Key("ENVELOPE_FROM").MustString("") MailService.EnvelopeFrom = sec.Key("ENVELOPE_FROM").MustString("")
// FIXME: DEPRECATED to be removed in v1.18.0 // FIXME: DEPRECATED to be removed in v1.19.0
deprecatedSetting("mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT") deprecatedSetting("mailer", "MAILER_TYPE", "mailer", "PROTOCOL")
if sec.HasKey("ENABLE_HTML_ALTERNATIVE") { if sec.HasKey("MAILER_TYPE") && !sec.HasKey("PROTOCOL") {
MailService.SendAsPlainText = !sec.Key("ENABLE_HTML_ALTERNATIVE").MustBool(false) if sec.Key("MAILER_TYPE").String() == "sendmail" {
} MailService.Protocol = "sendmail"
// FIXME: DEPRECATED to be removed in v1.18.0
deprecatedSetting("mailer", "USE_SENDMAIL", "mailer", "MAILER_TYPE")
if sec.HasKey("USE_SENDMAIL") {
if MailService.MailerType == "" && sec.Key("USE_SENDMAIL").MustBool(false) {
MailService.MailerType = "sendmail"
} }
} }
parsed, err := mail.ParseAddress(MailService.From) // FIXME: DEPRECATED to be removed in v1.19.0
if err != nil { deprecatedSetting("mailer", "HOST", "mailer", "SMTP_ADDR")
log.Fatal("Invalid mailer.FROM (%s): %v", MailService.From, err) if sec.HasKey("HOST") && !sec.HasKey("SMTP_ADDR") {
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.19.0
deprecatedSetting("mailer", "IS_TLS_ENABLED", "mailer", "PROTOCOL")
if sec.HasKey("IS_TLS_ENABLED") && !sec.HasKey("PROTOCOL") {
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.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.Error("unable to infer unspecified mailer.PROTOCOL from mailer.SMTP_PORT = %q, assume using smtps", MailService.SMTPPort)
MailService.Protocol = "smtps"
}
}
}
// we want to warn if users use SMTP on a non-local IP;
// we might as well take the opportunity to check that it has an IP at all
ips := tryResolveAddr(MailService.SMTPAddr)
if MailService.Protocol == "smtp" {
for _, ip := range ips {
if !ip.IsLoopback() {
log.Warn("connecting over insecure SMTP protocol to non-local address is not recommended")
break
}
}
}
// FIXME: DEPRECATED to be removed in v1.19.0
deprecatedSetting("mailer", "DISABLE_HELO", "mailer", "ENABLE_HELO")
if sec.HasKey("DISABLE_HELO") && !sec.HasKey("ENABLE_HELO") {
MailService.EnableHelo = !sec.Key("DISABLE_HELO").MustBool()
}
// FIXME: DEPRECATED to be removed in v1.19.0
deprecatedSetting("mailer", "SKIP_VERIFY", "mailer", "FORCE_TRUST_SERVER_CERT")
if sec.HasKey("SKIP_VERIFY") && !sec.HasKey("FORCE_TRUST_SERVER_CERT") {
MailService.ForceTrustServerCert = sec.Key("SKIP_VERIFY").MustBool()
}
// FIXME: DEPRECATED to be removed in v1.19.0
deprecatedSetting("mailer", "USE_CERTIFICATE", "mailer", "USE_CLIENT_CERT")
if sec.HasKey("USE_CERTIFICATE") && !sec.HasKey("USE_CLIENT_CERT") {
MailService.UseClientCert = sec.Key("USE_CLIENT_CERT").MustBool()
}
// FIXME: DEPRECATED to be removed in v1.19.0
deprecatedSetting("mailer", "CERT_FILE", "mailer", "CLIENT_CERT_FILE")
if sec.HasKey("CERT_FILE") && !sec.HasKey("CLIENT_CERT_FILE") {
MailService.ClientCertFile = sec.Key("CERT_FILE").String()
}
// FIXME: DEPRECATED to be removed in v1.19.0
deprecatedSetting("mailer", "KEY_FILE", "mailer", "CLIENT_KEY_FILE")
if sec.HasKey("KEY_FILE") && !sec.HasKey("CLIENT_KEY_FILE") {
MailService.ClientKeyFile = sec.Key("KEY_FILE").String()
}
// FIXME: DEPRECATED to be removed in v1.19.0
deprecatedSetting("mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT")
if sec.HasKey("ENABLE_HTML_ALTERNATIVE") && !sec.HasKey("SEND_AS_PLAIN_TEXT") {
MailService.SendAsPlainText = !sec.Key("ENABLE_HTML_ALTERNATIVE").MustBool(false)
}
if MailService.From != "" {
parsed, err := mail.ParseAddress(MailService.From)
if err != nil {
log.Fatal("Invalid mailer.FROM (%s): %v", MailService.From, err)
}
MailService.FromName = parsed.Name
MailService.FromEmail = parsed.Address
} else {
log.Error("no mailer.FROM provided, email system may not work.")
} }
MailService.FromName = parsed.Name
MailService.FromEmail = parsed.Address
switch MailService.EnvelopeFrom { switch MailService.EnvelopeFrom {
case "": case "":
@ -105,7 +205,7 @@ func newMailService() {
MailService.EnvelopeFrom = "" MailService.EnvelopeFrom = ""
MailService.OverrideEnvelopeFrom = true MailService.OverrideEnvelopeFrom = true
default: default:
parsed, err = mail.ParseAddress(MailService.EnvelopeFrom) parsed, err := mail.ParseAddress(MailService.EnvelopeFrom)
if err != nil { if err != nil {
log.Fatal("Invalid mailer.ENVELOPE_FROM (%s): %v", MailService.EnvelopeFrom, err) log.Fatal("Invalid mailer.ENVELOPE_FROM (%s): %v", MailService.EnvelopeFrom, err)
} }
@ -113,11 +213,8 @@ func newMailService() {
MailService.EnvelopeFrom = parsed.Address MailService.EnvelopeFrom = parsed.Address
} }
if MailService.MailerType == "" { if MailService.Protocol == "sendmail" {
MailService.MailerType = "smtp" var err error
}
if MailService.MailerType == "sendmail" {
MailService.SendmailArgs, err = shellquote.Split(sec.Key("SENDMAIL_ARGS").String()) MailService.SendmailArgs, err = shellquote.Split(sec.Key("SENDMAIL_ARGS").String())
if err != nil { if err != nil {
log.Error("Failed to parse Sendmail args: %s with error %v", CustomConf, err) log.Error("Failed to parse Sendmail args: %s with error %v", CustomConf, err)
@ -148,3 +245,21 @@ func newNotifyMailService() {
Service.EnableNotifyMail = true Service.EnableNotifyMail = true
log.Info("Notify Mail Service Enabled") log.Info("Notify Mail Service Enabled")
} }
func tryResolveAddr(addr string) []net.IP {
if strings.HasPrefix(addr, "[") && strings.HasSuffix(addr, "]") {
addr = addr[1 : len(addr)-1]
}
ip := net.ParseIP(addr)
if ip != nil {
ips := make([]net.IP, 1)
ips[0] = ip
return ips
}
ips, err := net.LookupIP(addr)
if err != nil {
log.Warn("could not look up mailer.SMTP_ADDR: %v", err)
return make([]net.IP, 0)
}
return ips
}

View file

@ -179,7 +179,8 @@ log_root_path_helper = Log files will be written to this directory.
optional_title = Optional Settings optional_title = Optional Settings
email_title = Email Settings email_title = Email Settings
smtp_host = SMTP Host smtp_addr = SMTP Host
smtp_port = SMTP Port
smtp_from = Send Email As smtp_from = Send Email As
smtp_from_helper = Email address Gitea will use. Enter a plain email address or use the "Name" <email@example.com> format. smtp_from_helper = Email address Gitea will use. Enter a plain email address or use the "Name" <email@example.com> format.
mailer_user = SMTP Username mailer_user = SMTP Username

View file

@ -133,7 +133,8 @@ func Install(ctx *context.Context) {
// E-mail service settings // E-mail service settings
if setting.MailService != nil { 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.SMTPFrom = setting.MailService.From
form.SMTPUser = setting.MailService.User form.SMTPUser = setting.MailService.User
form.SMTPPasswd = setting.MailService.Passwd form.SMTPPasswd = setting.MailService.Passwd
@ -421,9 +422,10 @@ func SubmitInstall(ctx *context.Context) {
cfg.Section("server").Key("LFS_START_SERVER").SetValue("false") 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("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("FROM").SetValue(form.SMTPFrom)
cfg.Section("mailer").Key("USER").SetValue(form.SMTPUser) cfg.Section("mailer").Key("USER").SetValue(form.SMTPUser)
cfg.Section("mailer").Key("PASSWD").SetValue(form.SMTPPasswd) cfg.Section("mailer").Key("PASSWD").SetValue(form.SMTPPasswd)

View file

@ -159,7 +159,7 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source { func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
return &smtp.Source{ return &smtp.Source{
Auth: form.SMTPAuth, Auth: form.SMTPAuth,
Host: form.SMTPHost, Addr: form.SMTPAddr,
Port: form.SMTPPort, Port: form.SMTPPort,
AllowedDomains: form.AllowedDomains, AllowedDomains: form.AllowedDomains,
ForceSMTPS: form.ForceSMTPS, ForceSMTPS: form.ForceSMTPS,

View file

@ -58,10 +58,10 @@ var ErrUnsupportedLoginType = errors.New("Login source is unknown")
func Authenticate(a smtp.Auth, source *Source) error { func Authenticate(a smtp.Auth, source *Source) error {
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
InsecureSkipVerify: source.SkipVerify, 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 { if err != nil {
return err return err
} }
@ -71,7 +71,7 @@ func Authenticate(a smtp.Auth, source *Source) error {
conn = tls.Client(conn, tlsConfig) conn = tls.Client(conn, tlsConfig)
} }
client, err := smtp.NewClient(conn, source.Host) client, err := smtp.NewClient(conn, source.Addr)
if err != nil { if err != nil {
return fmt.Errorf("failed to create NewClient: %w", err) return fmt.Errorf("failed to create NewClient: %w", err)
} }

View file

@ -19,7 +19,7 @@ import (
// Source holds configuration for the SMTP login source. // Source holds configuration for the SMTP login source.
type Source struct { type Source struct {
Auth string Auth string
Host string Addr string
Port int Port int
AllowedDomains string `xorm:"TEXT"` AllowedDomains string `xorm:"TEXT"`
ForceSMTPS bool ForceSMTPS bool

View file

@ -32,7 +32,7 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
var auth smtp.Auth var auth smtp.Auth
switch source.Auth { switch source.Auth {
case PlainAuthentication: case PlainAuthentication:
auth = smtp.PlainAuth("", userName, password, source.Host) auth = smtp.PlainAuth("", userName, password, source.Addr)
case LoginAuthentication: case LoginAuthentication:
auth = &loginAuthenticator{userName, password} auth = &loginAuthenticator{userName, password}
case CRAMMD5Authentication: case CRAMMD5Authentication:

View file

@ -45,7 +45,7 @@ type AuthenticationForm struct {
IsActive bool IsActive bool
IsSyncEnabled bool IsSyncEnabled bool
SMTPAuth string SMTPAuth string
SMTPHost string SMTPAddr string
SMTPPort int SMTPPort int
AllowedDomains string AllowedDomains string
SecurityProtocol int `binding:"Range(0,2)"` SecurityProtocol int `binding:"Range(0,2)"`

View file

@ -40,7 +40,8 @@ type InstallForm struct {
AppURL string `binding:"Required"` AppURL string `binding:"Required"`
LogRootPath string `binding:"Required"` LogRootPath string `binding:"Required"`
SMTPHost string SMTPAddr string
SMTPPort string
SMTPFrom string SMTPFrom string
SMTPUser string `binding:"OmitEmpty;MaxSize(254)" locale:"install.mailer_user"` SMTPUser string `binding:"OmitEmpty;MaxSize(254)" locale:"install.mailer_user"`
SMTPPasswd string SMTPPasswd string

View file

@ -147,65 +147,82 @@ type smtpSender struct{}
func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error { func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
opts := setting.MailService opts := setting.MailService
host, port, err := net.SplitHostPort(opts.Host) 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)
}
conn, err := net.Dial(network, address)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to establish network connection to SMTP server: %v", err)
}
tlsconfig := &tls.Config{
InsecureSkipVerify: opts.SkipVerify,
ServerName: host,
}
if opts.UseCertificate {
cert, err := tls.LoadX509KeyPair(opts.CertFile, opts.KeyFile)
if err != nil {
return err
}
tlsconfig.Certificates = []tls.Certificate{cert}
}
conn, err := net.Dial("tcp", net.JoinHostPort(host, port))
if err != nil {
return err
} }
defer conn.Close() defer conn.Close()
isSecureConn := opts.IsTLSEnabled || (strings.HasSuffix(port, "465")) var tlsconfig *tls.Config
// Start TLS directly if the port ends with 465 (SMTPS protocol) if opts.Protocol == "smtps" || opts.Protocol == "smtp+startls" {
if isSecureConn { tlsconfig = &tls.Config{
InsecureSkipVerify: opts.ForceTrustServerCert,
ServerName: opts.SMTPAddr,
}
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}
}
}
if opts.Protocol == "smtps" {
conn = tls.Client(conn, tlsconfig) conn = tls.Client(conn, tlsconfig)
} }
host := "localhost"
if opts.Protocol == "smtp+unix" {
host = opts.SMTPAddr
}
client, err := smtp.NewClient(conn, host) client, err := smtp.NewClient(conn, host)
if err != nil { 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 hostname := opts.HeloHostname
if len(hostname) == 0 { if len(hostname) == 0 {
hostname, err = os.Hostname() hostname, err = os.Hostname()
if err != nil { if err != nil {
return err return fmt.Errorf("could not retrieve system hostname: %v", err)
} }
} }
if err = client.Hello(hostname); err != nil { 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 if opts.Protocol == "smtp+startls" {
hasStartTLS, _ := client.Extension("STARTTLS") hasStartTLS, _ := client.Extension("STARTTLS")
if !isSecureConn && hasStartTLS { if hasStartTLS {
if err = client.StartTLS(tlsconfig); err != nil { if err = client.StartTLS(tlsconfig); err != nil {
return fmt.Errorf("StartTLS: %v", err) 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") 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 var auth smtp.Auth
if strings.Contains(options, "CRAM-MD5") { 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 auth != nil {
if err = client.Auth(auth); err != 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 opts.OverrideEnvelopeFrom {
if err = client.Mail(opts.EnvelopeFrom); err != nil { 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 { } else {
if err = client.Mail(from); err != nil { 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 { for _, rec := range to {
if err = client.Rcpt(rec); err != nil { 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() w, err := client.Data()
if err != nil { 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 { } 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 { } else if err = w.Close(); err != nil {
return fmt.Errorf("Close: %v", err) return fmt.Errorf("SMTP close failed: %v", err)
} }
return client.Quit() return client.Quit()
@ -338,13 +355,13 @@ func NewContext() {
return return
} }
switch setting.MailService.MailerType { switch setting.MailService.Protocol {
case "smtp":
Sender = &smtpSender{}
case "sendmail": case "sendmail":
Sender = &sendmailSender{} Sender = &sendmailSender{}
case "dummy": case "dummy":
Sender = &dummySender{} Sender = &dummySender{}
default:
Sender = &smtpSender{}
} }
mailQueue = queue.CreateQueue("mail", func(data ...queue.Data) []queue.Data { mailQueue = queue.CreateQueue("mail", func(data ...queue.Data) []queue.Data {

View file

@ -173,8 +173,12 @@
{{.locale.Tr "install.email_title"}} {{.locale.Tr "install.email_title"}}
</summary> </summary>
<div class="inline field"> <div class="inline field">
<label for="smtp_host">{{.locale.Tr "install.smtp_host"}}</label> <label for="smtp_addr">{{.locale.Tr "install.smtp_addr"}}</label>
<input id="smtp_host" name="smtp_host" value="{{.smtp_host}}"> <input id="smtp_addr" name="smtp_addr" value="{{.smtp_addr}}">
</div>
<div class="inline field">
<label for="smtp_port">{{.locale.Tr "install.smtp_port"}}</label>
<input id="smtp_port" name="smtp_port" value="{{.smtp_port}}">
</div> </div>
<div class="inline field {{if .Err_SMTPFrom}}error{{end}}"> <div class="inline field {{if .Err_SMTPFrom}}error{{end}}">
<label for="smtp_from">{{.locale.Tr "install.smtp_from"}}</label> <label for="smtp_from">{{.locale.Tr "install.smtp_from"}}</label>