gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. // Copyright 2016 The Gogs Authors. All rights reserved.
  2. // Copyright 2019 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package mailer
  5. import (
  6. "bytes"
  7. "context"
  8. "encoding/base64"
  9. "errors"
  10. "fmt"
  11. "html/template"
  12. "io"
  13. "mime"
  14. "regexp"
  15. "strings"
  16. "sync/atomic"
  17. repo_model "code.gitea.io/gitea/models/repo"
  18. user_model "code.gitea.io/gitea/models/user"
  19. "code.gitea.io/gitea/modules/httplib"
  20. "code.gitea.io/gitea/modules/log"
  21. "code.gitea.io/gitea/modules/setting"
  22. "code.gitea.io/gitea/modules/storage"
  23. "code.gitea.io/gitea/modules/templates"
  24. "code.gitea.io/gitea/modules/typesniffer"
  25. sender_service "code.gitea.io/gitea/services/mailer/sender"
  26. "golang.org/x/net/html"
  27. )
  28. const mailMaxSubjectRunes = 256 // There's no actual limit for subject in RFC 5322
  29. var loadedTemplates atomic.Pointer[templates.MailTemplates]
  30. var subjectRemoveSpaces = regexp.MustCompile(`[\s]+`)
  31. func LoadedTemplates() *templates.MailTemplates {
  32. return loadedTemplates.Load()
  33. }
  34. // SendTestMail sends a test mail
  35. func SendTestMail(email string) error {
  36. if setting.MailService == nil {
  37. // No mail service configured
  38. return nil
  39. }
  40. return sender_service.Send(sender, sender_service.NewMessage(email, "Gitea Test Email!", "Gitea Test Email!"))
  41. }
  42. func sanitizeSubject(subject string) string {
  43. runes := []rune(strings.TrimSpace(subjectRemoveSpaces.ReplaceAllLiteralString(subject, " ")))
  44. if len(runes) > mailMaxSubjectRunes {
  45. runes = runes[:mailMaxSubjectRunes]
  46. }
  47. // Encode non-ASCII characters
  48. return mime.QEncoding.Encode("utf-8", string(runes))
  49. }
  50. type mailAttachmentBase64Embedder struct {
  51. doer *user_model.User
  52. repo *repo_model.Repository
  53. maxSize int64
  54. estimateSize int64
  55. }
  56. func newMailAttachmentBase64Embedder(doer *user_model.User, repo *repo_model.Repository, maxSize int64) *mailAttachmentBase64Embedder {
  57. return &mailAttachmentBase64Embedder{doer: doer, repo: repo, maxSize: maxSize}
  58. }
  59. func (b64embedder *mailAttachmentBase64Embedder) Base64InlineImages(ctx context.Context, body template.HTML) (template.HTML, error) {
  60. doc, err := html.Parse(strings.NewReader(string(body)))
  61. if err != nil {
  62. return "", fmt.Errorf("html.Parse failed: %w", err)
  63. }
  64. b64embedder.estimateSize = int64(len(string(body)))
  65. var processNode func(*html.Node)
  66. processNode = func(n *html.Node) {
  67. if n.Type == html.ElementNode {
  68. if n.Data == "img" {
  69. for i, attr := range n.Attr {
  70. if attr.Key == "src" {
  71. attachmentSrc := attr.Val
  72. dataURI, err := b64embedder.AttachmentSrcToBase64DataURI(ctx, attachmentSrc)
  73. if err != nil {
  74. // Not an error, just skip. This is probably an image from outside the gitea instance.
  75. log.Trace("Unable to embed attachment %q to mail body: %v", attachmentSrc, err)
  76. } else {
  77. n.Attr[i].Val = dataURI
  78. }
  79. break
  80. }
  81. }
  82. }
  83. }
  84. for c := n.FirstChild; c != nil; c = c.NextSibling {
  85. processNode(c)
  86. }
  87. }
  88. processNode(doc)
  89. var buf bytes.Buffer
  90. err = html.Render(&buf, doc)
  91. if err != nil {
  92. return "", fmt.Errorf("html.Render failed: %w", err)
  93. }
  94. return template.HTML(buf.String()), nil
  95. }
  96. func (b64embedder *mailAttachmentBase64Embedder) AttachmentSrcToBase64DataURI(ctx context.Context, attachmentSrc string) (string, error) {
  97. parsedSrc := httplib.ParseGiteaSiteURL(ctx, attachmentSrc)
  98. var attachmentUUID string
  99. if parsedSrc != nil {
  100. var ok bool
  101. attachmentUUID, ok = strings.CutPrefix(parsedSrc.RoutePath, "/attachments/")
  102. if !ok {
  103. attachmentUUID, ok = strings.CutPrefix(parsedSrc.RepoSubPath, "/attachments/")
  104. }
  105. if !ok {
  106. return "", errors.New("not an attachment")
  107. }
  108. }
  109. attachment, err := repo_model.GetAttachmentByUUID(ctx, attachmentUUID)
  110. if err != nil {
  111. return "", err
  112. }
  113. if attachment.RepoID != b64embedder.repo.ID {
  114. return "", errors.New("attachment does not belong to the repository")
  115. }
  116. if attachment.Size+b64embedder.estimateSize > b64embedder.maxSize {
  117. return "", errors.New("total embedded images exceed max limit")
  118. }
  119. fr, err := storage.Attachments.Open(attachment.RelativePath())
  120. if err != nil {
  121. return "", err
  122. }
  123. defer fr.Close()
  124. lr := &io.LimitedReader{R: fr, N: b64embedder.maxSize + 1}
  125. content, err := io.ReadAll(lr)
  126. if err != nil {
  127. return "", fmt.Errorf("LimitedReader ReadAll: %w", err)
  128. }
  129. mimeType := typesniffer.DetectContentType(content)
  130. if !mimeType.IsImage() {
  131. return "", errors.New("not an image")
  132. }
  133. encoded := base64.StdEncoding.EncodeToString(content)
  134. dataURI := fmt.Sprintf("data:%s;base64,%s", mimeType.GetMimeType(), encoded)
  135. b64embedder.estimateSize += int64(len(dataURI))
  136. return dataURI, nil
  137. }
  138. func fromDisplayName(u *user_model.User) string {
  139. if setting.MailService.FromDisplayNameFormatTemplate != nil {
  140. var ctx bytes.Buffer
  141. err := setting.MailService.FromDisplayNameFormatTemplate.Execute(&ctx, map[string]any{
  142. "DisplayName": u.DisplayName(),
  143. "AppName": setting.AppName,
  144. "Domain": setting.Domain,
  145. })
  146. if err == nil {
  147. return mime.QEncoding.Encode("utf-8", ctx.String())
  148. }
  149. log.Error("fromDisplayName: %w", err)
  150. }
  151. return u.GetCompleteName()
  152. }
  153. func generateMetadataHeaders(repo *repo_model.Repository) map[string]string {
  154. return map[string]string{
  155. // https://datatracker.ietf.org/doc/html/rfc2919
  156. "List-ID": fmt.Sprintf("%s <%s.%s.%s>", repo.FullName(), repo.Name, repo.OwnerName, setting.Domain),
  157. // https://datatracker.ietf.org/doc/html/rfc2369
  158. "List-Archive": fmt.Sprintf("<%s>", repo.HTMLURL()),
  159. "X-Mailer": "Gitea",
  160. "X-Gitea-Repository": repo.Name,
  161. "X-Gitea-Repository-Path": repo.FullName(),
  162. "X-Gitea-Repository-Link": repo.HTMLURL(),
  163. "X-GitLab-Project": repo.Name,
  164. "X-GitLab-Project-Path": repo.FullName(),
  165. }
  166. }
  167. func generateSenderRecipientHeaders(doer, recipient *user_model.User) map[string]string {
  168. return map[string]string{
  169. "X-Gitea-Sender": doer.Name,
  170. "X-Gitea-Recipient": recipient.Name,
  171. "X-Gitea-Recipient-Address": recipient.Email,
  172. "X-GitHub-Sender": doer.Name,
  173. "X-GitHub-Recipient": recipient.Name,
  174. "X-GitHub-Recipient-Address": recipient.Email,
  175. }
  176. }
  177. func generateReasonHeaders(reason string) map[string]string {
  178. return map[string]string{
  179. "X-Gitea-Reason": reason,
  180. "X-GitHub-Reason": reason,
  181. "X-GitLab-NotificationReason": reason,
  182. }
  183. }