mirror of
https://github.com/go-gitea/gitea
synced 2025-07-19 16:58:37 +00:00
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>
This commit is contained in:
171
services/mailer/incoming/incoming_handler.go
Normal file
171
services/mailer/incoming/incoming_handler.go
Normal file
@@ -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)
|
||||
}
|
Reference in New Issue
Block a user