code.gitea.io/gitea@v1.21.7/services/mailer/incoming/incoming_handler.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package incoming
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  
    11  	issues_model "code.gitea.io/gitea/models/issues"
    12  	access_model "code.gitea.io/gitea/models/perm/access"
    13  	repo_model "code.gitea.io/gitea/models/repo"
    14  	user_model "code.gitea.io/gitea/models/user"
    15  	"code.gitea.io/gitea/modules/log"
    16  	"code.gitea.io/gitea/modules/setting"
    17  	"code.gitea.io/gitea/modules/upload"
    18  	"code.gitea.io/gitea/modules/util"
    19  	attachment_service "code.gitea.io/gitea/services/attachment"
    20  	issue_service "code.gitea.io/gitea/services/issue"
    21  	incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
    22  	"code.gitea.io/gitea/services/mailer/token"
    23  	pull_service "code.gitea.io/gitea/services/pull"
    24  )
    25  
    26  type MailHandler interface {
    27  	Handle(ctx context.Context, content *MailContent, doer *user_model.User, payload []byte) error
    28  }
    29  
    30  var handlers = map[token.HandlerType]MailHandler{
    31  	token.ReplyHandlerType:       &ReplyHandler{},
    32  	token.UnsubscribeHandlerType: &UnsubscribeHandler{},
    33  }
    34  
    35  // ReplyHandler handles incoming emails to create a reply from them
    36  type ReplyHandler struct{}
    37  
    38  func (h *ReplyHandler) Handle(ctx context.Context, content *MailContent, doer *user_model.User, payload []byte) error {
    39  	if doer == nil {
    40  		return util.NewInvalidArgumentErrorf("doer can't be nil")
    41  	}
    42  
    43  	ref, err := incoming_payload.GetReferenceFromPayload(ctx, payload)
    44  	if err != nil {
    45  		return err
    46  	}
    47  
    48  	var issue *issues_model.Issue
    49  
    50  	switch r := ref.(type) {
    51  	case *issues_model.Issue:
    52  		issue = r
    53  	case *issues_model.Comment:
    54  		comment := r
    55  
    56  		if err := comment.LoadIssue(ctx); err != nil {
    57  			return err
    58  		}
    59  
    60  		issue = comment.Issue
    61  	default:
    62  		return util.NewInvalidArgumentErrorf("unsupported reply reference: %v", ref)
    63  	}
    64  
    65  	if err := issue.LoadRepo(ctx); err != nil {
    66  		return err
    67  	}
    68  
    69  	perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
    70  	if err != nil {
    71  		return err
    72  	}
    73  
    74  	// Locked issues require write permissions
    75  	if issue.IsLocked && !perm.CanWriteIssuesOrPulls(issue.IsPull) && !doer.IsAdmin {
    76  		log.Debug("can't write issue or pull")
    77  		return nil
    78  	}
    79  
    80  	if !perm.CanReadIssuesOrPulls(issue.IsPull) {
    81  		log.Debug("can't read issue or pull")
    82  		return nil
    83  	}
    84  
    85  	switch r := ref.(type) {
    86  	case *issues_model.Issue:
    87  		attachmentIDs := make([]string, 0, len(content.Attachments))
    88  		if setting.Attachment.Enabled {
    89  			for _, attachment := range content.Attachments {
    90  				a, err := attachment_service.UploadAttachment(bytes.NewReader(attachment.Content), setting.Attachment.AllowedTypes, int64(len(attachment.Content)), &repo_model.Attachment{
    91  					Name:       attachment.Name,
    92  					UploaderID: doer.ID,
    93  					RepoID:     issue.Repo.ID,
    94  				})
    95  				if err != nil {
    96  					if upload.IsErrFileTypeForbidden(err) {
    97  						log.Info("Skipping disallowed attachment type: %s", attachment.Name)
    98  						continue
    99  					}
   100  					return err
   101  				}
   102  				attachmentIDs = append(attachmentIDs, a.UUID)
   103  			}
   104  		}
   105  
   106  		if content.Content == "" && len(attachmentIDs) == 0 {
   107  			return nil
   108  		}
   109  
   110  		_, err = issue_service.CreateIssueComment(ctx, doer, issue.Repo, issue, content.Content, attachmentIDs)
   111  		if err != nil {
   112  			return fmt.Errorf("CreateIssueComment failed: %w", err)
   113  		}
   114  	case *issues_model.Comment:
   115  		comment := r
   116  
   117  		if content.Content == "" {
   118  			return nil
   119  		}
   120  
   121  		if comment.Type == issues_model.CommentTypeCode {
   122  			_, err := pull_service.CreateCodeComment(
   123  				ctx,
   124  				doer,
   125  				nil,
   126  				issue,
   127  				comment.Line,
   128  				content.Content,
   129  				comment.TreePath,
   130  				false, // not pending review but a single review
   131  				comment.ReviewID,
   132  				"",
   133  			)
   134  			if err != nil {
   135  				return fmt.Errorf("CreateCodeComment failed: %w", err)
   136  			}
   137  		}
   138  	}
   139  	return nil
   140  }
   141  
   142  // UnsubscribeHandler handles unwatching issues/pulls
   143  type UnsubscribeHandler struct{}
   144  
   145  func (h *UnsubscribeHandler) Handle(ctx context.Context, _ *MailContent, doer *user_model.User, payload []byte) error {
   146  	if doer == nil {
   147  		return util.NewInvalidArgumentErrorf("doer can't be nil")
   148  	}
   149  
   150  	ref, err := incoming_payload.GetReferenceFromPayload(ctx, payload)
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	switch r := ref.(type) {
   156  	case *issues_model.Issue:
   157  		issue := r
   158  
   159  		if err := issue.LoadRepo(ctx); err != nil {
   160  			return err
   161  		}
   162  
   163  		perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
   164  		if err != nil {
   165  			return err
   166  		}
   167  
   168  		if !perm.CanReadIssuesOrPulls(issue.IsPull) {
   169  			log.Debug("can't read issue or pull")
   170  			return nil
   171  		}
   172  
   173  		return issues_model.CreateOrUpdateIssueWatch(ctx, doer.ID, issue.ID, false)
   174  	}
   175  
   176  	return fmt.Errorf("unsupported unsubscribe reference: %v", ref)
   177  }