1// Copyright 2019 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package setting
5
6import (
7 "context"
8 "net"
9 "net/mail"
10 "strings"
11 "text/template"
12 "time"
13
14 "forgejo.org/modules/log"
15
16 shellquote "github.com/kballard/go-shellquote"
17)
18
19// Mailer represents mail service.
20type Mailer struct {
21 // Mailer
22 Name string `ini:"NAME"`
23 From string `ini:"FROM"`
24 EnvelopeFrom string `ini:"ENVELOPE_FROM"`
25 OverrideEnvelopeFrom bool `ini:"-"`
26 FromName string `ini:"-"`
27 FromEmail string `ini:"-"`
28 SendAsPlainText bool `ini:"SEND_AS_PLAIN_TEXT"`
29 SubjectPrefix string `ini:"SUBJECT_PREFIX"`
30 OverrideHeader map[string][]string `ini:"-"`
31
32 // SMTP sender
33 Protocol string `ini:"PROTOCOL"`
34 SMTPAddr string `ini:"SMTP_ADDR"`
35 SMTPPort string `ini:"SMTP_PORT"`
36 User string `ini:"USER"`
37 Passwd string `ini:"PASSWD"`
38 EnableHelo bool `ini:"ENABLE_HELO"`
39 HeloHostname string `ini:"HELO_HOSTNAME"`
40 ForceTrustServerCert bool `ini:"FORCE_TRUST_SERVER_CERT"`
41 UseClientCert bool `ini:"USE_CLIENT_CERT"`
42 ClientCertFile string `ini:"CLIENT_CERT_FILE"`
43 ClientKeyFile string `ini:"CLIENT_KEY_FILE"`
44
45 // Sendmail sender
46 SendmailPath string `ini:"SENDMAIL_PATH"`
47 SendmailArgs []string `ini:"-"`
48 SendmailTimeout time.Duration `ini:"SENDMAIL_TIMEOUT"`
49 SendmailConvertCRLF bool `ini:"SENDMAIL_CONVERT_CRLF"`
50
51 // Customization
52 FromDisplayNameFormat string `ini:"FROM_DISPLAY_NAME_FORMAT"`
53 FromDisplayNameFormatTemplate *template.Template `ini:"-"`
54}
55
56// MailService the global mailer
57var MailService *Mailer
58
59func loadMailsFrom(rootCfg ConfigProvider) {
60 loadMailerFrom(rootCfg)
61 loadRegisterMailFrom(rootCfg)
62 loadNotifyMailFrom(rootCfg)
63 loadIncomingEmailFrom(rootCfg)
64}
65
66func loadMailerFrom(rootCfg ConfigProvider) {
67 sec := rootCfg.Section("mailer")
68 // Check mailer setting.
69 if !sec.Key("ENABLED").MustBool() {
70 return
71 }
72
73 // Handle Deprecations and map on to new configuration
74 // DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
75 // if these are removed, the warning will not be shown
76 deprecatedSetting(rootCfg, "mailer", "MAILER_TYPE", "mailer", "PROTOCOL", "v1.19.0")
77 if sec.HasKey("MAILER_TYPE") && !sec.HasKey("PROTOCOL") {
78 if sec.Key("MAILER_TYPE").String() == "sendmail" {
79 sec.Key("PROTOCOL").MustString("sendmail")
80 }
81 }
82
83 deprecatedSetting(rootCfg, "mailer", "HOST", "mailer", "SMTP_ADDR", "v1.19.0")
84 if sec.HasKey("HOST") && !sec.HasKey("SMTP_ADDR") {
85 givenHost := sec.Key("HOST").String()
86 addr, port, err := net.SplitHostPort(givenHost)
87 if err != nil && strings.Contains(err.Error(), "missing port in address") {
88 addr = givenHost
89 } else if err != nil {
90 log.Fatal("Invalid mailer.HOST (%s): %v", givenHost, err)
91 }
92 if addr == "" {
93 addr = "127.0.0.1"
94 }
95 sec.Key("SMTP_ADDR").MustString(addr)
96 sec.Key("SMTP_PORT").MustString(port)
97 }
98
99 deprecatedSetting(rootCfg, "mailer", "IS_TLS_ENABLED", "mailer", "PROTOCOL", "v1.19.0")
100 if sec.HasKey("IS_TLS_ENABLED") && !sec.HasKey("PROTOCOL") {
101 if sec.Key("IS_TLS_ENABLED").MustBool() {
102 sec.Key("PROTOCOL").MustString("smtps")
103 } else {
104 sec.Key("PROTOCOL").MustString("smtp+starttls")
105 }
106 }
107
108 deprecatedSetting(rootCfg, "mailer", "DISABLE_HELO", "mailer", "ENABLE_HELO", "v1.19.0")
109 if sec.HasKey("DISABLE_HELO") && !sec.HasKey("ENABLE_HELO") {
110 sec.Key("ENABLE_HELO").MustBool(!sec.Key("DISABLE_HELO").MustBool())
111 }
112
113 deprecatedSetting(rootCfg, "mailer", "SKIP_VERIFY", "mailer", "FORCE_TRUST_SERVER_CERT", "v1.19.0")
114 if sec.HasKey("SKIP_VERIFY") && !sec.HasKey("FORCE_TRUST_SERVER_CERT") {
115 sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(sec.Key("SKIP_VERIFY").MustBool())
116 }
117
118 deprecatedSetting(rootCfg, "mailer", "USE_CERTIFICATE", "mailer", "USE_CLIENT_CERT", "v1.19.0")
119 if sec.HasKey("USE_CERTIFICATE") && !sec.HasKey("USE_CLIENT_CERT") {
120 sec.Key("USE_CLIENT_CERT").MustBool(sec.Key("USE_CERTIFICATE").MustBool())
121 }
122
123 deprecatedSetting(rootCfg, "mailer", "CERT_FILE", "mailer", "CLIENT_CERT_FILE", "v1.19.0")
124 if sec.HasKey("CERT_FILE") && !sec.HasKey("CLIENT_CERT_FILE") {
125 sec.Key("CERT_FILE").MustString(sec.Key("CERT_FILE").String())
126 }
127
128 deprecatedSetting(rootCfg, "mailer", "KEY_FILE", "mailer", "CLIENT_KEY_FILE", "v1.19.0")
129 if sec.HasKey("KEY_FILE") && !sec.HasKey("CLIENT_KEY_FILE") {
130 sec.Key("KEY_FILE").MustString(sec.Key("KEY_FILE").String())
131 }
132
133 deprecatedSetting(rootCfg, "mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT", "v1.19.0")
134 if sec.HasKey("ENABLE_HTML_ALTERNATIVE") && !sec.HasKey("SEND_AS_PLAIN_TEXT") {
135 sec.Key("SEND_AS_PLAIN_TEXT").MustBool(!sec.Key("ENABLE_HTML_ALTERNATIVE").MustBool(false))
136 }
137
138 if sec.HasKey("PROTOCOL") && sec.Key("PROTOCOL").String() == "smtp+startls" {
139 log.Error("Deprecated fallback `[mailer]` `PROTOCOL = smtp+startls` present. Use `[mailer]` `PROTOCOL = smtp+starttls`` instead. This fallback will be removed in v1.19.0")
140 sec.Key("PROTOCOL").SetValue("smtp+starttls")
141 }
142
143 // Handle aliases
144 if sec.HasKey("USERNAME") && !sec.HasKey("USER") {
145 sec.Key("USER").SetValue(sec.Key("USERNAME").String())
146 }
147 if sec.HasKey("PASSWORD") && !sec.HasKey("PASSWD") {
148 sec.Key("PASSWD").SetValue(sec.Key("PASSWORD").String())
149 }
150
151 // Set default values & validate
152 sec.Key("NAME").MustString(AppName)
153 sec.Key("PROTOCOL").In("", []string{"smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy"})
154 sec.Key("ENABLE_HELO").MustBool(true)
155 sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(false)
156 sec.Key("USE_CLIENT_CERT").MustBool(false)
157 sec.Key("SENDMAIL_PATH").MustString("sendmail")
158 sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute)
159 sec.Key("SENDMAIL_CONVERT_CRLF").MustBool(true)
160 sec.Key("FROM").MustString(sec.Key("USER").String())
161
162 // Now map the values on to the MailService
163 MailService = &Mailer{}
164 if err := sec.MapTo(MailService); err != nil {
165 log.Fatal("Unable to map [mailer] section on to MailService. Error: %v", err)
166 }
167
168 overrideHeader := rootCfg.Section("mailer.override_header").Keys()
169 MailService.OverrideHeader = make(map[string][]string)
170 for _, key := range overrideHeader {
171 MailService.OverrideHeader[key.Name()] = key.Strings(",")
172 }
173
174 // Infer SMTPPort if not set
175 if MailService.SMTPPort == "" {
176 switch MailService.Protocol {
177 case "smtp":
178 MailService.SMTPPort = "25"
179 case "smtps":
180 MailService.SMTPPort = "465"
181 case "smtp+starttls":
182 MailService.SMTPPort = "587"
183 }
184 }
185
186 // Infer Protocol
187 if MailService.Protocol == "" {
188 if strings.ContainsAny(MailService.SMTPAddr, "/\\") {
189 MailService.Protocol = "smtp+unix"
190 } else {
191 switch MailService.SMTPPort {
192 case "25":
193 MailService.Protocol = "smtp"
194 case "465":
195 MailService.Protocol = "smtps"
196 case "587":
197 MailService.Protocol = "smtp+starttls"
198 default:
199 log.Error("unable to infer unspecified mailer.PROTOCOL from mailer.SMTP_PORT = %q, assume using smtps", MailService.SMTPPort)
200 MailService.Protocol = "smtps"
201 if MailService.SMTPPort == "" {
202 MailService.SMTPPort = "465"
203 }
204 }
205 }
206 }
207
208 // we want to warn if users use SMTP on a non-local IP;
209 // we might as well take the opportunity to check that it has an IP at all
210 // This check is not needed for sendmail
211 switch MailService.Protocol {
212 case "sendmail":
213 var err error
214 MailService.SendmailArgs, err = shellquote.Split(sec.Key("SENDMAIL_ARGS").String())
215 if err != nil {
216 log.Error("Failed to parse Sendmail args: '%s' with error %v", sec.Key("SENDMAIL_ARGS").String(), err)
217 }
218 case "smtp", "smtps", "smtp+starttls", "smtp+unix":
219 ips := tryResolveAddr(MailService.SMTPAddr)
220 if MailService.Protocol == "smtp" {
221 for _, ip := range ips {
222 if !ip.IP.IsLoopback() {
223 log.Warn("connecting over insecure SMTP protocol to non-local address is not recommended")
224 break
225 }
226 }
227 }
228 case "dummy": // just mention and do nothing
229 }
230
231 if MailService.From != "" {
232 parsed, err := mail.ParseAddress(MailService.From)
233 if err != nil {
234 log.Fatal("Invalid mailer.FROM (%s): %v", MailService.From, err)
235 }
236 MailService.FromName = parsed.Name
237 MailService.FromEmail = parsed.Address
238 } else {
239 log.Error("no mailer.FROM provided, email system may not work.")
240 }
241
242 MailService.FromDisplayNameFormatTemplate, _ = template.New("mailFrom").Parse("{{ .DisplayName }}")
243 if MailService.FromDisplayNameFormat != "" {
244 template, err := template.New("mailFrom").Parse(MailService.FromDisplayNameFormat)
245 if err != nil {
246 log.Error("mailer.FROM_DISPLAY_NAME_FORMAT is no valid template: %v", err)
247 } else {
248 MailService.FromDisplayNameFormatTemplate = template
249 }
250 }
251
252 switch MailService.EnvelopeFrom {
253 case "":
254 MailService.OverrideEnvelopeFrom = false
255 case "<>":
256 MailService.EnvelopeFrom = ""
257 MailService.OverrideEnvelopeFrom = true
258 default:
259 parsed, err := mail.ParseAddress(MailService.EnvelopeFrom)
260 if err != nil {
261 log.Fatal("Invalid mailer.ENVELOPE_FROM (%s): %v", MailService.EnvelopeFrom, err)
262 }
263 MailService.OverrideEnvelopeFrom = true
264 MailService.EnvelopeFrom = parsed.Address
265 }
266}
267
268func loadRegisterMailFrom(rootCfg ConfigProvider) {
269 if !rootCfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").MustBool() {
270 return
271 } else if MailService == nil {
272 log.Warn("Register Mail Service: Mail Service is not enabled")
273 return
274 }
275 Service.RegisterEmailConfirm = true
276}
277
278func loadNotifyMailFrom(rootCfg ConfigProvider) {
279 if !rootCfg.Section("service").Key("ENABLE_NOTIFY_MAIL").MustBool() {
280 return
281 } else if MailService == nil {
282 log.Warn("Notify Mail Service: Mail Service is not enabled")
283 return
284 }
285 Service.EnableNotifyMail = true
286}
287
288func tryResolveAddr(addr string) []net.IPAddr {
289 if strings.HasPrefix(addr, "[") && strings.HasSuffix(addr, "]") {
290 addr = addr[1 : len(addr)-1]
291 }
292 ip := net.ParseIP(addr)
293 if ip != nil {
294 return []net.IPAddr{{IP: ip}}
295 }
296
297 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
298 defer cancel()
299 ips, err := net.DefaultResolver.LookupIPAddr(ctx, addr)
300 if err != nil {
301 log.Warn("could not look up mailer.SMTP_ADDR: %v", err)
302 return nil
303 }
304 return ips
305}