@@ -6,13 +6,14 @@ package mailer
66
77import (
88 "bytes"
9+ "code.gitea.io/gitea/modules/httplib"
10+ "code.gitea.io/gitea/modules/typesniffer"
911 "context"
1012 "encoding/base64"
1113 "fmt"
1214 "html/template"
1315 "io"
1416 "mime"
15- "net/http"
1617 "regexp"
1718 "strings"
1819 texttmpl "text/template"
@@ -54,42 +55,43 @@ func sanitizeSubject(subject string) string {
5455}
5556
5657type mailAttachmentBase64Embedder struct {
57- doer * user_model.User
58- repo * repo_model.Repository
59- maxSize int64
58+ doer * user_model.User
59+ repo * repo_model.Repository
60+ maxSize int64
61+ estimateSize int64
6062}
6163
6264func newMailAttachmentBase64Embedder (doer * user_model.User , repo * repo_model.Repository , maxSize int64 ) * mailAttachmentBase64Embedder {
6365 return & mailAttachmentBase64Embedder {doer : doer , repo : repo , maxSize : maxSize }
6466}
6567
66- func (b64embedder * mailAttachmentBase64Embedder ) Base64InlineImages (ctx context.Context , body string ) (string , error ) {
67- doc , err := html .Parse (strings .NewReader (body ))
68+ func (b64embedder * mailAttachmentBase64Embedder ) Base64InlineImages (ctx context.Context , body template. HTML ) (template. HTML , error ) {
69+ doc , err := html .Parse (strings .NewReader (string ( body ) ))
6870 if err != nil {
69- return "" , fmt .Errorf ("%w" , err )
71+ return "" , fmt .Errorf ("html.Parse failed: %w" , err )
7072 }
7173
72- var totalEmbeddedImagesSize int64
74+ b64embedder . estimateSize = int64 ( len ( string ( body )))
7375
7476 var processNode func (* html.Node )
7577 processNode = func (n * html.Node ) {
7678 if n .Type == html .ElementNode {
7779 if n .Data == "img" {
7880 for i , attr := range n .Attr {
7981 if attr .Key == "src" {
80- attachmentPath := attr .Val
81- dataURI , err := b64embedder .AttachmentSrcToBase64DataURI (ctx , attachmentPath , & totalEmbeddedImagesSize )
82+ attachmentSrc := attr .Val
83+ dataURI , err := b64embedder .AttachmentSrcToBase64DataURI (ctx , attachmentSrc )
8284 if err != nil {
83- log .Trace ("attachmentSrcToDataURI not possible: %v" , err ) // Not an error, just skip. This is probably an image from outside the gitea instance.
84- continue
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
8589 }
86- n .Attr [i ].Val = dataURI
8790 break
8891 }
8992 }
9093 }
9194 }
92-
9395 for c := n .FirstChild ; c != nil ; c = c .NextSibling {
9496 processNode (c )
9597 }
@@ -100,22 +102,24 @@ func (b64embedder *mailAttachmentBase64Embedder) Base64InlineImages(ctx context.
100102 var buf bytes.Buffer
101103 err = html .Render (& buf , doc )
102104 if err != nil {
103- log .Error ("Failed to render modified HTML: %v" , err )
104- return "" , err
105+ return "" , fmt .Errorf ("html.Render failed: %w" , err )
105106 }
106- return buf .String (), nil
107+ return template . HTML ( buf .String () ), nil
107108}
108109
109- func (b64embedder * mailAttachmentBase64Embedder ) AttachmentSrcToBase64DataURI (ctx context.Context , attachmentPath string , totalEmbeddedImagesSize * int64 ) (string , error ) {
110- if ! strings .HasPrefix (attachmentPath , setting .AppURL ) { // external image
111- return "" , fmt .Errorf ("external image" )
112- }
113- parts := strings .Split (attachmentPath , "/attachments/" )
114- if len (parts ) <= 1 {
115- return "" , fmt .Errorf ("invalid attachment path: %s" , attachmentPath )
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+ }
116122 }
117-
118- attachmentUUID := parts [len (parts )- 1 ]
119123 attachment , err := repo_model .GetAttachmentByUUID (ctx , attachmentUUID )
120124 if err != nil {
121125 return "" , err
@@ -124,6 +128,10 @@ func (b64embedder *mailAttachmentBase64Embedder) AttachmentSrcToBase64DataURI(ct
124128 if attachment .RepoID != b64embedder .repo .ID {
125129 return "" , fmt .Errorf ("attachment does not belong to the repository" )
126130 }
131+ if attachment .Size + b64embedder .estimateSize > b64embedder .maxSize {
132+ return "" , fmt .Errorf ("total embedded images exceed max limit" )
133+ }
134+ b64embedder .estimateSize += attachment .Size
127135
128136 fr , err := storage .Attachments .Open (attachment .RelativePath ())
129137 if err != nil {
@@ -134,26 +142,16 @@ func (b64embedder *mailAttachmentBase64Embedder) AttachmentSrcToBase64DataURI(ct
134142 lr := & io.LimitedReader {R : fr , N : b64embedder .maxSize + 1 }
135143 content , err := io .ReadAll (lr )
136144 if err != nil {
137- return "" , err
138- }
139- if int64 (len (content )) > b64embedder .maxSize {
140- return "" , fmt .Errorf ("file size exceeds the embedded image max limit \\ (%d bytes\\ )" , b64embedder .maxSize )
141- }
142-
143- if * totalEmbeddedImagesSize + int64 (len (content )) > setting .MailService .Base64EmbedImagesMaxSizePerEmail {
144- return "" , fmt .Errorf ("total embedded images exceed max limit: %d > %d" , * totalEmbeddedImagesSize + int64 (len (content )), setting .MailService .Base64EmbedImagesMaxSizePerEmail )
145+ return "" , fmt .Errorf ("LimitedReader ReadAll: %w" , err )
145146 }
146- * totalEmbeddedImagesSize += int64 (len (content ))
147-
148- mimeType := http .DetectContentType (content )
149147
150- if ! strings .HasPrefix (mimeType , "image/" ) {
148+ mimeType := typesniffer .DetectContentType (content )
149+ if ! mimeType .IsImage () {
151150 return "" , fmt .Errorf ("not an image" )
152151 }
153152
154153 encoded := base64 .StdEncoding .EncodeToString (content )
155154 dataURI := fmt .Sprintf ("data:%s;base64,%s" , mimeType , encoded )
156-
157155 return dataURI , nil
158156}
159157
0 commit comments