@@ -6,16 +6,26 @@ package mailer
66
77import (
88 "bytes"
9+ "context"
10+ "encoding/base64"
11+ "fmt"
912 "html/template"
13+ "io"
1014 "mime"
1115 "regexp"
1216 "strings"
1317 texttmpl "text/template"
1418
19+ repo_model "code.gitea.io/gitea/models/repo"
1520 user_model "code.gitea.io/gitea/models/user"
21+ "code.gitea.io/gitea/modules/httplib"
1622 "code.gitea.io/gitea/modules/log"
1723 "code.gitea.io/gitea/modules/setting"
24+ "code.gitea.io/gitea/modules/storage"
25+ "code.gitea.io/gitea/modules/typesniffer"
1826 sender_service "code.gitea.io/gitea/services/mailer/sender"
27+
28+ "golang.org/x/net/html"
1929)
2030
2131const mailMaxSubjectRunes = 256 // There's no actual limit for subject in RFC 5322
@@ -44,6 +54,107 @@ func sanitizeSubject(subject string) string {
4454 return mime .QEncoding .Encode ("utf-8" , string (runes ))
4555}
4656
57+ type mailAttachmentBase64Embedder struct {
58+ doer * user_model.User
59+ repo * repo_model.Repository
60+ maxSize int64
61+ estimateSize int64
62+ }
63+
64+ func newMailAttachmentBase64Embedder (doer * user_model.User , repo * repo_model.Repository , maxSize int64 ) * mailAttachmentBase64Embedder {
65+ return & mailAttachmentBase64Embedder {doer : doer , repo : repo , maxSize : maxSize }
66+ }
67+
68+ func (b64embedder * mailAttachmentBase64Embedder ) Base64InlineImages (ctx context.Context , body template.HTML ) (template.HTML , error ) {
69+ doc , err := html .Parse (strings .NewReader (string (body )))
70+ if err != nil {
71+ return "" , fmt .Errorf ("html.Parse failed: %w" , err )
72+ }
73+
74+ b64embedder .estimateSize = int64 (len (string (body )))
75+
76+ var processNode func (* html.Node )
77+ processNode = func (n * html.Node ) {
78+ if n .Type == html .ElementNode {
79+ if n .Data == "img" {
80+ for i , attr := range n .Attr {
81+ if attr .Key == "src" {
82+ attachmentSrc := attr .Val
83+ dataURI , err := b64embedder .AttachmentSrcToBase64DataURI (ctx , attachmentSrc )
84+ if err != nil {
85+ // Not an error, just skip. This is probably an image from outside the gitea instance.
86+ log .Trace ("Unable to embed attachment %q to mail body: %v" , attachmentSrc , err )
87+ } else {
88+ n .Attr [i ].Val = dataURI
89+ }
90+ break
91+ }
92+ }
93+ }
94+ }
95+ for c := n .FirstChild ; c != nil ; c = c .NextSibling {
96+ processNode (c )
97+ }
98+ }
99+
100+ processNode (doc )
101+
102+ var buf bytes.Buffer
103+ err = html .Render (& buf , doc )
104+ if err != nil {
105+ return "" , fmt .Errorf ("html.Render failed: %w" , err )
106+ }
107+ return template .HTML (buf .String ()), nil
108+ }
109+
110+ func (b64embedder * mailAttachmentBase64Embedder ) AttachmentSrcToBase64DataURI (ctx context.Context , attachmentSrc string ) (string , error ) {
111+ parsedSrc := httplib .ParseGiteaSiteURL (ctx , attachmentSrc )
112+ var attachmentUUID string
113+ if parsedSrc != nil {
114+ var ok bool
115+ attachmentUUID , ok = strings .CutPrefix (parsedSrc .RoutePath , "/attachments/" )
116+ if ! ok {
117+ attachmentUUID , ok = strings .CutPrefix (parsedSrc .RepoSubPath , "/attachments/" )
118+ }
119+ if ! ok {
120+ return "" , fmt .Errorf ("not an attachment" )
121+ }
122+ }
123+ attachment , err := repo_model .GetAttachmentByUUID (ctx , attachmentUUID )
124+ if err != nil {
125+ return "" , err
126+ }
127+
128+ if attachment .RepoID != b64embedder .repo .ID {
129+ return "" , fmt .Errorf ("attachment does not belong to the repository" )
130+ }
131+ if attachment .Size + b64embedder .estimateSize > b64embedder .maxSize {
132+ return "" , fmt .Errorf ("total embedded images exceed max limit" )
133+ }
134+
135+ fr , err := storage .Attachments .Open (attachment .RelativePath ())
136+ if err != nil {
137+ return "" , err
138+ }
139+ defer fr .Close ()
140+
141+ lr := & io.LimitedReader {R : fr , N : b64embedder .maxSize + 1 }
142+ content , err := io .ReadAll (lr )
143+ if err != nil {
144+ return "" , fmt .Errorf ("LimitedReader ReadAll: %w" , err )
145+ }
146+
147+ mimeType := typesniffer .DetectContentType (content )
148+ if ! mimeType .IsImage () {
149+ return "" , fmt .Errorf ("not an image" )
150+ }
151+
152+ encoded := base64 .StdEncoding .EncodeToString (content )
153+ dataURI := fmt .Sprintf ("data:%s;base64,%s" , mimeType .GetMimeType (), encoded )
154+ b64embedder .estimateSize += int64 (len (dataURI ))
155+ return dataURI , nil
156+ }
157+
47158func fromDisplayName (u * user_model.User ) string {
48159 if setting .MailService .FromDisplayNameFormatTemplate != nil {
49160 var ctx bytes.Buffer
0 commit comments