diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 8b67a45175..83904080de 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -548,17 +548,17 @@ func SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath string)
 
 // LoadFromExisting initializes setting options from an existing config file (app.ini)
 func LoadFromExisting() {
-	loadFromConf(false)
+	loadFromConf(false, "")
 }
 
 // LoadAllowEmpty initializes setting options, it's also fine that if the config file (app.ini) doesn't exist
 func LoadAllowEmpty() {
-	loadFromConf(true)
+	loadFromConf(true, "")
 }
 
 // LoadForTest initializes setting options for tests
-func LoadForTest() {
-	loadFromConf(true)
+func LoadForTest(extraConfigs ...string) {
+	loadFromConf(true, strings.Join(extraConfigs, "\n"))
 	if err := PrepareAppDataPath(); err != nil {
 		log.Fatal("Can not prepare APP_DATA_PATH: %v", err)
 	}
@@ -566,7 +566,7 @@ func LoadForTest() {
 
 // loadFromConf initializes configuration context.
 // NOTE: do not print any log except error.
-func loadFromConf(allowEmpty bool) {
+func loadFromConf(allowEmpty bool, extraConfig string) {
 	Cfg = ini.Empty()
 
 	if WritePIDFile && len(PIDFile) > 0 {
@@ -585,6 +585,12 @@ func loadFromConf(allowEmpty bool) {
 		log.Fatal("Unable to find configuration file: %q.\nEnsure you are running in the correct environment or set the correct configuration file with -c.", CustomConf)
 	} // else: no config file, a config file might be created at CustomConf later (might not)
 
+	if extraConfig != "" {
+		if err = Cfg.Append([]byte(extraConfig)); err != nil {
+			log.Fatal("Unable to append more config: %v", err)
+		}
+	}
+
 	Cfg.NameMapper = ini.SnackCase
 
 	homeDir, err := com.HomeDir()
diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go
index 4b1492df0b..eac2b15c3c 100644
--- a/services/mailer/mailer.go
+++ b/services/mailer/mailer.go
@@ -9,6 +9,7 @@ import (
 	"bytes"
 	"crypto/tls"
 	"fmt"
+	"hash/fnv"
 	"io"
 	"net"
 	"net/smtp"
@@ -67,6 +68,10 @@ func (m *Message) ToMessage() *gomail.Message {
 		msg.SetBody("text/plain", plainBody)
 		msg.AddAlternative("text/html", m.Body)
 	}
+
+	if len(msg.GetHeader("Message-ID")) == 0 {
+		msg.SetHeader("Message-ID", m.generateAutoMessageID())
+	}
 	return msg
 }
 
@@ -75,6 +80,17 @@ func (m *Message) SetHeader(field string, value ...string) {
 	m.Headers[field] = value
 }
 
+func (m *Message) generateAutoMessageID() string {
+	dateMs := m.Date.UnixNano() / 1e6
+	h := fnv.New64()
+	if len(m.To) > 0 {
+		_, _ = h.Write([]byte(m.To[0]))
+	}
+	_, _ = h.Write([]byte(m.Subject))
+	_, _ = h.Write([]byte(m.Body))
+	return fmt.Sprintf("<autogen-%d-%016x@%s>", dateMs, h.Sum64(), setting.Domain)
+}
+
 // NewMessageFrom creates new mail message object with custom From header.
 func NewMessageFrom(to []string, fromDisplayName, fromAddress, subject, body string) *Message {
 	log.Trace("NewMessageFrom (body):\n%s", body)
diff --git a/services/mailer/mailer_test.go b/services/mailer/mailer_test.go
new file mode 100644
index 0000000000..8505803d22
--- /dev/null
+++ b/services/mailer/mailer_test.go
@@ -0,0 +1,39 @@
+// Copyright 2021 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package mailer
+
+import (
+	"testing"
+	"time"
+
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGenerateMessageID(t *testing.T) {
+	setting.LoadForTest(`
+[mailer]
+ENABLED = true
+FROM = test@domain.com
+`)
+	setting.NewServices()
+
+	date := time.Date(2000, 01, 02, 03, 04, 05, 06, time.UTC)
+	m := NewMessageFrom(nil, "display-name", "from-address", "subject", "body")
+	m.Date = date
+	gm := m.ToMessage()
+	assert.Equal(t, "<autogen-946782245000-41e8fc54a8ad3a3f@localhost>", gm.GetHeader("Message-ID")[0])
+
+	m = NewMessageFrom([]string{"a@b.com"}, "display-name", "from-address", "subject", "body")
+	m.Date = date
+	gm = m.ToMessage()
+	assert.Equal(t, "<autogen-946782245000-cc88ce3cfe9bd04f@localhost>", gm.GetHeader("Message-ID")[0])
+
+	m = NewMessageFrom([]string{"a@b.com"}, "display-name", "from-address", "subject", "body")
+	m.SetHeader("Message-ID", "<msg-d@domain.com>")
+	gm = m.ToMessage()
+	assert.Equal(t, "<msg-d@domain.com>", gm.GetHeader("Message-ID")[0])
+}