aboutsummaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
authorKN4CK3R <admin@oldschoolhack.me>2023-01-14 16:57:10 +0100
committerGitHub <noreply@github.com>2023-01-14 23:57:10 +0800
commitfc037b4b825f0501a1489e10d7c822435d825cb7 (patch)
tree551590b5ec197d8efca8b7bc3a9acc5961637d9d /modules
parent20e3ffd2085d7066b3206809dfae7b6ebd59cb5d (diff)
downloadgitea-fc037b4b825f0501a1489e10d7c822435d825cb7.tar.gz
gitea-fc037b4b825f0501a1489e10d7c822435d825cb7.zip
Add support for incoming emails (#22056)
closes #13585 fixes #9067 fixes #2386 ref #6226 ref #6219 fixes #745 This PR adds support to process incoming emails to perform actions. Currently I added handling of replies and unsubscribing from issues/pulls. In contrast to #13585 the IMAP IDLE command is used instead of polling which results (in my opinion 😉) in cleaner code. Procedure: - When sending an issue/pull reply email, a token is generated which is present in the Reply-To and References header. - IMAP IDLE waits until a new email arrives - The token tells which action should be performed A possible signature and/or reply gets stripped from the content. I added a new service to the drone pipeline to test the receiving of incoming mails. If we keep this in, we may test our outgoing emails too in future. Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Diffstat (limited to 'modules')
-rw-r--r--modules/setting/incoming_email.go73
-rw-r--r--modules/setting/setting.go1
-rw-r--r--modules/util/pack.go33
-rw-r--r--modules/util/pack_test.go28
4 files changed, 135 insertions, 0 deletions
diff --git a/modules/setting/incoming_email.go b/modules/setting/incoming_email.go
new file mode 100644
index 0000000000..b6a637bccd
--- /dev/null
+++ b/modules/setting/incoming_email.go
@@ -0,0 +1,73 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+ "fmt"
+ "net/mail"
+ "strings"
+
+ "code.gitea.io/gitea/modules/log"
+)
+
+var IncomingEmail = struct {
+ Enabled bool
+ ReplyToAddress string
+ TokenPlaceholder string `ini:"-"`
+ Host string
+ Port int
+ UseTLS bool `ini:"USE_TLS"`
+ SkipTLSVerify bool `ini:"SKIP_TLS_VERIFY"`
+ Username string
+ Password string
+ Mailbox string
+ DeleteHandledMessage bool
+ MaximumMessageSize uint32
+}{
+ Mailbox: "INBOX",
+ DeleteHandledMessage: true,
+ TokenPlaceholder: "%{token}",
+ MaximumMessageSize: 10485760,
+}
+
+func newIncomingEmail() {
+ if err := Cfg.Section("email.incoming").MapTo(&IncomingEmail); err != nil {
+ log.Fatal("Unable to map [email.incoming] section on to IncomingEmail. Error: %v", err)
+ }
+
+ if !IncomingEmail.Enabled {
+ return
+ }
+
+ if err := checkReplyToAddress(IncomingEmail.ReplyToAddress); err != nil {
+ log.Fatal("Invalid incoming_mail.REPLY_TO_ADDRESS (%s): %v", IncomingEmail.ReplyToAddress, err)
+ }
+}
+
+func checkReplyToAddress(address string) error {
+ parsed, err := mail.ParseAddress(IncomingEmail.ReplyToAddress)
+ if err != nil {
+ return err
+ }
+
+ if parsed.Name != "" {
+ return fmt.Errorf("name must not be set")
+ }
+
+ c := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmail.TokenPlaceholder)
+ switch c {
+ case 0:
+ return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
+ case 1:
+ default:
+ return fmt.Errorf("%s must appear only once", IncomingEmail.TokenPlaceholder)
+ }
+
+ parts := strings.Split(IncomingEmail.ReplyToAddress, "@")
+ if !strings.Contains(parts[0], IncomingEmail.TokenPlaceholder) {
+ return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
+ }
+
+ return nil
+}
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index fa65b94891..92861fb528 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -1341,6 +1341,7 @@ func NewServices() {
newSessionService()
newCORSService()
parseMailerConfig(Cfg)
+ newIncomingEmail()
newRegisterMailService()
newNotifyMailService()
newProxyService()
diff --git a/modules/util/pack.go b/modules/util/pack.go
new file mode 100644
index 0000000000..315d9f5066
--- /dev/null
+++ b/modules/util/pack.go
@@ -0,0 +1,33 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package util
+
+import (
+ "bytes"
+ "encoding/gob"
+)
+
+// PackData uses gob to encode the given data in sequence
+func PackData(data ...interface{}) ([]byte, error) {
+ var buf bytes.Buffer
+ enc := gob.NewEncoder(&buf)
+ for _, datum := range data {
+ if err := enc.Encode(datum); err != nil {
+ return nil, err
+ }
+ }
+ return buf.Bytes(), nil
+}
+
+// UnpackData uses gob to decode the given data in sequence
+func UnpackData(buf []byte, data ...interface{}) error {
+ r := bytes.NewReader(buf)
+ enc := gob.NewDecoder(r)
+ for _, datum := range data {
+ if err := enc.Decode(datum); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/modules/util/pack_test.go b/modules/util/pack_test.go
new file mode 100644
index 0000000000..592c69cd0a
--- /dev/null
+++ b/modules/util/pack_test.go
@@ -0,0 +1,28 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package util
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackAndUnpackData(t *testing.T) {
+ s := "string"
+ i := int64(4)
+ f := float32(4.1)
+
+ var s2 string
+ var i2 int64
+ var f2 float32
+
+ data, err := PackData(s, i, f)
+ assert.NoError(t, err)
+
+ assert.NoError(t, UnpackData(data, &s2, &i2, &f2))
+ assert.NoError(t, UnpackData(data, &s2))
+ assert.Error(t, UnpackData(data, &i2))
+ assert.Error(t, UnpackData(data, &s2, &f2))
+}