Add option for mailer to override mail headers (#27860)

Add option to override headers of mails, gitea send out

---
*Sponsored by Kithara Software GmbH*
This commit is contained in:
6543 2024-06-03 20:42:52 +02:00 committed by GitHub
parent 8c68c5e436
commit aace3bccc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 128 additions and 10 deletions

@ -1687,6 +1687,16 @@ LEVEL = Info
;; convert \r\n to \n for Sendmail ;; convert \r\n to \n for Sendmail
;SENDMAIL_CONVERT_CRLF = true ;SENDMAIL_CONVERT_CRLF = true
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[mailer.override_header]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; This is empty by default, use it only if you know what you need it for.
;Reply-To = test@example.com, test2@example.com
;Content-Type = text/html; charset=utf-8
;In-Reply-To =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[email.incoming] ;[email.incoming]

@ -724,11 +724,13 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type
## Mailer (`mailer`) ## Mailer (`mailer`)
⚠️ This section is for Gitea 1.18 and later. If you are using Gitea 1.17 or older, :::warning
This section is for Gitea 1.18 and later. If you are using Gitea 1.17 or older,
please refer to please refer to
[Gitea 1.17 app.ini example](https://github.com/go-gitea/gitea/blob/release/v1.17/custom/conf/app.example.ini) [Gitea 1.17 app.ini example](https://github.com/go-gitea/gitea/blob/release/v1.17/custom/conf/app.example.ini)
and and
[Gitea 1.17 configuration document](https://github.com/go-gitea/gitea/blob/release/v1.17/docs/content/doc/advanced/config-cheat-sheet.en-us.md) [Gitea 1.17 configuration document](https://github.com/go-gitea/gitea/blob/release/v1.17/docs/content/doc/advanced/config-cheat-sheet.en-us.md)
:::
- `ENABLED`: **false**: Enable to use a mail service. - `ENABLED`: **false**: Enable to use a mail service.
- `PROTOCOL`: **_empty_**: Mail server protocol. One of "smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy". _Before 1.18, this was inferred from a combination of `MAILER_TYPE` and `IS_TLS_ENABLED`._ - `PROTOCOL`: **_empty_**: Mail server protocol. One of "smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy". _Before 1.18, this was inferred from a combination of `MAILER_TYPE` and `IS_TLS_ENABLED`._
@ -761,6 +763,21 @@ and
- `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. - `SEND_AS_PLAIN_TEXT`: **false**: Send mails only in plain text, without HTML alternative.
## Override Email Headers (`mailer.override_header`)
:::warning
This is empty by default, use it only if you know what you need it for.
:::
examples would be:
```ini
[mailer.override_header]
Reply-To = test@example.com, test2@example.com
Content-Type = text/html; charset=utf-8
In-Reply-To =
```
## Incoming Email (`email.incoming`) ## Incoming Email (`email.incoming`)
- `ENABLED`: **false**: Enable handling of incoming emails. - `ENABLED`: **false**: Enable handling of incoming emails.

@ -18,14 +18,15 @@ import (
// Mailer represents mail service. // Mailer represents mail service.
type Mailer struct { type Mailer struct {
// Mailer // Mailer
Name string `ini:"NAME"` Name string `ini:"NAME"`
From string `ini:"FROM"` From string `ini:"FROM"`
EnvelopeFrom string `ini:"ENVELOPE_FROM"` EnvelopeFrom string `ini:"ENVELOPE_FROM"`
OverrideEnvelopeFrom bool `ini:"-"` OverrideEnvelopeFrom bool `ini:"-"`
FromName string `ini:"-"` FromName string `ini:"-"`
FromEmail string `ini:"-"` FromEmail string `ini:"-"`
SendAsPlainText bool `ini:"SEND_AS_PLAIN_TEXT"` SendAsPlainText bool `ini:"SEND_AS_PLAIN_TEXT"`
SubjectPrefix string `ini:"SUBJECT_PREFIX"` SubjectPrefix string `ini:"SUBJECT_PREFIX"`
OverrideHeader map[string][]string `ini:"-"`
// SMTP sender // SMTP sender
Protocol string `ini:"PROTOCOL"` Protocol string `ini:"PROTOCOL"`
@ -151,6 +152,12 @@ func loadMailerFrom(rootCfg ConfigProvider) {
log.Fatal("Unable to map [mailer] section on to MailService. Error: %v", err) 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 // Infer SMTPPort if not set
if MailService.SMTPPort == "" { if MailService.SMTPPort == "" {
switch MailService.Protocol { switch MailService.Protocol {

@ -57,7 +57,7 @@ func (m *Message) ToMessage() *gomail.Message {
msg.SetHeader(header, m.Headers[header]...) msg.SetHeader(header, m.Headers[header]...)
} }
if len(setting.MailService.SubjectPrefix) > 0 { if setting.MailService.SubjectPrefix != "" {
msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject) msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject)
} else { } else {
msg.SetHeader("Subject", m.Subject) msg.SetHeader("Subject", m.Subject)
@ -79,6 +79,14 @@ func (m *Message) ToMessage() *gomail.Message {
if len(msg.GetHeader("Message-ID")) == 0 { if len(msg.GetHeader("Message-ID")) == 0 {
msg.SetHeader("Message-ID", m.generateAutoMessageID()) msg.SetHeader("Message-ID", m.generateAutoMessageID())
} }
for k, v := range setting.MailService.OverrideHeader {
if len(msg.GetHeader(k)) != 0 {
log.Debug("Mailer override header '%s' as per config", k)
}
msg.SetHeader(k, v...)
}
return msg return msg
} }

@ -4,6 +4,7 @@
package mailer package mailer
import ( import (
"strings"
"testing" "testing"
"time" "time"
@ -36,3 +37,78 @@ func TestGenerateMessageID(t *testing.T) {
gm = m.ToMessage() gm = m.ToMessage()
assert.Equal(t, "<msg-d@domain.com>", gm.GetHeader("Message-ID")[0]) assert.Equal(t, "<msg-d@domain.com>", gm.GetHeader("Message-ID")[0])
} }
func TestToMessage(t *testing.T) {
oldConf := *setting.MailService
defer func() {
setting.MailService = &oldConf
}()
setting.MailService.From = "test@gitea.com"
m1 := Message{
Info: "info",
FromAddress: "test@gitea.com",
FromDisplayName: "Test Gitea",
To: "a@b.com",
Subject: "Issue X Closed",
Body: "Some Issue got closed by Y-Man",
}
buf := &strings.Builder{}
_, err := m1.ToMessage().WriteTo(buf)
assert.NoError(t, err)
header, _ := extractMailHeaderAndContent(t, buf.String())
assert.EqualValues(t, map[string]string{
"Content-Type": "multipart/alternative;",
"Date": "Mon, 01 Jan 0001 00:00:00 +0000",
"From": "\"Test Gitea\" <test@gitea.com>",
"Message-ID": "<autogen--6795364578871-69c000786adc60dc@localhost>",
"Mime-Version": "1.0",
"Subject": "Issue X Closed",
"To": "a@b.com",
"X-Auto-Response-Suppress": "All",
}, header)
setting.MailService.OverrideHeader = map[string][]string{
"Message-ID": {""}, // delete message id
"Auto-Submitted": {"auto-generated"}, // suppress auto replay
}
buf = &strings.Builder{}
_, err = m1.ToMessage().WriteTo(buf)
assert.NoError(t, err)
header, _ = extractMailHeaderAndContent(t, buf.String())
assert.EqualValues(t, map[string]string{
"Content-Type": "multipart/alternative;",
"Date": "Mon, 01 Jan 0001 00:00:00 +0000",
"From": "\"Test Gitea\" <test@gitea.com>",
"Message-ID": "",
"Mime-Version": "1.0",
"Subject": "Issue X Closed",
"To": "a@b.com",
"X-Auto-Response-Suppress": "All",
"Auto-Submitted": "auto-generated",
}, header)
}
func extractMailHeaderAndContent(t *testing.T, mail string) (map[string]string, string) {
header := make(map[string]string)
parts := strings.SplitN(mail, "boundary=", 2)
if !assert.Len(t, parts, 2) {
return nil, ""
}
content := strings.TrimSpace("boundary=" + parts[1])
hParts := strings.Split(parts[0], "\n")
for _, hPart := range hParts {
parts := strings.SplitN(hPart, ":", 2)
hk := strings.TrimSpace(parts[0])
if hk != "" {
header[hk] = strings.TrimSpace(parts[1])
}
}
return header, content
}