diff options
Diffstat (limited to '')
-rw-r--r-- | modules/setting/mailer.go | 309 |
1 files changed, 309 insertions, 0 deletions
diff --git a/modules/setting/mailer.go b/modules/setting/mailer.go new file mode 100644 index 0000000..136d932 --- /dev/null +++ b/modules/setting/mailer.go @@ -0,0 +1,309 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "context" + "net" + "net/mail" + "strings" + "text/template" + "time" + + "code.gitea.io/gitea/modules/log" + + shellquote "github.com/kballard/go-shellquote" +) + +// Mailer represents mail service. +type Mailer struct { + // Mailer + Name string `ini:"NAME"` + From string `ini:"FROM"` + EnvelopeFrom string `ini:"ENVELOPE_FROM"` + OverrideEnvelopeFrom bool `ini:"-"` + FromName string `ini:"-"` + FromEmail string `ini:"-"` + SendAsPlainText bool `ini:"SEND_AS_PLAIN_TEXT"` + SubjectPrefix string `ini:"SUBJECT_PREFIX"` + OverrideHeader map[string][]string `ini:"-"` + + // SMTP sender + Protocol string `ini:"PROTOCOL"` + SMTPAddr string `ini:"SMTP_ADDR"` + SMTPPort string `ini:"SMTP_PORT"` + User string `ini:"USER"` + Passwd string `ini:"PASSWD"` + EnableHelo bool `ini:"ENABLE_HELO"` + HeloHostname string `ini:"HELO_HOSTNAME"` + ForceTrustServerCert bool `ini:"FORCE_TRUST_SERVER_CERT"` + UseClientCert bool `ini:"USE_CLIENT_CERT"` + ClientCertFile string `ini:"CLIENT_CERT_FILE"` + ClientKeyFile string `ini:"CLIENT_KEY_FILE"` + + // Sendmail sender + SendmailPath string `ini:"SENDMAIL_PATH"` + SendmailArgs []string `ini:"-"` + SendmailTimeout time.Duration `ini:"SENDMAIL_TIMEOUT"` + SendmailConvertCRLF bool `ini:"SENDMAIL_CONVERT_CRLF"` + + // Customization + FromDisplayNameFormat string `ini:"FROM_DISPLAY_NAME_FORMAT"` + FromDisplayNameFormatTemplate *template.Template `ini:"-"` +} + +// MailService the global mailer +var MailService *Mailer + +func loadMailsFrom(rootCfg ConfigProvider) { + loadMailerFrom(rootCfg) + loadRegisterMailFrom(rootCfg) + loadNotifyMailFrom(rootCfg) + loadIncomingEmailFrom(rootCfg) +} + +func loadMailerFrom(rootCfg ConfigProvider) { + sec := rootCfg.Section("mailer") + // Check mailer setting. + if !sec.Key("ENABLED").MustBool() { + return + } + + // Handle Deprecations and map on to new configuration + // DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version + // if these are removed, the warning will not be shown + deprecatedSetting(rootCfg, "mailer", "MAILER_TYPE", "mailer", "PROTOCOL", "v1.19.0") + if sec.HasKey("MAILER_TYPE") && !sec.HasKey("PROTOCOL") { + if sec.Key("MAILER_TYPE").String() == "sendmail" { + sec.Key("PROTOCOL").MustString("sendmail") + } + } + + deprecatedSetting(rootCfg, "mailer", "HOST", "mailer", "SMTP_ADDR", "v1.19.0") + if sec.HasKey("HOST") && !sec.HasKey("SMTP_ADDR") { + givenHost := sec.Key("HOST").String() + addr, port, err := net.SplitHostPort(givenHost) + if err != nil && strings.Contains(err.Error(), "missing port in address") { + addr = givenHost + } else if err != nil { + log.Fatal("Invalid mailer.HOST (%s): %v", givenHost, err) + } + if addr == "" { + addr = "127.0.0.1" + } + sec.Key("SMTP_ADDR").MustString(addr) + sec.Key("SMTP_PORT").MustString(port) + } + + deprecatedSetting(rootCfg, "mailer", "IS_TLS_ENABLED", "mailer", "PROTOCOL", "v1.19.0") + if sec.HasKey("IS_TLS_ENABLED") && !sec.HasKey("PROTOCOL") { + if sec.Key("IS_TLS_ENABLED").MustBool() { + sec.Key("PROTOCOL").MustString("smtps") + } else { + sec.Key("PROTOCOL").MustString("smtp+starttls") + } + } + + deprecatedSetting(rootCfg, "mailer", "DISABLE_HELO", "mailer", "ENABLE_HELO", "v1.19.0") + if sec.HasKey("DISABLE_HELO") && !sec.HasKey("ENABLE_HELO") { + sec.Key("ENABLE_HELO").MustBool(!sec.Key("DISABLE_HELO").MustBool()) + } + + deprecatedSetting(rootCfg, "mailer", "SKIP_VERIFY", "mailer", "FORCE_TRUST_SERVER_CERT", "v1.19.0") + if sec.HasKey("SKIP_VERIFY") && !sec.HasKey("FORCE_TRUST_SERVER_CERT") { + sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(sec.Key("SKIP_VERIFY").MustBool()) + } + + deprecatedSetting(rootCfg, "mailer", "USE_CERTIFICATE", "mailer", "USE_CLIENT_CERT", "v1.19.0") + if sec.HasKey("USE_CERTIFICATE") && !sec.HasKey("USE_CLIENT_CERT") { + sec.Key("USE_CLIENT_CERT").MustBool(sec.Key("USE_CERTIFICATE").MustBool()) + } + + deprecatedSetting(rootCfg, "mailer", "CERT_FILE", "mailer", "CLIENT_CERT_FILE", "v1.19.0") + if sec.HasKey("CERT_FILE") && !sec.HasKey("CLIENT_CERT_FILE") { + sec.Key("CERT_FILE").MustString(sec.Key("CERT_FILE").String()) + } + + deprecatedSetting(rootCfg, "mailer", "KEY_FILE", "mailer", "CLIENT_KEY_FILE", "v1.19.0") + if sec.HasKey("KEY_FILE") && !sec.HasKey("CLIENT_KEY_FILE") { + sec.Key("KEY_FILE").MustString(sec.Key("KEY_FILE").String()) + } + + deprecatedSetting(rootCfg, "mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT", "v1.19.0") + if sec.HasKey("ENABLE_HTML_ALTERNATIVE") && !sec.HasKey("SEND_AS_PLAIN_TEXT") { + sec.Key("SEND_AS_PLAIN_TEXT").MustBool(!sec.Key("ENABLE_HTML_ALTERNATIVE").MustBool(false)) + } + + if sec.HasKey("PROTOCOL") && sec.Key("PROTOCOL").String() == "smtp+startls" { + log.Error("Deprecated fallback `[mailer]` `PROTOCOL = smtp+startls` present. Use `[mailer]` `PROTOCOL = smtp+starttls`` instead. This fallback will be removed in v1.19.0") + sec.Key("PROTOCOL").SetValue("smtp+starttls") + } + + // Handle aliases + if sec.HasKey("USERNAME") && !sec.HasKey("USER") { + sec.Key("USER").SetValue(sec.Key("USERNAME").String()) + } + if sec.HasKey("PASSWORD") && !sec.HasKey("PASSWD") { + sec.Key("PASSWD").SetValue(sec.Key("PASSWORD").String()) + } + + // Set default values & validate + sec.Key("NAME").MustString(AppName) + sec.Key("PROTOCOL").In("", []string{"smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy"}) + sec.Key("ENABLE_HELO").MustBool(true) + sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(false) + sec.Key("USE_CLIENT_CERT").MustBool(false) + sec.Key("SENDMAIL_PATH").MustString("sendmail") + sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute) + sec.Key("SENDMAIL_CONVERT_CRLF").MustBool(true) + sec.Key("FROM").MustString(sec.Key("USER").String()) + + // Now map the values on to the MailService + MailService = &Mailer{} + if err := sec.MapTo(MailService); err != nil { + log.Fatal("Unable to map [mailer] section on to MailService. Error: %v", err) + } + + overrideHeader := rootCfg.Section("mailer.override_header").Keys() + MailService.OverrideHeader = make(map[string][]string) + for _, key := range overrideHeader { + MailService.OverrideHeader[key.Name()] = key.Strings(",") + } + + // Infer SMTPPort if not set + if MailService.SMTPPort == "" { + switch MailService.Protocol { + case "smtp": + MailService.SMTPPort = "25" + case "smtps": + MailService.SMTPPort = "465" + case "smtp+starttls": + MailService.SMTPPort = "587" + } + } + + // Infer Protocol + 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+starttls" + default: + log.Error("unable to infer unspecified mailer.PROTOCOL from mailer.SMTP_PORT = %q, assume using smtps", MailService.SMTPPort) + MailService.Protocol = "smtps" + if MailService.SMTPPort == "" { + MailService.SMTPPort = "465" + } + } + } + } + + // 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 + // This check is not needed for sendmail + switch MailService.Protocol { + case "sendmail": + var err error + MailService.SendmailArgs, err = shellquote.Split(sec.Key("SENDMAIL_ARGS").String()) + if err != nil { + log.Error("Failed to parse Sendmail args: '%s' with error %v", sec.Key("SENDMAIL_ARGS").String(), err) + } + case "smtp", "smtps", "smtp+starttls", "smtp+unix": + ips := tryResolveAddr(MailService.SMTPAddr) + if MailService.Protocol == "smtp" { + for _, ip := range ips { + if !ip.IP.IsLoopback() { + log.Warn("connecting over insecure SMTP protocol to non-local address is not recommended") + break + } + } + } + case "dummy": // just mention and do nothing + } + + 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.FromDisplayNameFormatTemplate, _ = template.New("mailFrom").Parse("{{ .DisplayName }}") + if MailService.FromDisplayNameFormat != "" { + template, err := template.New("mailFrom").Parse(MailService.FromDisplayNameFormat) + if err != nil { + log.Error("mailer.FROM_DISPLAY_NAME_FORMAT is no valid template: %v", err) + } else { + MailService.FromDisplayNameFormatTemplate = template + } + } + + switch MailService.EnvelopeFrom { + case "": + MailService.OverrideEnvelopeFrom = false + case "<>": + MailService.EnvelopeFrom = "" + MailService.OverrideEnvelopeFrom = true + default: + parsed, err := mail.ParseAddress(MailService.EnvelopeFrom) + if err != nil { + log.Fatal("Invalid mailer.ENVELOPE_FROM (%s): %v", MailService.EnvelopeFrom, err) + } + MailService.OverrideEnvelopeFrom = true + MailService.EnvelopeFrom = parsed.Address + } + + log.Info("Mail Service Enabled") +} + +func loadRegisterMailFrom(rootCfg ConfigProvider) { + if !rootCfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").MustBool() { + return + } else if MailService == nil { + log.Warn("Register Mail Service: Mail Service is not enabled") + return + } + Service.RegisterEmailConfirm = true + log.Info("Register Mail Service Enabled") +} + +func loadNotifyMailFrom(rootCfg ConfigProvider) { + if !rootCfg.Section("service").Key("ENABLE_NOTIFY_MAIL").MustBool() { + return + } else if MailService == nil { + log.Warn("Notify Mail Service: Mail Service is not enabled") + return + } + Service.EnableNotifyMail = true + log.Info("Notify Mail Service Enabled") +} + +func tryResolveAddr(addr string) []net.IPAddr { + if strings.HasPrefix(addr, "[") && strings.HasSuffix(addr, "]") { + addr = addr[1 : len(addr)-1] + } + ip := net.ParseIP(addr) + if ip != nil { + return []net.IPAddr{{IP: ip}} + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + ips, err := net.DefaultResolver.LookupIPAddr(ctx, addr) + if err != nil { + log.Warn("could not look up mailer.SMTP_ADDR: %v", err) + return nil + } + return ips +} |