From 1d4bff4f65d5e4a3969871ef91d3612daf272b45 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Mon, 3 Jun 2024 20:42:52 +0200
Subject: [PATCH] Add option for mailer to override mail headers (#27860)

Add option to override headers of mails, gitea send out

---
*Sponsored by Kithara Software GmbH*

(cherry picked from commit aace3bccc3290446637cac30b121b94b5d03075f)

Conflicts:
	docs/content/administration/config-cheat-sheet.en-us.md
	does not exist in Forgejo
	services/mailer/mailer_test.go
	trivial context conflict
---
 custom/conf/app.example.ini    | 10 +++++
 modules/setting/mailer.go      | 23 ++++++----
 services/mailer/mailer.go      | 10 ++++-
 services/mailer/mailer_test.go | 76 ++++++++++++++++++++++++++++++++++
 4 files changed, 110 insertions(+), 9 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 451742ac06..013a0e5bec 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1746,6 +1746,16 @@ LEVEL = Info
 ;; convert \r\n to \n for Sendmail
 ;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]
diff --git a/modules/setting/mailer.go b/modules/setting/mailer.go
index e9ce640c7f..cfedb4613f 100644
--- a/modules/setting/mailer.go
+++ b/modules/setting/mailer.go
@@ -18,14 +18,15 @@ import (
 // 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"`
+	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"`
@@ -159,6 +160,12 @@ func loadMailerFrom(rootCfg ConfigProvider) {
 		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 {
diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go
index a9316cca76..0a723f974a 100644
--- a/services/mailer/mailer.go
+++ b/services/mailer/mailer.go
@@ -57,7 +57,7 @@ func (m *Message) ToMessage() *gomail.Message {
 		msg.SetHeader(header, m.Headers[header]...)
 	}
 
-	if len(setting.MailService.SubjectPrefix) > 0 {
+	if setting.MailService.SubjectPrefix != "" {
 		msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject)
 	} else {
 		msg.SetHeader("Subject", m.Subject)
@@ -79,6 +79,14 @@ func (m *Message) ToMessage() *gomail.Message {
 	if len(msg.GetHeader("Message-ID")) == 0 {
 		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
 }
 
diff --git a/services/mailer/mailer_test.go b/services/mailer/mailer_test.go
index 253454e89c..2f7da08697 100644
--- a/services/mailer/mailer_test.go
+++ b/services/mailer/mailer_test.go
@@ -4,6 +4,7 @@
 package mailer
 
 import (
+	"strings"
 	"testing"
 	"time"
 
@@ -51,3 +52,78 @@ func TestGenerateMessageIDForRelease(t *testing.T) {
 	m := createMessageIDForRelease(&rel)
 	assert.Equal(t, "<test/tag-test/releases/42@localhost>", m)
 }
+
+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
+}