@@ -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