summaryrefslogtreecommitdiffstats
path: root/modules/setting/mailer.go
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
commitdd136858f1ea40ad3c94191d647487fa4f31926c (patch)
tree58fec94a7b2a12510c9664b21793f1ed560c6518 /modules/setting/mailer.go
parentInitial commit. (diff)
downloadforgejo-upstream.tar.xz
forgejo-upstream.zip
Adding upstream version 9.0.0.upstream/9.0.0upstreamdebian
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'modules/setting/mailer.go')
-rw-r--r--modules/setting/mailer.go309
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
+}