Files
forge/services/mailer/incoming/incoming_handler.go
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

189 lines
4.9 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package incoming
import (
"bytes"
"context"
"errors"
"fmt"
issues_model "github.com/hanzoai/git/models/issues"
access_model "github.com/hanzoai/git/models/perm/access"
repo_model "github.com/hanzoai/git/models/repo"
user_model "github.com/hanzoai/git/models/user"
"github.com/hanzoai/git/modules/log"
"github.com/hanzoai/git/modules/setting"
"github.com/hanzoai/git/modules/util"
attachment_service "github.com/hanzoai/git/services/attachment"
"github.com/hanzoai/git/services/context/upload"
issue_service "github.com/hanzoai/git/services/issue"
incoming_payload "github.com/hanzoai/git/services/mailer/incoming/payload"
"github.com/hanzoai/git/services/mailer/token"
pull_service "github.com/hanzoai/git/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.GetDoerRepoPermission(ctx, issue.Repo, doer)
if err != nil {
return err
}
// Locked issues require write permissions
if issue.IsLocked && !perm.CanWriteIssuesOrPulls(issue.IsPull) && !doer.IsAdmin {
log.Debug("can't write issue or pull")
return nil
}
if !perm.CanReadIssuesOrPulls(issue.IsPull) {
log.Debug("can't read issue or pull")
return nil
}
attachmentIDs := make([]string, 0, len(content.Attachments))
if setting.Attachment.Enabled {
for _, attachment := range content.Attachments {
attachmentBuf := bytes.NewReader(attachment.Content)
uploaderFile := attachment_service.NewLimitedUploaderKnownSize(attachmentBuf, attachmentBuf.Size())
a, err := attachment_service.UploadAttachmentForIssue(ctx, uploaderFile, &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
}
if errors.Is(err, util.ErrContentTooLarge) {
log.Info("Skipping attachment exceeding size limit: %s", attachment.Name)
continue
}
return err
}
attachmentIDs = append(attachmentIDs, a.UUID)
}
}
if content.Content == "" && len(attachmentIDs) == 0 {
return nil
}
switch r := ref.(type) {
case *issues_model.Issue:
_, 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
switch comment.Type {
case issues_model.CommentTypeCode:
_, err := pull_service.CreateCodeComment(
ctx,
doer,
nil,
issue,
comment.Line,
content.Content,
comment.TreePath,
false, // not pending review but a single review
comment.ReviewID,
"",
attachmentIDs,
)
if err != nil {
return fmt.Errorf("CreateCodeComment failed: %w", err)
}
default:
_, err := issue_service.CreateIssueComment(ctx, doer, issue.Repo, issue, content.Content, attachmentIDs)
if err != nil {
return fmt.Errorf("CreateIssueComment 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.GetDoerRepoPermission(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(ctx, doer.ID, issue.ID, false)
}
return fmt.Errorf("unsupported unsubscribe reference: %v", ref)
}