summaryrefslogtreecommitdiffstats
path: root/services/mailer/incoming/incoming_handler.go
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 /services/mailer/incoming/incoming_handler.go
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 'services/mailer/incoming/incoming_handler.go')
-rw-r--r--services/mailer/incoming/incoming_handler.go171
1 files changed, 171 insertions, 0 deletions
diff --git a/services/mailer/incoming/incoming_handler.go b/services/mailer/incoming/incoming_handler.go
new file mode 100644
index 0000000000..173b362a55
--- /dev/null
+++ b/services/mailer/incoming/incoming_handler.go
@@ -0,0 +1,171 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package incoming
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/upload"
+ "code.gitea.io/gitea/modules/util"
+ attachment_service "code.gitea.io/gitea/services/attachment"
+ issue_service "code.gitea.io/gitea/services/issue"
+ incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
+ "code.gitea.io/gitea/services/mailer/token"
+ pull_service "code.gitea.io/gitea/services/pull"
+)
+
+type MailHandler interface {
+ Handle(ctx context.Context, content *MailContent, doer *user_model.User, payload []byte) error
+}
+
+var handlers = map[token.HandlerType]MailHandler{
+ token.ReplyHandlerType: &ReplyHandler{},
+ token.UnsubscribeHandlerType: &UnsubscribeHandler{},
+}
+
+// ReplyHandler handles incoming emails to create a reply from them
+type ReplyHandler struct{}
+
+func (h *ReplyHandler) Handle(ctx context.Context, content *MailContent, doer *user_model.User, payload []byte) error {
+ if doer == nil {
+ return util.NewInvalidArgumentErrorf("doer can't be nil")
+ }
+
+ ref, err := incoming_payload.GetReferenceFromPayload(ctx, payload)
+ if err != nil {
+ return err
+ }
+
+ var issue *issues_model.Issue
+
+ switch r := ref.(type) {
+ case *issues_model.Issue:
+ issue = r
+ case *issues_model.Comment:
+ comment := r
+
+ if err := comment.LoadIssue(ctx); err != nil {
+ return err
+ }
+
+ issue = comment.Issue
+ default:
+ return util.NewInvalidArgumentErrorf("unsupported reply reference: %v", ref)
+ }
+
+ if err := issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+
+ perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
+ if err != nil {
+ return err
+ }
+
+ if !perm.CanWriteIssuesOrPulls(issue.IsPull) || issue.IsLocked && !doer.IsAdmin {
+ log.Debug("can't write issue or pull")
+ return nil
+ }
+
+ switch r := ref.(type) {
+ case *issues_model.Issue:
+ attachmentIDs := make([]string, 0, len(content.Attachments))
+ if setting.Attachment.Enabled {
+ for _, attachment := range content.Attachments {
+ a, err := attachment_service.UploadAttachment(bytes.NewReader(attachment.Content), setting.Attachment.AllowedTypes, &repo_model.Attachment{
+ Name: attachment.Name,
+ UploaderID: doer.ID,
+ RepoID: issue.Repo.ID,
+ })
+ if err != nil {
+ if upload.IsErrFileTypeForbidden(err) {
+ log.Info("Skipping disallowed attachment type: %s", attachment.Name)
+ continue
+ }
+ return err
+ }
+ attachmentIDs = append(attachmentIDs, a.UUID)
+ }
+ }
+
+ if content.Content == "" && len(attachmentIDs) == 0 {
+ return nil
+ }
+
+ _, err = issue_service.CreateIssueComment(ctx, doer, issue.Repo, issue, content.Content, attachmentIDs)
+ if err != nil {
+ return fmt.Errorf("CreateIssueComment failed: %w", err)
+ }
+ case *issues_model.Comment:
+ comment := r
+
+ if content.Content == "" {
+ return nil
+ }
+
+ if comment.Type == issues_model.CommentTypeCode {
+ _, err := pull_service.CreateCodeComment(
+ ctx,
+ doer,
+ nil,
+ issue,
+ comment.Line,
+ content.Content,
+ comment.TreePath,
+ false,
+ comment.ReviewID,
+ "",
+ )
+ if err != nil {
+ return fmt.Errorf("CreateCodeComment failed: %w", err)
+ }
+ }
+ }
+ return nil
+}
+
+// UnsubscribeHandler handles unwatching issues/pulls
+type UnsubscribeHandler struct{}
+
+func (h *UnsubscribeHandler) Handle(ctx context.Context, _ *MailContent, doer *user_model.User, payload []byte) error {
+ if doer == nil {
+ return util.NewInvalidArgumentErrorf("doer can't be nil")
+ }
+
+ ref, err := incoming_payload.GetReferenceFromPayload(ctx, payload)
+ if err != nil {
+ return err
+ }
+
+ switch r := ref.(type) {
+ case *issues_model.Issue:
+ issue := r
+
+ if err := issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+
+ perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
+ if err != nil {
+ return err
+ }
+
+ if !perm.CanReadIssuesOrPulls(issue.IsPull) {
+ log.Debug("can't read issue or pull")
+ return nil
+ }
+
+ return issues_model.CreateOrUpdateIssueWatch(doer.ID, issue.ID, false)
+ }
+
+ return fmt.Errorf("unsupported unsubscribe reference: %v", ref)
+}