code.gitea.io/gitea@v1.22.3/tests/integration/incoming_email_test.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package integration
     5  
     6  import (
     7  	"io"
     8  	"net"
     9  	"net/smtp"
    10  	"net/url"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"code.gitea.io/gitea/models/db"
    16  	issues_model "code.gitea.io/gitea/models/issues"
    17  	"code.gitea.io/gitea/models/unittest"
    18  	user_model "code.gitea.io/gitea/models/user"
    19  	"code.gitea.io/gitea/modules/setting"
    20  	"code.gitea.io/gitea/services/mailer/incoming"
    21  	incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
    22  	token_service "code.gitea.io/gitea/services/mailer/token"
    23  	"code.gitea.io/gitea/tests"
    24  
    25  	"github.com/stretchr/testify/assert"
    26  	"gopkg.in/gomail.v2"
    27  )
    28  
    29  func TestIncomingEmail(t *testing.T) {
    30  	onGiteaRun(t, func(t *testing.T, u *url.URL) {
    31  		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
    32  		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
    33  
    34  		t.Run("Payload", func(t *testing.T) {
    35  			defer tests.PrintCurrentTest(t)()
    36  
    37  			comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
    38  
    39  			_, err := incoming_payload.CreateReferencePayload(user)
    40  			assert.Error(t, err)
    41  
    42  			issuePayload, err := incoming_payload.CreateReferencePayload(issue)
    43  			assert.NoError(t, err)
    44  			commentPayload, err := incoming_payload.CreateReferencePayload(comment)
    45  			assert.NoError(t, err)
    46  
    47  			_, err = incoming_payload.GetReferenceFromPayload(db.DefaultContext, []byte{1, 2, 3})
    48  			assert.Error(t, err)
    49  
    50  			ref, err := incoming_payload.GetReferenceFromPayload(db.DefaultContext, issuePayload)
    51  			assert.NoError(t, err)
    52  			assert.IsType(t, ref, new(issues_model.Issue))
    53  			assert.EqualValues(t, issue.ID, ref.(*issues_model.Issue).ID)
    54  
    55  			ref, err = incoming_payload.GetReferenceFromPayload(db.DefaultContext, commentPayload)
    56  			assert.NoError(t, err)
    57  			assert.IsType(t, ref, new(issues_model.Comment))
    58  			assert.EqualValues(t, comment.ID, ref.(*issues_model.Comment).ID)
    59  		})
    60  
    61  		t.Run("Token", func(t *testing.T) {
    62  			defer tests.PrintCurrentTest(t)()
    63  
    64  			payload := []byte{1, 2, 3, 4, 5}
    65  
    66  			token, err := token_service.CreateToken(token_service.ReplyHandlerType, user, payload)
    67  			assert.NoError(t, err)
    68  			assert.NotEmpty(t, token)
    69  
    70  			ht, u, p, err := token_service.ExtractToken(db.DefaultContext, token)
    71  			assert.NoError(t, err)
    72  			assert.Equal(t, token_service.ReplyHandlerType, ht)
    73  			assert.Equal(t, user.ID, u.ID)
    74  			assert.Equal(t, payload, p)
    75  		})
    76  
    77  		t.Run("Handler", func(t *testing.T) {
    78  			t.Run("Reply", func(t *testing.T) {
    79  				t.Run("Comment", func(t *testing.T) {
    80  					defer tests.PrintCurrentTest(t)()
    81  
    82  					handler := &incoming.ReplyHandler{}
    83  
    84  					payload, err := incoming_payload.CreateReferencePayload(issue)
    85  					assert.NoError(t, err)
    86  
    87  					assert.Error(t, handler.Handle(db.DefaultContext, &incoming.MailContent{}, nil, payload))
    88  					assert.NoError(t, handler.Handle(db.DefaultContext, &incoming.MailContent{}, user, payload))
    89  
    90  					content := &incoming.MailContent{
    91  						Content: "reply by mail",
    92  						Attachments: []*incoming.Attachment{
    93  							{
    94  								Name:    "attachment.txt",
    95  								Content: []byte("test"),
    96  							},
    97  						},
    98  					}
    99  
   100  					assert.NoError(t, handler.Handle(db.DefaultContext, content, user, payload))
   101  
   102  					comments, err := issues_model.FindComments(db.DefaultContext, &issues_model.FindCommentsOptions{
   103  						IssueID: issue.ID,
   104  						Type:    issues_model.CommentTypeComment,
   105  					})
   106  					assert.NoError(t, err)
   107  					assert.NotEmpty(t, comments)
   108  					comment := comments[len(comments)-1]
   109  					assert.Equal(t, user.ID, comment.PosterID)
   110  					assert.Equal(t, content.Content, comment.Content)
   111  					assert.NoError(t, comment.LoadAttachments(db.DefaultContext))
   112  					assert.Len(t, comment.Attachments, 1)
   113  					attachment := comment.Attachments[0]
   114  					assert.Equal(t, content.Attachments[0].Name, attachment.Name)
   115  					assert.EqualValues(t, 4, attachment.Size)
   116  				})
   117  
   118  				t.Run("CodeComment", func(t *testing.T) {
   119  					defer tests.PrintCurrentTest(t)()
   120  
   121  					comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 6})
   122  					issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
   123  
   124  					handler := &incoming.ReplyHandler{}
   125  					content := &incoming.MailContent{
   126  						Content: "code reply by mail",
   127  						Attachments: []*incoming.Attachment{
   128  							{
   129  								Name:    "attachment.txt",
   130  								Content: []byte("test"),
   131  							},
   132  						},
   133  					}
   134  
   135  					payload, err := incoming_payload.CreateReferencePayload(comment)
   136  					assert.NoError(t, err)
   137  
   138  					assert.NoError(t, handler.Handle(db.DefaultContext, content, user, payload))
   139  
   140  					comments, err := issues_model.FindComments(db.DefaultContext, &issues_model.FindCommentsOptions{
   141  						IssueID: issue.ID,
   142  						Type:    issues_model.CommentTypeCode,
   143  					})
   144  					assert.NoError(t, err)
   145  					assert.NotEmpty(t, comments)
   146  					comment = comments[len(comments)-1]
   147  					assert.Equal(t, user.ID, comment.PosterID)
   148  					assert.Equal(t, content.Content, comment.Content)
   149  					assert.NoError(t, comment.LoadAttachments(db.DefaultContext))
   150  					assert.Len(t, comment.Attachments, 1)
   151  					attachment := comment.Attachments[0]
   152  					assert.Equal(t, content.Attachments[0].Name, attachment.Name)
   153  					assert.EqualValues(t, 4, attachment.Size)
   154  				})
   155  			})
   156  
   157  			t.Run("Unsubscribe", func(t *testing.T) {
   158  				defer tests.PrintCurrentTest(t)()
   159  
   160  				watching, err := issues_model.CheckIssueWatch(db.DefaultContext, user, issue)
   161  				assert.NoError(t, err)
   162  				assert.True(t, watching)
   163  
   164  				handler := &incoming.UnsubscribeHandler{}
   165  
   166  				content := &incoming.MailContent{
   167  					Content: "unsub me",
   168  				}
   169  
   170  				payload, err := incoming_payload.CreateReferencePayload(issue)
   171  				assert.NoError(t, err)
   172  
   173  				assert.NoError(t, handler.Handle(db.DefaultContext, content, user, payload))
   174  
   175  				watching, err = issues_model.CheckIssueWatch(db.DefaultContext, user, issue)
   176  				assert.NoError(t, err)
   177  				assert.False(t, watching)
   178  			})
   179  		})
   180  
   181  		if setting.IncomingEmail.Enabled {
   182  			// This test connects to the configured email server and is currently only enabled for MySql integration tests.
   183  			// It sends a reply to create a comment. If the comment is not detected after 10 seconds the test fails.
   184  			t.Run("IMAP", func(t *testing.T) {
   185  				defer tests.PrintCurrentTest(t)()
   186  
   187  				payload, err := incoming_payload.CreateReferencePayload(issue)
   188  				assert.NoError(t, err)
   189  				token, err := token_service.CreateToken(token_service.ReplyHandlerType, user, payload)
   190  				assert.NoError(t, err)
   191  
   192  				msg := gomail.NewMessage()
   193  				msg.SetHeader("To", strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1))
   194  				msg.SetHeader("From", user.Email)
   195  				msg.SetBody("text/plain", token)
   196  				err = gomail.Send(&smtpTestSender{}, msg)
   197  				assert.NoError(t, err)
   198  
   199  				assert.Eventually(t, func() bool {
   200  					comments, err := issues_model.FindComments(db.DefaultContext, &issues_model.FindCommentsOptions{
   201  						IssueID: issue.ID,
   202  						Type:    issues_model.CommentTypeComment,
   203  					})
   204  					assert.NoError(t, err)
   205  					assert.NotEmpty(t, comments)
   206  
   207  					comment := comments[len(comments)-1]
   208  
   209  					return comment.PosterID == user.ID && comment.Content == token
   210  				}, 10*time.Second, 1*time.Second)
   211  			})
   212  		}
   213  	})
   214  }
   215  
   216  // A simple SMTP mail sender used for integration tests.
   217  type smtpTestSender struct{}
   218  
   219  func (s *smtpTestSender) Send(from string, to []string, msg io.WriterTo) error {
   220  	conn, err := net.Dial("tcp", net.JoinHostPort(setting.IncomingEmail.Host, "25"))
   221  	if err != nil {
   222  		return err
   223  	}
   224  	defer conn.Close()
   225  
   226  	client, err := smtp.NewClient(conn, setting.IncomingEmail.Host)
   227  	if err != nil {
   228  		return err
   229  	}
   230  
   231  	if err = client.Mail(from); err != nil {
   232  		return err
   233  	}
   234  
   235  	for _, rec := range to {
   236  		if err = client.Rcpt(rec); err != nil {
   237  			return err
   238  		}
   239  	}
   240  
   241  	w, err := client.Data()
   242  	if err != nil {
   243  		return err
   244  	}
   245  	if _, err := msg.WriteTo(w); err != nil {
   246  		return err
   247  	}
   248  	if err := w.Close(); err != nil {
   249  		return err
   250  	}
   251  
   252  	return client.Quit()
   253  }