aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorKN4CK3R2023-01-14 16:57:10 +0100
committerGitHub2023-01-14 23:57:10 +0800
commitfc037b4b825f0501a1489e10d7c822435d825cb7 (patch)
tree551590b5ec197d8efca8b7bc3a9acc5961637d9d /tests
parent20e3ffd2085d7066b3206809dfae7b6ebd59cb5d (diff)
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 'tests')
-rw-r--r--tests/integration/incoming_email_test.go249
-rw-r--r--tests/mysql.ini.tmpl10
2 files changed, 259 insertions, 0 deletions
diff --git a/tests/integration/incoming_email_test.go b/tests/integration/incoming_email_test.go
new file mode 100644
index 000000000..b4478f578
--- /dev/null
+++ b/tests/integration/incoming_email_test.go
@@ -0,0 +1,249 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+ "io"
+ "net"
+ "net/smtp"
+ "strings"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/mailer/incoming"
+ incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
+ token_service "code.gitea.io/gitea/services/mailer/token"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+ "gopkg.in/gomail.v2"
+)
+
+func TestIncomingEmail(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
+
+ t.Run("Payload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
+
+ _, err := incoming_payload.CreateReferencePayload(user)
+ assert.Error(t, err)
+
+ issuePayload, err := incoming_payload.CreateReferencePayload(issue)
+ assert.NoError(t, err)
+ commentPayload, err := incoming_payload.CreateReferencePayload(comment)
+ assert.NoError(t, err)
+
+ _, err = incoming_payload.GetReferenceFromPayload(db.DefaultContext, []byte{1, 2, 3})
+ assert.Error(t, err)
+
+ ref, err := incoming_payload.GetReferenceFromPayload(db.DefaultContext, issuePayload)
+ assert.NoError(t, err)
+ assert.IsType(t, ref, new(issues_model.Issue))
+ assert.EqualValues(t, issue.ID, ref.(*issues_model.Issue).ID)
+
+ ref, err = incoming_payload.GetReferenceFromPayload(db.DefaultContext, commentPayload)
+ assert.NoError(t, err)
+ assert.IsType(t, ref, new(issues_model.Comment))
+ assert.EqualValues(t, comment.ID, ref.(*issues_model.Comment).ID)
+ })
+
+ t.Run("Token", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ payload := []byte{1, 2, 3, 4, 5}
+
+ token, err := token_service.CreateToken(token_service.ReplyHandlerType, user, payload)
+ assert.NoError(t, err)
+ assert.NotEmpty(t, token)
+
+ ht, u, p, err := token_service.ExtractToken(db.DefaultContext, token)
+ assert.NoError(t, err)
+ assert.Equal(t, token_service.ReplyHandlerType, ht)
+ assert.Equal(t, user.ID, u.ID)
+ assert.Equal(t, payload, p)
+ })
+
+ t.Run("Handler", func(t *testing.T) {
+ t.Run("Reply", func(t *testing.T) {
+ t.Run("Comment", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ handler := &incoming.ReplyHandler{}
+
+ payload, err := incoming_payload.CreateReferencePayload(issue)
+ assert.NoError(t, err)
+
+ assert.Error(t, handler.Handle(db.DefaultContext, &incoming.MailContent{}, nil, payload))
+ assert.NoError(t, handler.Handle(db.DefaultContext, &incoming.MailContent{}, user, payload))
+
+ content := &incoming.MailContent{
+ Content: "reply by mail",
+ Attachments: []*incoming.Attachment{
+ {
+ Name: "attachment.txt",
+ Content: []byte("test"),
+ },
+ },
+ }
+
+ assert.NoError(t, handler.Handle(db.DefaultContext, content, user, payload))
+
+ comments, err := issues_model.FindComments(db.DefaultContext, &issues_model.FindCommentsOptions{
+ IssueID: issue.ID,
+ Type: issues_model.CommentTypeComment,
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, comments)
+ comment := comments[len(comments)-1]
+ assert.Equal(t, user.ID, comment.PosterID)
+ assert.Equal(t, content.Content, comment.Content)
+ assert.NoError(t, comment.LoadAttachments(db.DefaultContext))
+ assert.Len(t, comment.Attachments, 1)
+ attachment := comment.Attachments[0]
+ assert.Equal(t, content.Attachments[0].Name, attachment.Name)
+ assert.EqualValues(t, 4, attachment.Size)
+ })
+
+ t.Run("CodeComment", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 6})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
+
+ handler := &incoming.ReplyHandler{}
+ content := &incoming.MailContent{
+ Content: "code reply by mail",
+ Attachments: []*incoming.Attachment{
+ {
+ Name: "attachment.txt",
+ Content: []byte("test"),
+ },
+ },
+ }
+
+ payload, err := incoming_payload.CreateReferencePayload(comment)
+ assert.NoError(t, err)
+
+ assert.NoError(t, handler.Handle(db.DefaultContext, content, user, payload))
+
+ comments, err := issues_model.FindComments(db.DefaultContext, &issues_model.FindCommentsOptions{
+ IssueID: issue.ID,
+ Type: issues_model.CommentTypeCode,
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, comments)
+ comment = comments[len(comments)-1]
+ assert.Equal(t, user.ID, comment.PosterID)
+ assert.Equal(t, content.Content, comment.Content)
+ assert.NoError(t, comment.LoadAttachments(db.DefaultContext))
+ assert.Empty(t, comment.Attachments)
+ })
+ })
+
+ t.Run("Unsubscribe", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ watching, err := issues_model.CheckIssueWatch(user, issue)
+ assert.NoError(t, err)
+ assert.True(t, watching)
+
+ handler := &incoming.UnsubscribeHandler{}
+
+ content := &incoming.MailContent{
+ Content: "unsub me",
+ }
+
+ payload, err := incoming_payload.CreateReferencePayload(issue)
+ assert.NoError(t, err)
+
+ assert.NoError(t, handler.Handle(db.DefaultContext, content, user, payload))
+
+ watching, err = issues_model.CheckIssueWatch(user, issue)
+ assert.NoError(t, err)
+ assert.False(t, watching)
+ })
+ })
+
+ if setting.IncomingEmail.Enabled {
+ // This test connects to the configured email server and is currently only enabled for MySql integration tests.
+ // It sends a reply to create a comment. If the comment is not detected after 10 seconds the test fails.
+ t.Run("IMAP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ payload, err := incoming_payload.CreateReferencePayload(issue)
+ assert.NoError(t, err)
+ token, err := token_service.CreateToken(token_service.ReplyHandlerType, user, payload)
+ assert.NoError(t, err)
+
+ msg := gomail.NewMessage()
+ msg.SetHeader("To", strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1))
+ msg.SetHeader("From", user.Email)
+ msg.SetBody("text/plain", token)
+ err = gomail.Send(&smtpTestSender{}, msg)
+ assert.NoError(t, err)
+
+ assert.Eventually(t, func() bool {
+ comments, err := issues_model.FindComments(db.DefaultContext, &issues_model.FindCommentsOptions{
+ IssueID: issue.ID,
+ Type: issues_model.CommentTypeComment,
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, comments)
+
+ comment := comments[len(comments)-1]
+
+ return comment.PosterID == user.ID && comment.Content == token
+ }, 10*time.Second, 1*time.Second)
+ })
+ }
+}
+
+// A simple SMTP mail sender used for integration tests.
+type smtpTestSender struct{}
+
+func (s *smtpTestSender) Send(from string, to []string, msg io.WriterTo) error {
+ conn, err := net.Dial("tcp", net.JoinHostPort(setting.IncomingEmail.Host, "25"))
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+
+ client, err := smtp.NewClient(conn, setting.IncomingEmail.Host)
+ if err != nil {
+ return err
+ }
+
+ if err = client.Mail(from); err != nil {
+ return err
+ }
+
+ for _, rec := range to {
+ if err = client.Rcpt(rec); err != nil {
+ return err
+ }
+ }
+
+ w, err := client.Data()
+ if err != nil {
+ return err
+ }
+ if _, err := msg.WriteTo(w); err != nil {
+ return err
+ }
+ if err := w.Close(); err != nil {
+ return err
+ }
+
+ return client.Quit()
+}
diff --git a/tests/mysql.ini.tmpl b/tests/mysql.ini.tmpl
index 24a9a02dc..44914d087 100644
--- a/tests/mysql.ini.tmpl
+++ b/tests/mysql.ini.tmpl
@@ -124,3 +124,13 @@ INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.h
[packages]
ENABLED = true
+
+[email.incoming]
+ENABLED = true
+HOST = smtpimap
+PORT = 993
+USERNAME = debug@localdomain.test
+PASSWORD = debug
+USE_TLS = true
+SKIP_TLS_VERIFY = true
+REPLY_TO_ADDRESS = incoming+%{token}@localhost